Tesis de grado de Ingeniería en Informática
Extensión del estándar ���
de ����� para proveer
servicios de estado persistente con acceso remoto: Análisis, diseño e
implementación.
�� �� � ������ ���� � �������������� ��� �� ����� �� ����
Buenos Aires, Argentina. Diciembre 2007
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
3
INFORMACIÓN DE CONTACTO
PABLO MAXIMILIANO ILARDI: [email protected] O [email protected]
MARÍA FELDGEN: [email protected] O [email protected]
Abstract Pablo M. Ilardi
5
RESUMEN
Muchas de las aplicaciones distribuidas tienen requerimientos de persistencia y se encuentran
desarrolladas en lenguajes de programación orientados a objetos (POO) tales como Java. Una de las
arquitecturas, CORBA de OMG, define un estándar de persistencia denominado ����������� ���������� (PSS), siendo una de sus alternativas con un lenguaje PSDL. En este trabajo se analizó el estado
del arte de la persistencia, en particular en el marco de las aplicaciones CORBA desarrolladas en
lenguaje Java y la especificación del servicio PSS. Para estas aplicaciones, se diseñó y construyó un
servicio de estado persistente, junto con un compilador de lenguaje PSDL. Este servicio, además de la
persistencia, permite compartir objetos persistentes entre servants ubicados en distintas máquinas o
procesos, característica que no está prevista en la especificación PSS de CORBA. El compilador y el
servicio construidos se caracterizan por ser extensibles y configurables, soportando distintas formas de
lograr persistencia. Finalmente, los resultados obtenidos fueron comparados con otros servicios PSS.
Palabras clave: Persistencia, OMG, CORBA, Servicio, POO, Java, PSS, PSDL, Compilador, Servant,
Conector, OODB
ABSTRACT
A large number of distributed applications require persistence. Many of these applications are
developed using object oriented programming languages (OOP) like Java. The OMG CORBA
architecture defines the ���������� ���� ������ (PSS) as a standard for persistence. One of the options
is using a language PSDL. In this work, the state of art of persistence was analyzed specifically in the
context of CORBA applications developed in Java, altogether with the PSS specification itself. For these
applications, a persistent state service with a PSDL language compiler was designed and built. The
service includes in addition of persistence, the sharing of persistent objects with servants existing in
different machines or processes, a feature which is not provided by the PSS specification. The built
compiler and service are characterized for being extensible and configurable, supporting different ways
for persisting objects. Finally, the work’s results were compared to other existing PSS services.
Keywords: Persistence, OMG, CORBA, Service, POO, Java, PSS, PSDL, Compiler, Servant, Connector,
OODB
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
7
CONTENIDO
Introducción ................................................................................................................... 9
Introducción a POO - Programación Orientada a Objetos ............................................ 13
Introducción a CORBA................................................................................................... 21
Servicio de Estado Persistente CORBA - PSS ................................................................. 31
Implementación del servicio de estado persistente CORBA - PSS................................. 47
Comparación con otras implementaciones del servicio de estado persistente de CORBA ........................................................................................................................ 121
Trabajos adicionales ................................................................................................... 131
Índice de Gráficos ....................................................................................................... 133
Glosario ...................................................................................................................... 135
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
9
INTRODUCCIÓN
os juegos, sistemas de bases de datos distribuidas, multimedia, aplicaciones gráficas y los sistemas
distribuidos en general, almacenan información que requiere persistencia. La mayoría de los
sistemas de información, requieren de algún soporte para mantener su estado en forma
persistente. Persistencia en la programación orientada a objetos (POO), significa almacenar los objetos y
por consiguiente el valor de sus atributos para uso futuro.
El soporte de persistencia no es trivial, ya que la representación de un objeto en memoria puede
variar en tamaño y estructura de una plataforma a otra. Además, no está soportado en forma nativa por
todos los lenguajes de programación (por ejemplo, C++), plataformas de desarrollo y sistemas operativos.
Por consiguiente, es necesario el uso de frameworks, como por ejemplo, CORBA, para su
implementación.
CORBA (Common Object Request Broker Architecture) o ������������ �� ���� �� ��� �� �������� �� ������ [CR01] es la propuesta del OMG (Object Management Group) para la construcción de
software interoperable, portable y reusable. OMG define una arquitectura de software detallada, que
consta de interfases claramente definidas. Esta arquitectura, llamada OMA (Object Management
Architecture) [OM0], está dividida en dos modelos fundamentales. El primero, llamado ���� � ������ ������ (�� ������ ����), permite el desarrollo de aplicaciones distribuidas basadas en un ��� ���� ������ � ������ u ORB (Object Request Broker). El segundo modelo, llamado ���� �� ������ ���
(������ �� ����), define un marco para el desarrollo de las aplicaciones junto a un conjunto de
interfases estandarizadas llamadas �������� �� ������ (������ ��������) que utilizan la ORB.
Uno de los servicios que CORBA provee, tiene el objeto de facilitar y unificar la forma de hacer
persistentes a los objetos utilizados por los servants [CRO1], por medio de una interfase común para el
manejo de persistencia. Los servants son aquellos objetos que están bajo el control de la ORB. Todo
servant tiene una interfase definida en lenguaje IDL (Interface Definition Language o �� ����� ����� � ���� �� � �������� y puede ser accedido en forma remota por medio de los mecanismos que la ORB
provee a tal fin.
El servicio de CORBA que da soporte de persistencia de objetos se denomina: ������� �� !������������ �� (P"#$%$&"'&S&(&"
S"#)%*") PSS [PS0]. Este servicio no intenta ser una interfase a una base de
datos orientada a objetos OODBMS (Object Oriented Database Management System), sino que provee
una forma estándar de persistencia de objetos. No incluye los dos conceptos fundamentales de OODBMS
como ser las transacciones y las consultas [BgVaDk]. Su fundamento, es la separación de intereses
(+,-./.0123 24 5235,/3+) que está presente en la arquitectura y las especificaciones de servicios CORBA.
De esta forma, si se tienen requerimientos transaccionales no serán implementados por este servicio,
sino que este servicio se conectará o comunicará con el servicio de transacciones CORBA [TS01].
El modelo de trabajo que propone PSS, es similar al modelo general propuesto por CORBA. Los
objetos persistidos por PSS, son denominados 267,02+ .89.5,3.:2+ (+02/.;, 267,50+). Todo objeto
persiste en un .89.5<3 (+02/.;, =29,). Los almacenes existen en /,-2+102/12+. PSS provee un lenguaje
para definir estos objetos y almacenes, llamado PSDL (P>?@A@B>CBSBDB>
D>E ACABAFC
LDCGHDG>), que es una
extensión al lenguaje IDL. Los servants que utilicen PSS, pueden hacer persistentes a los objetos de dos
formas: por su definición en lenguaje PSDL, o por medio de objetos comunes definidos en el lenguaje de
programación que se utilice. La segunda forma de persistencia se la denomina IJKLMLNJOPMQ NKQOLIQKJONJREn ambos casos, los objetos persistidos por los servants, son sólo conocidos por el servant que los define,
y no son exportados a la ORB, lo que implica que los objetos almacenados no pueden ser accedidos en
forma remota.
L
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
10
Para usar el lenguaje PSDL se requiere de un compilador que traduzca las definiciones PSDL en
las definiciones del lenguaje de programación que se utiliza. En cambio, la persistencia transparente,
permite persistir objetos directamente en el lenguaje nativo, pero a costa de perder muchas
funcionalidades que solo están disponibles en las definiciones PSDL.
Si bien PSS es un estándar cuya última revisión es del año 2002, no existen muchas
implementaciones del mismo, dado que no se trata de una de las especificaciones más difundidas.
El problema a resolver es: ¿Cómo hacer uso de objetos que requieran estado persistente en
CORBA? Se deben analizar las soluciones existentes, viendo que alternativas son viables de construir. Las
especificaciones son ambiguas en algunas definiciones, en las cuales se pierde la idea de interfase de
servicio CORBA. En particular, sucede cuando se trata de soportar dos modelos de persistencia, como lo
son el transparente y el de definición por PSDL, por medio de una misma interfase de servicio.
Sin embargo, la construcción de un servicio siguiendo la especificación PSS se plantea como una
idea atractiva y desafiante. Es atractiva, porque permite tener una interfase o modelo estándar para
solucionar este problema no trivial, lo cual es algo muy útil y valioso. Es desafiante, porque se trata de un
problema complejo con muchos matices a considerar, tales como la construcción de un compilador del
lenguaje PSDL, proveer persistencia transparente sin uso de un lenguaje nuevo, definir dónde y cómo
almacenar el estado que define a los objetos utilizando una base de datos u otro medio, evaluar cómo se
podría extender el servicio para que soporte el acceso remoto de los objetos almacenados, no
contemplado en el estándar actual, etc.
OBJETIVOS
Se realizó un estudio del estado del arte de la persistencia en objetos, focalizado principalmente
en CORBA y el lenguaje de programación Java.
Se realizó un análisis de la especificación del servicio de estado persistente (PSS) CORBA,
evaluando sus posibles usos, ventajas y desventajas. Se analizaron las distintas alternativas que plantea el
estándar, considerando los beneficios y las desventajas de la ������������ ������������. Se evaluaron las
diferentes estrategias a seguir frente a ambigüedades en las definiciones. Se evaluaron las distintas
herramientas que pueden utilizarse para mantener el estado persistente, y su posible uso en un servicio
de persistencia.
Se diseñó y construyó un servicio de persistencia, que soporta el estándar CORBA, utilizando el
lenguaje de programación Java y patrones de diseño adecuados para este fin [GA0]. Se proveyó de un
mecanismo básico que permite a dos o más servants acceder en forma remota a un repositorio definido
en otra máquina, pudiendo así acceder a objetos almacenados definidos por otros servants, extendiendo
el servicio que propone el estándar. El servicio de persistencia que se construyó, soporta definiciones
PSDL, junto con las funcionalidades que este lenguaje provee. Para ello, fue necesaria la construcción de
un compilador [CooperRice00] PSDL que genera código Java que es compatible con el servicio construido.
Finalmente, se realizó una comparación entre otras implementaciones del servicio de
persistencia de CORBA y la construida.
ETAPAS DEL TRABAJO
1. REVISIÓN DEL MARCO TEÓRICO
Esta etapa del trabajo se focalizó en obtener un conocimiento del marco teórico necesario para
la realización de este trabajo. Este marco incluye CORBA, Persistencia de Objetos Java y Servicios de
CORBA.
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
11
2. ANÁLISIS
Se realizó un análisis completo de la especificación del servicio de estado persistente de CORBA. Se evaluó el aporte del servicio al desarrollo de aplicaciones CORBA, valorizando sus beneficios y desventajas. Se analizaron los requerimientos para la construcción de un servicio CORBA que cumple con este estándar y permite compartir un repositorio por más de una ORB. Se consideraron las diferentes herramientas, lenguajes de programación y � ��������� que podrían utilizarse en la construcción. Esta etapa definió los requerimientos del servicio a construir.
3. DISEÑO
En esta etapa se diseñó una solución que cumple con los requerimientos relevados durante la
etapa de análisis. Esta solución incluye el diseño de un compilador de lenguaje PSDL a Java, y varios
conectores para el servicio. El diseño obtenido es lo suficientemente amplio, como para permitir futuras
extensiones al servicio en forma de nuevos conectores para distintos tipos de repositorios, tales como
archivos o bases de datos relacionales. La solución también contempla el soporte para su funcionamiento
en distintas ORBs y no sólo para una en particular. El diseño hace uso de distintos patrones de diseño
adecuados para resolver los problemas que se presentaron.
4. CONSTRUCCIÓN
En esta etapa se construyeron los artefactos definidos en la etapa de diseño. La construcción se
realizó en forma incremental, y se proveyeron pruebas unitarias que permiten futuras extensiones al
servicio con otros conectores. Esta forma de construcción se realizó en dos iteraciones entre la etapa de
diseño y construcción, focalizando la primera en el compilador y las herramientas. Entre los artefactos
construidos se encuentran el compilador y el conector que permite compartir repositorios entre distintas
ORBs. También se construyeron herramientas para la generación de código y junto con sus respectivas
pruebas unitarias.
5. EVALUACIÓN DEL SERVICIO
Esta etapa se evaluó el servicio construido en una aplicación CORBA. También se compararon las
funcionalidades y las formas de uso de otras implementaciones de este mismo servicio. El objetivo de
esta etapa fue poner un marco de referencia de este trabajo en relación con otros servicios.
6. REDACCIÓN DEL INFORME Y CONCLUSIONES
Esta epata consistió en la realización de este informe final del trabajo. Se adjuntan también un CDROM con el código fuente de todas las construcciones realizadas para este trabajo, referencias en formato digital, herramientas utilizadas, y este documento en formato digital.
Tesis de Grado – Ingeniería en Informática Pablo M. Ilardi
12
REFERENCIAS
OM0 – “������ �������� ��� �������� �����”. Third Edition June 13, 1995. Richard Mark Soley,
Ph.D. (ed.) Christopher M. Stone
BM0 – “��������������� �������� ������������”. Meyer, Bertrand. Prentice Hall. (1997) ISBN 0-13-
629155-4.
CR01 – “���� ������ ������� ������ ��� ��������� ���� ������������� OMG”.
http://www.omg.org/technology/documents/corba_spec_catalog.htm
PS0 – “Persistent State Service”, V2.0 OMG 2002. http://www.omg.org/cgi-bin/doc?formal/02-09-06
GA0 – “Design Patterns: Elements of Reusable Object-Oriented Software”, Addison-Wesley
Professional Computing Series - by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides,
January 15, 1995.
BgVaDk – “Java Programming with CORBA, Advanced Techniques for Building Distributed
Applications”, 3rd Edition – Gerald Brose, Andreas Vogel, Keith Duddy, Wiley 2001.
TS01 – “Transaction Service Specification”, OMG 2003.
http://www.omg.org/technology/documents/corbaservices_spec_catalog.htm
EisenbergMelton01 – “SQL: 1999, formerly known as SQL3”. Andrew Eisenberg, Sybase, Concord,
MA 01742 [email protected]; Jim Melton Sandy UT 84093 [email protected]
Ramakanth01 – “Object-Relational Database Systems - The Road Ahead”; Ramakanth S.
Devarakonda; http://www.acm.org/crossroads/xrds7-3/ordbms.html
CooperRice00 – “Engineering A Compiler”, Keith Cooper - Rice University, Houston, Texas; Linda
Torczon - Rice University, Houston, Texas. http://www.cs.rice.edu/~keith/
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
13
INTRODUCCIÓN A POO - PROGRAMACIÓN ORIENTADA A OBJETOS
Elementos principales .............................................................................................. 15
Clase ..................................................................................................................... 15
Objeto .................................................................................................................. 15
Atributo ................................................................................................................ 15
Método ................................................................................................................ 16
Mensaje ................................................................................................................ 16
Características:......................................................................................................... 16
Encapsulamiento .................................................................................................. 16
Herencia ............................................................................................................... 16
Abstracción ........................................................................................................... 16
Polimorfismo ........................................................................................................ 17
Ciclo de Vida ............................................................................................................ 17
Persistencia .............................................................................................................. 17
Java .......................................................................................................................... 18
Patrones de Diseño .................................................................................................. 18
Referencias .............................................................................................................. 20
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
15
a Programación Orientada a Objetos (POO) es un paradigma de programación, así como también lo
son el Estructurado y el Funcional que preceden a POO. Un paradigma de programación es un estilo
de programación que conlleva una cierta filosofía. Esta filosofía define una serie de premisas que
son las llamadas mejores prácticas (best practices). Existen múltiples lenguajes de programación para un
paradigma dado. El paradigma define las herramientas que los lenguajes de programación deben proveer
para permitir el desarrollo de aplicaciones de la forma más natural posible [HJ0].
Si bien la POO tiene ya muchos años no fue recién hasta los comienzos de los 90 donde se
popularizó y comenzó a ser utilizado como paradigma de desarrollo de software.
Muchas de las buenas prácticas en otros paradigmas son igualmente válidas en POO, como por
ejemplo la Modularización.
La Modularización tiene como objetivo separar un sistema en módulos, simplificando su diseño,
implementación y entendimiento, así como también facilitar su evolución y mantenimiento. El grado de
efectividad de esta técnica depende del criterio utilizado para descomponer el sistema en módulos [PD0].
La modularización está presente en POO principalmente en las clases. Para algunos autores, las clases
debieran ser los únicos módulos existentes en un sistema [BM0]. En algunos lenguajes, tales como ADA o
Java, existe un concepto que también se podría asociar a los módulos, que es el de paquete. Estos
paquetes se utilizan para agrupar clases, pero no son estrictamente necesarios en el mundo de POO.
La idea básica en POO es resolver un problema planteándolo como un modelo del mundo real en
el que existen objetos que interactúan, a diferencia del modelo de desarrollo estructurado donde se trata
de resolver el problema descomponiéndolo en un conjunto de funciones.
Existen múltiples lenguajes de programación orientados a objetos, entre los que se encuentran:
Smalltalk, Object Pascal, ADA, C++, Java, Eiffel, C#, entre muchos otros.
ELEMENTOS PRINCIPALES
Todo lenguaje que se base en POO debe proveer de algunas herramientas o construcciones
básicas como son las: clases, objetos, métodos, mensajes y atributos.
CLASE Una clase define o modela las características de una “cosa”. Las características incluyen los
atributos o propiedades junto con su comportamiento (métodos). El típico ejemplo es el de un Animal, el
cual tiene propiedades tales como: edad, ubicación, forma de alimentación, etc. su comportamiento
podría estar definido por: alimentarse, dormir, procrear, etc.
OBJETO Un objeto es un individuo de una clase. Los individuos en POO se denominan instancias. En el
ejemplo de los animales un objeto seria un Animal concreto, por ejemplo, un animal en el zoológico, que
tiene un estado determinado, como por ejemplo, 2 años de vida.
ATRIBUTO Un atributo de una clase es análogo a una variable en la programación estructurada, con la
diferencia del contexto donde se define. Los atributos son accesibles desde una instancia de una clase u objeto, mientras que las variables son accesibles desde el contexto en el que se definieron. En la programación más estricta todo es un objeto, o sea un entero o un string es un objeto. Los atributos son tipados. Se puede tener un atributo de tipo Animal y otro de tipo Entero, o un atributo de tipo Objeto, que podría referenciar tanto una instancia de un Animal como de un Entero. Los valores de los atributos de una instancia de un objeto determinan su estado.
L
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
16
MÉTODO
Los métodos de POO son análogos a las funciones de programación estructurada. La diferencia
fundamental es que las funciones o procedimientos se ejecutan en un contexto global, mientras que los
métodos se ejecutan en el contexto del objeto en el cual se llamó al método. Se dice que el conjunto de
métodos de un objeto determinan su comportamiento.
MENSAJE Se entiende por mensaje, a la invocación a un método de un objeto. Esta invocación se realiza
con los parámetros aceptados por el método. El mensaje es el equivalente a una llamada a una función en
el paradigma estructurado.
CARACTERÍSTICAS:
La POO se basa en cuatro características: Encapsulamiento, Herencia, Abstracción y
Polimorfismo.
ENCAPSULAMIENTO El encapsulamiento es una forma de ocultar y agrupar información de forma tal de unificar el
acceso a la misma. El encapsulamiento es generalmente equiparado a “ocultamiento de la información”.
Según [BM0]:
“Debiera ser posible para el autor de una clase especificar los atributos que estarán disponibles
para todos, ninguno o algunos de los clientes de la clase”.
Pero ocultar información, no se refiere solamente a los aspectos físicos de la misma. Por
ejemplo, que una clase Lista utilice un array para almacenar sus elementos no debiera ser visible para un
usuario de esta clase. Los usuarios de la clase debieran acceder a los elementos de una Lista por medio
las operaciones de la misma, por ejemplo: obtenerElementoX. Esto garantiza que los clientes de las clases
se comuniquen por medio de su interfase o contrato sin depender de la implementación de la misma.
HERENCIA La herencia es una característica que permite agrupar funcionalidades comunes en lugares
comunes, es la forma más natural de reutilización en POO. Al igual que en el mundo real, la herencia
refleja que una clase hereda de otra. En POO las clases pueden heredar de otras clases sus características
y comportamiento. Por ejemplo, en la familia de los animales, algunos son de tipo carnívoro y otros de
tipo herbívoro. Una forma de herencia se podría definir como que las clases Carnívoro y Herbívoro
heredan de la clase Animal. Esto implica que ambas tiene las mismas características que cualquier animal
como la edad, peso, etc. pero además cada una tiene sus características propias, por ejemplo, un
Carnívoro se alimenta de distinta forma que un Herbívoro.
Existen dos tipos distintos de herencia: la herencia simple y la herencia múltiple. La primera,
implica que una clase solo puede heredar a lo sumo de una única clase, mientras que la segunda, permite
que una clase pueda heredar de más de una clase al mismo tiempo. No todos los lenguajes de
programación soportan el segundo tipo de herencia, por ejemplo, C++ la soporta y Java no lo hace.
La herencia incluye un nuevo concepto llamado redefinición. Básicamente significa que una clase hija puede redefinir una característica de la clase padre, por ejemplo un atributo o un método.
ABSTRACCIÓN La abstracción permite que dos objetos sean tratados de forma uniforme sin que importe cual
clase sea. Por ejemplo, dos animales, uno Carnívoro y otro Herbívoro como un Perro y un Caballo, pueden
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
17
interpretar un mensaje como dormir. Para quien envía el mensaje dormir, es indistinto si es un Perro o un Caballo el que lo recibe.
POLIMORFISMO El polimorfismo es una de las características esenciales de POO y está muy asociada a la
redefinición y a la abstracción. Cuando un Animal recibe el mensaje alimentarse, que debe hacer,
depende básicamente del tipo de Animal. Los Carnívoros consumirán carne y los Herbívoros vegetales.
Desde el punto de vista del cliente o usuario de una clase de tipo Animal, el mensaje enviado es el mismo,
pero los resultados son muy distintos.
La forma de conseguir un comportamiento similar en programación estructurada, sería mediante
una condición. Por ejemplo, se tiene la función alimentar(animal, alimento): dentro del código fuente de
la función se debe preguntar ‘si animal es carnívoro alimentar_carne() sino alimentar_vegetal()’. Mientras
que en objetos sólo sería necesario escribir: ‘animal.alimentar(alimento)’. El polimorfismo es la propiedad
que permite que un objeto, dado un mismo mensaje responda de manera distinta en función del tipo de
objeto.
CICLO DE VIDA
Para utilizar una instancia de una clase es necesario tener una referencia al objeto, o construir
una instancia de la misma. Para construir una instancia de una clase, se utilizan métodos especiales de las
clases llamados ‘constructores’. Todo constructor devuelve como resultado una instancia de la clase
donde está definido. Cuando la instancia es creada se dice que es referenciada por la variable a la que se
asignó. Una vez creada la instancia de la clase, ya está lista para ser utilizada mediante sus métodos y
atributos.
Toda instancia que está creada ocupa un espacio en memoria. La forma de liberar este espacio
depende principalmente del lenguaje de programación utilizado. Cuando se libera esta memoria, se dice
que es el objeto se destruye.
Los primeros lenguajes de POO requerían que el usuario de los mismos se encargara de alocar y
liberar la memoria utilizada por las instancias de los objetos. Por ejemplo, en C++ es necesario llamar
explícitamente al operador delete para liberar la memoria que el objeto utiliza. Algunos lenguajes más
modernos, liberan la memoria que utilizan los objetos de forma automática, este es el caso de Java, o C#.
PERSISTENCIA
Muchas aplicaciones desarrolladas utilizando POO requieren mantener los objetos utilizados
entre distintas sesiones o ejecuciones de la aplicación. Los objetos que deben ser compartidos entre
distintas sesiones se dicen que son Persistentes, mientras que los que no lo son se dice que son
Transientes. Los objetos no pueden ser mantenidos en memoria entre distintas sesiones, ya que la
memoria es liberada cada vez que se cierre la aplicación donde se utilizan los objetos. Es necesario
almacenar los objetos en algún repositorio permanente.
Si el estado de un objeto está definido por el valor de sus atributos, se puede asumir que para
recuperar un objeto de un repositorio permanente, es suficiente con almacenar los valores de sus
atributos. Pero esto no es suficiente cuando estos atributos son otros objetos con sus propios atributos o
cuando estos atributos referencian a otros objetos que pueden referenciar al objeto inicial que se quería
presistir. Los objetos en memoria pueden constituir un grafo de dependencias complejo que puede incluir
ciclos definidos por referencias cruzadas. Un mecanismo de persitencia que pueda almacenar en forma
automática el objeto junto con estas dependecinas cruzadas, se dice que soporta persistence closure o
persitencia cerrada [BM0].
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
18
Cuando los objetos necesitan ser compartidos entre distintas sesiones, y además ser
compartidos entre distintas aplicaciones, se agrega la necesidad de la existencia de una bases de datos u
objetos. Las bases de datos pueden ser de tipo relacional (RDBMS) que require un mecanismo de
traducción entre el modelo de objetos y el modelo relacional, o pueden ser sistemas de bases de datos
orientadas a objetos (OODBS) que soportan los objetos en forma nativa.
Algunos mecanismos de persitencia proveen soluciones para la evolución del esquema [BM0]. La
evolución del esquema se aplica cuando se modifica una clase y para dicha clases ya existen instancias de
objetos con la estructura original de la clase almacenadas en un repositorio. Cuando se recuperan dichas
intancias puede ocurrir que ya no esté disponible la clase original y no sea posible recuperarla con la
nueva clase ya que no existe algún atributo de la misma. A este problema se lo denomina object
mismatch o incompatibilidad de objeto. La incompatibilidad de objetos no ocurre solamente cuando se
recupera un objeto de un repositorio donde persiste, también puede ocurrir cuando se transmite un
objeto por la red, y el receptor del mismo posee una versión distinta de la clase a la que pertenece el
objeto transmitido.
La persistencia de los objetos no está ligada directamente a la POO, cada lenguaje puede proveer
o no, herramientas que permitan la persistencia de los objetos utilizados.
JAVA
Java es un lenguaje de programación orientado a objetos cuya sintaxis deriva de C y C++, pero
que provee un modelo de objetos más simple que C++. Java fue creado por James Gosling [JavaTech01] y
por la empresa SUN en el año 1995, como parte de una plataforma llamada Java Virtual Machine o
Máquina Virtual Java. Desde entonces ha evolucionado hasta su versión 6. La máquina virtual permite
que un programa escrito y compilado en Java pueda ejecutarse en cualquier sistema operativo y
arquitectura sin ser modificado. El sistema operativo requiere tener una implementación de la máquina
virtual instalada. Java se compila en código de bytes o bytecode, y no en código binario. El bytecode es la
representación interna que entiende la máquina virtual, y es lo que permite la independencia de
arquitectura y sistema operativo.
Una de las características principales de Java y que lo diferencian fundamentalmente de C++, es
el manejo de memoria. Java provee manejo automático de memoria, lo que implica que el programador
no debe encargarse de pedir y liberar la memoria necesaria para la creación y destrucción de los objetos.
Esta característica se implementa por medio de automatic garbage collection o recolección automática
de basura [HoskingMoss01]. Este mecanismo permite que un objeto sea eliminado cuando no existan
referencias accesibles a él desde los objetos activos. La máquina virtual es la encargada de llevar el
control de las referencias a los objetos y de administrar la memoria utilizada.
Existen algunas otras diferencias importantes con C++ [TateB01]. Java no permite herencia
múltiple de clases, aunque si permite herencia múltiple de interfases. Las interfases son definiciones de
comportamientos que pueden tener o implementar las clases, pero sin su implementación. Son análogas
a las clases abstractas con todos sus métodos abstractos de C++. En Java todos los métodos son
potencialmente polimórficos, mientras que en C++, un método debe ser declarado virtual para que pueda
comportarse en forma polimórfica. Java no posee destructores ni operadores redefinibles por el
programador. En Java las clases son definidas en paquetes y un paquete puede contener múltiples clases
y sub paquetes.
PATRONES DE DISEÑO
El diseño de sistemas en lenguajes orientados a objetos no es una tarea simple, se deben
considerar múltiples factores que permitan reutilización, bajo acoplamiento y alta cohesión de las clases
que conformen el sistema.
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
19
Generalmente, el mejor diseño no se obtiene en un solo paso, sino a través de iteraciones que lo
refinen. Cuando se plantea diseñar una solución a un problema, se trata de reutilizar soluciones
anteriores buscando analogías a otros problemas similares. Muchos de los problemas que se plantean al
realizar un diseño, fueron afrontados por otros diseños anteriores de la misma u otras personas. Las
soluciones a estos problemas recurrentes siguen patrones de comunicación y estructuras de clases
comunes.
Estos patrones son los llamados Design Patterns o Patrones de Diseño que permiten reutilizar
diseños y arquitecturas exitosas. Un patrón de diseño fue definido por Erich Gamma [GA01] como:
“Un patrón de diseño nombra, motiva y explica en forma sistemática un diseño general que
soluciona un problema recurrente en sistemas orientados a objetos. Describe el problema, da su solución,
y define cuando es aplicable, determinando las consecuencias de la solución. También da ejemplos y
lineamientos para su implementación. La solución es un esquema general de objetos y clases que
solucionan el problema. La solución se debe adaptar e implementar para resolver el problema en el
contexto particular”.
Los patrones de diseño se popularizaron a partir de la tesis doctoral de Erich Gamma [GA02], que
luego fue ampliada con nuevos patrones y publicada en el libro “Design Patterns: Elements of Reusable
Object-Oriented Software” [GA01] o “Patrones de Diseño: Elementos de Software Orientado a Objetos
Reutilizable”.
Algunos de los patrones de diseño más difundidos son: Abstract Factory, Adapter, Composite,
Decorator, Factory Method, Observer, Strategy, Template Method, etc.
Introducción a POO – Programación Orientada a Objetos Pablo M. Ilardi
20
REFERENCIAS
AD0 – Armstrong, Deborah J. “The Quarks of Object-Oriented Development”. Communications of the
ACM 49 (February 2006): 123-128. ISSN 0001-0782.
BM0 – Meyer, Bertrand. “Object-Oriented Software Construction”. Prentice Hall PTR, 1997, 2da
Edición, ISBN 0136291554.
CO0 – “Software engineering: Report of a conference sponsored by the NATO” Science Committee.
Peter Naur, Brian Randell (eds.) Garmisch, Germany, 7–11 October 1968, Brussels, Scientific Affairs
Division, NATO (1969).
HJ0 – John Hunt. “Samlltalk and Object Orientation: An Introduction”. JayDee Technology Ltd,
Hartham Park Corsham, Wiltshire, SN13 0RP United Kingdom.
PD0 – David Parnas, "On the Criteria to Be Used in Decomposing Systems Into Modules".
Communications of the ACM in December, 1972.
VRH0 –Peter Van Roy, Seif Haridi, “Concepts Techniques and Models of Computer Programming”. The
MIT Press, Cambridge, Massachusetts. London, England 2004. ISBN 0-262-22069-5
JavaTech01 – “Java Technology: The Early Years", by Jon Byous.
http://java.sun.com/features/1998/05/birthday.html
HoskingMoss01 – “Protection traps and alternatives for memory management of an object-oriented
language”. Antony L. Hosking J. Eliot B. Moss. Object Systems Laboratory Department of Computer
Science University of Massachusetts Amherst, MA 01003.
ftp://ftp.cs.purdue.edu/pub/hosking/papers/sosp93.pdf
TateB01 – "Beyond Java", by Bruce A. Tate. O'Reilly, September 2005. ISBN 0-596-10094-9
GA01 – “Design Patterns: Elements of Reusable Object-Oriented Software”, Addison-Wesley
Professional Computing Series - by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides,
January 15, 1995.
GA02 – “Object-Oriented Software Development based on ET++: Design Patterns, Class Library,
Tools”. Erich Gamma PhD thesis, University of Zurich Institut für Informatik, 1991.
Introducción a CORBA Pablo M. Ilardi
21
INTRODUCCIÓN A CORBA
OMG y CORBA _______________________________________________________ 23
OMA _______________________________________________________________ 23
Object Services _____________________________________________________ 23
Common Facilities __________________________________________________ 23
Domain Interfases __________________________________________________ 24
Principios de Diseño en CORBA__________________________________________ 24
Características de CORBA ______________________________________________ 24
IDL Interface Definition Language ______________________________________ 24
Pasaje de Parámetros: _______________________________________________ 25
Mapeos de lenguajes (Language Mappings) ______________________________ 25
Invocación de Operaciones y facilidades de despacho (dispatching) ___________ 25
Object Adapters (OA) ________________________________________________ 26
Protocolo Inter-ORB _________________________________________________ 26
Un request en CORBA _________________________________________________ 27
Object References __________________________________________________ 27
Adquisición de OR ________________________________________________ 27
Proceso para la invocación de un request ________________________________ 27
Las características de la invocación _____________________________________ 27
Contenedor de Componentes CORBA ____________________________________ 28
Referencias _________________________________________________________ 29
Introducción a CORBA Pablo M. Ilardi
23
n Sistema Distribuido es un conjunto de computadoras independientes que se presentan al
usuario del sistema como un único sistema. Desde la perspectiva de hardware, las máquinas o
computadoras son autónomas, pero desde el punto de vista del software, el sistema se ve por el
usuario como un todo [Tanenbaum01].
La construcción de este tipo de sistemas plantea desafíos que no se presentan en los Sistemas
Centralizados. Las computadoras que se interconectan son heterogéneas y pueden tener arquitecturas
diversas. La comunicación conlleva latencia y fallos que deben ser contemplados. Esta problemática
impone la necesidad de un modelo de abstracción que simplifique la construcción y diseño de los
Sistemas Distribuidos [TariBukhers01]. La especificación CORBA de OMG provee un modelo para estos
problemas.
OMG Y CORBA
OMG (Object Management Group) o Grupo de Administración de Objetos, es una organización o
consorcio internacional fundado en 1989 que trata de establecer los lineamientos y modelos para el
desarrollo de aplicaciones reusables, portables e interoperables en ambientes distribuidos heterogéneos,
bajo un modelo de objetos e interfases claramente definidas. OMG ha recibido un gran apoyo de la
industria, transformándose en el consorcio de software más grande del mundo.
CORBA (Common Object Request Broker Arquitecture) o Arquitectura Común para el Agente de
Pedidos de Objetos [CR0] es el núcleo del modelo de arquitectura propuesto por OMG, llamado OMA.
OMA
Esta arquitectura, llamada OMA (Object Management Architecture) [OM0], está dividida en dos
modelos fundamentales. El primer modelo, llamado Modelo Central de Objetos (Core Object Model),
permite el desarrollo de aplicaciones distribuidas basadas en un Agente de Pedidos a Objetos u ORB
(Object Request Broker). Se trata de un modelo con definiciones abstractas que no poseen ninguna
implementación y permiten interpretar el modelo de objetos e interfases. Este modelo fija las bases para
CORBA. Está enfocado en el diseño e implementación de la ORB y no en la construcción de aplicaciones.
CORBA es una especialización de los conceptos definidos en este modelo, llevando las definiciones
abstractas a construcciones concretas.
El segundo modelo, llamado Modelo de Referencia (Reference Model), define a la ORB junto a un
grupo de interfases estandarizadas, como el núcleo para la construcción de aplicaciones distribuidas.
Estas interfases son: los Servicios de Objetos (Object Services) [COS0], las Utilidades Comunes (Common
Facilities) [CCF0] y las Interfases de Dominio (Application Domain Interfases). El modelo de referencia es el
utilizado por los desarrolladores de aplicaciones.
OBJECT SERVICES
Los servicios de objetos (Object Services) son un conjunto de interfases de servicios y objetos que
soportan operaciones básicas para la implementación y utilización de objetos. Estos servicios permiten la
construcción de aplicaciones distribuidas independientes del dominio. Los servicios definen operaciones
genéricas que se aplican a objetos de cualquier dominio, por ejemplo acceder a un objeto por algún
identificador. Existen servicios, tales como Naming Service o Trading Service, que proveen referencias a
objetos que permiten accederlos desde cualquier ubicación. Algunos de los servicios de objetos son:
Naming Service [NS01], Persistence State Service [PSS01], Transaction Service [TSS01], etc.
COMMON FACILITIES
U
Introducción a CORBA Pablo M. Ilardi
24
Las utilidades comunes (Common Facilities), son también servicios u objetos, pero no esenciales
para la construcción de aplicaciones, como los servicios de objetos. Su existencia permite que diferentes
aplicaciones puedan beneficiarse compartiéndolos, sin tener que implementar su funcionalidad en cada
aplicación. Un ejemplo de estas utilidades son los servicios para el manejo de correo electrónico.
DOMAIN INTERFASES Las interfases de dominio corresponden a grupos de interfases generalizadas para dominios de
aplicación particulares, tales como: telecomunicaciones, internet, salud, etc.
PRINCIPIOS DE DISEÑO EN CORBA
OMA define lineamientos que se deben respetar en el diseño de interfases de servicios de CORBA:
• Separación de la interfase de su implementación.
• Las referencias a objetos deben ser tipadas por interfases.
• Los clientes sólo dependen de interfases, no de implementaciones.
• Las interfases permiten herencia múltiple.
• Para especializar o extender funcionalidades se utilizan subtipos.
• Se asume que las implementaciones de los servicios son eficientes al cumplir su cometido, de forma tal que la eficiencia no sea un problema para los clientes.
CARACTERÍSTICAS DE CORBA
La ORB se puede interpretar como un canal de comunicación que transporta pedidos y
respuestas desde y hacia objetos en cualquier ubicación, sin importar como estén implementados. La
noción de transparencia es fundamental en CORBA. Un objeto puede ser invocado independientemente
de su ubicación física, por medio de este canal. La transparencia también se aplica al lenguaje de
programación. Se pueden realizar pedidos a objetos independientemente del lenguaje en el cual estén
implementados, por medio del lenguaje de definición de interfases o IDL que es común a todas las
implementaciones y usuarios.
IDL INTERFACE DEFINITION LANGUAGE Uno de los lineamientos que permite la abstracción de CORBA, es la separación de las interfases
de los objetos de su implementación. Las interfases se definen en un lenguaje provisto por CORBA para
este propósito, llamado IDL (Interface Definition Language o Lenguaje de Definición de Interfases). IDL no
permite definir la implementaciones. El hecho de que la definición se realice en un lenguaje genérico,
permite que la especificación sea independiente del lenguaje de implementación de los objetos.
IDL soporta todos los tipos básicos del lenguaje C, enumerados y relaciones entre otras
interfases. La definición de interfases, también soporta herencia en el formato diamante [CR0]. Toda
interfase que no declare heredar de otra en forma explícita, hereda en forma implícita de la interfase IDL
Object. De esta forma, todos los objetos CORBA, tienen una interfase común.
La definición IDL, es compilada en un lenguaje específico, como puede ser C, C++. Java, Fortran,
etc. El mecanismo de compilación de la definición IDL, se concreta según la especificación de CORBA,
denominada language mappings (o mapeos de lenguaje). El código que genera el proceso de compilación
de un segmento de código en lenguaje IDL, es sólo la cáscara para la implementación. Para generar un
objeto servidor o servant, un desarrollador toma esta cáscara e implementa el cuerpo de las operaciones
o funciones que se hayan definido en la interfase.
Cuando se compila un archivo que tiene definiciones IDL, para el caso de C++, por lo general se
crean cuatro archivos. Un archivo contiene las interfases C++, otro contiene las operaciones de la
interfase, otro contiene la implementación C++ del cliente (stubs), y el último contiene la implementación
Introducción a CORBA Pablo M. Ilardi
25
base del servidor o esqueleto (skeleton) de la misma, ya que el cuerpo de cada función en particular es lo
que se desarrolla a posteriori.
Cuando se comunican diferentes implementaciones de ORBs, no se pueden compartir los
archivos generados por el compilador IDL, ya que el código que éste genera es propietario. No tiene
sentido hablar de compartir código si se comunica una ORB que funciona con Java y otra con C++.
Mediante el uso del IDL, se puede llamar a una operación definida en una interfase IDL, que esté
implementada en un objeto programado en otro lenguaje y corriendo en otra ORB.
PASAJE DE PARÁMETROS: La definición de una operación requiere que se especifique el modo de pasaje de sus
parámetros. Las formas permitidas son: in, out, e inout.
- in: el argumento se pasa del cliente al servidor. - out: el argumento es devuelto al cliente, desde el servidor. - inout: el argumento es inicializado por el cliente, modificado por el servidor, y devuelto al cliente
nuevamente.
Dado que IDL, carece del concepto de punteros, tiene que existir una forma más eficiente de
pasar por parámetro, estructuras más complejas que un tipo de dato simple. Cuando se compila una
operación de una interfase que recibe como parámetro a otra interfase, se pasa una referencia al objeto
que implemente este parámetro, lo que es análogo a un “puntero”.
MAPEOS DE LENGUAJES (LANGUAGE MAPPINGS) Los Lenguaje Mappings especifican como se mapean las interfases IDL a un lenguaje
determinado. Para cada construcción IDL, el mapeo del lenguaje define como se traduce a ese lenguaje.
Por ejemplo, en C++ las interfases IDL se mapean a clases, mientras que en Java [IDJ0], se mapean a
interfases públicas.
Las referencias a objetos en C++ se mapean a una construcción que soporta el operator->,
mientras que en C, se mapean a un void *.
De igual forma, los mapeos de lenguajes especifican como utilizar las facilidades que provee la
ORB (ORB facilities), y como las aplicaciones servidoras implementan los servants.
Existen múltiples mapeos de lenguajes que permite que las aplicaciones distribuidas puedan ser
construidas en partes y utilizando múltiples lenguajes.
La independencia del lenguaje que plantea CORBA, es la clave para interconectar sistemas
heterogéneos.
INVOCACIÓN DE OPERACIONES Y FACILIDADES DE DESPACHO (DISPATCHING) Las aplicaciones CORBA funcionan recibiendo pedidos o request, o emitiendo request a objetos
CORBA. Los tipos de invocación que soporta CORBA son:
- Invocación estática: se realiza mediante una llamada a los stubs generados por el compilador IDL. El servant hereda o modifica el skeleton generado por el compilador IDL.
- Invocación dinámica: este mecanismo requiere la construcción del request en tiempo de ejecución. Dado que no se tiene información en tiempo de compilación, la creación e interpretación de los requests, requiere el acceso a los servicios que permitan obtener dicha información. Este servicio puede estar implementado, por ejemplo, mediante la interrogación a un operador que brinde la información necesaria, o utilizando el Interfase Repository Service, que es un servicio que provee acceso a interfases IDL en tiempo de ejecución.
Introducción a CORBA Pablo M. Ilardi
26
La invocación estática es comúnmente utilizada por los desarrolladores cuando se implementan
aplicaciones que utilicen interfases bien definidas. Mientras que el mecanismo dinámico es utilizado,
generalmente, por dispositivos que procesen información que a priori es desconocida, tales como los
bridges, o gateways.
OBJECT ADAPTERS (OA) Los OA son el nexo entre los servants y la ORB, corresponden al patrón de diseño Adapter [GA0].
Un objeto adapter adapta la interfase de un objeto a otra esperada por el emisor. De esta forma, permite
a un emisor comunicarse con un receptor sin conocer realmente cual es su interfase.
Los Object Adapters cumplen con tres requerimientos básicos:
1- Crean Object References, que permiten a los clientes acceder a los objetos. 2- Aseguran que cada objeto a invocar o target, esté encarnado o representado por un servant. 3- Toman los request que llegan a la ORB en la cual se aloja el objeto target y se los transmite
al servant que es la encarnación del target.
Esto permite a la ORB separarse de los diferentes tipos de servants que puedan existir. De esta
forma, la única responsabilidad relacionada con el request de la ORB es entregarlo al OA que es el que
realmente se encarga de comunicarse con el servant.
En C++ [HeVi0] un servant, es una clase que probablemente herede de un skeleton generado por
el mapeo del IDL al lenguaje. Para implementar las operaciones, se deben sobrescribir los métodos
virtuales. En el caso de Java [BgVaDk0], se deben implementar los métodos de la interfase que defina el
skeleton. El servant se registra con el OA, permitiéndole a la encarnación de las interfases que el servant
represente, recibir los request de los clientes.
Hasta la versión 2.1 de CORBA existía una especificación base del OA, llamada Basic Object
Adapter (BOA) solamente para C. La versión 2.2 de CORBA introdujo el Portable Object Adapter (POA),
para solucionar los inconvenientes que tenia BOA. El POA, mejoró mucho la relación entre los objetos
CORBA, y las encarnaciones servants. Como resultado de esto, la especificación de BOA fue eliminada de
CORBA.
PROTOCOLO INTER-ORB Antes de CORBA 2.0, una de las principales desventajas que se le criticaban a CORBA, era la
inexistencia de una especificación de protocolo para comunicar ORBs. Por ello, cada proveedor de ORB
debía definir el protocolo de red que usaba para comunicar la ORB. Lo que daba como resultado el
aislamiento de las distintas implementaciones de ORB, ya que cada una contaba con un protocolo
propietario.
En CORBA 2.0, se introdujo una arquitectura para la interoperabilidad de distintas
implementaciones de la ORB llamada General Inter-ORB Protocol (GIOP se pronuncia “gee-op”). GIOP es
un protocolo abstracto que define la sintaxis para la transmisión y el conjunto de formatos de mensajes
que permite a cualquier implementación de ORB comunicarse sobre una capa de transporte orientada a
la conexión. Internet Inter-ORB Protocol (IIOP se pronuncia “eye-op”) especifica como GIOP se mapea
sobre TCP/IP.
Para la interoperabilidad de las ORBs se requiere un formato estándar de Object References. Este
formato se denomina Interoperable Object Reference (IOR), y es lo suficientemente flexible como para
permite almacenar cualquier tipo de IOP. Los IOR, identifican uno o más protocolos soportados (IOPs).
Para cada protocolo, el paquete IOR contiene la información propietaria del mismo. En el caso de IIOP, el
Introducción a CORBA Pablo M. Ilardi
27
IOR contiene el nombre del host, el puerto TCP/IP, y una clave de objeto que identifica al objeto target,
para la combinación host - puerto.
UN REQUEST EN CORBA
OBJECT REFERENCES Las Object References (OR), son análogas a los punteros a objetos de C++ o las referencias de
Java.
- Cada OR identifica una única instancia de objeto. - Pueden existir múltiples OR que referencien al mismo objeto. - La referencia puede ser null (nil reference) - Las referencias pueden apuntar a objetos eliminados. - Son opacas, los cliente no conocen el contenido de las mismas. (deber ser tratadas como caja
negra) - Son fuertemente tipadas. - Soportan late-binding (soporta el polimorfismo de C++, a diferencia del de RPC). - Pueden ser persistentes (las referencias pueden haber sido almacenados en disco para luego ser
recuperadas nuevamente). - Pueden ser interoperables (diferentes implementaciones de ORBs, pueden intercambiar OR).
ADQUISICIÓN DE OR
La única forma de acceder a un objeto CORBA es a través de las OR. Cada servidor puede
publicar las ORs que contiene. La forma más natural de adquirir una OR por un cliente es la de recibirla
como respuesta a una invocación a un servicio. Otra forma es la de buscarla en un servicio como Naming
o Trading Service.
PROCESO PARA LA INVOCACIÓN DE UN REQUEST Cuando un cliente invoca una operación por medio de la OR de un objeto, la ORB hace lo
siguiente:
- Ubica el objeto target. - Activa la aplicación servidor, si es que esta no está activa. - Transmite los argumentos para la operación. - Activa el servant para el request si es necesario. - Espera que se complete el request. - Devuelve todos los parámetros out, o inout, y el resultado del request. - Alternativamente devuelve una excepción, en conjunto con los datos, cuando el request falle.
El mecanismo para el cliente es completamente transparente y lo ve como la invocación de
cualquier otro método en un objeto no controlado por la ORB.
LAS CARACTERÍSTICAS DE LA INVOCACIÓN
- Transparencia de la ubicación: El cliente no sabe si el objeto es local al proceso en el que corre, o si se encuentra en otro proceso de la misma máquina, o si realmente se encuentra en otro proceso de otra máquina. Los procesos servidores, no están obligados a permanecer en la misma máquina eternamente, pueden ser movidos de máquina en máquina sin que los clientes sean conscientes de ello.
- Transparencia del servidor: El cliente no necesita saber cual es el servidor que implementa el objeto.
- Independencia del lenguaje: Al cliente no le interesa saber en cual lenguaje está implementado el objeto que está invocando.
Introducción a CORBA Pablo M. Ilardi
28
- Independencia de la implementación: El cliente no necesita saber como funciona la implementación. Incluso los objetos servidores, no necesariamente deben estar implementados en un lenguaje orientado a objetos.
- Independencia de la arquitectura: El cliente no conoce realmente el tipo de máquina en la que está corriendo el objeto.
- Independencia del sistema operativo: no se sabe realmente cual es el sistema operativo que está corriendo el servidor.
- Independencia del protocolo: El cliente no conoce el protocolo que se está utilizando para comunicarse. La ORB, es la encargada de seleccionar, en tiempo de ejecución, el protocolo que utilizará para comunicarse.
- Independencia del Transporte: El cliente desconoce el transporte o topología de la red que se utilice para la comunicación. Se pueden utilizar distintos tipos de red sin que el cliente sea consciente de ello.
CONTENEDOR DE COMPONENTES CORBA
La versión 3.0 de CORBA incluye un nuevo modelo de trabajo, denominado modelo de
componentes [CCM01]. Los componentes extienden el modelo de objetos de CORBA (las interfases de
IDL), y promueven la composición en vez de la herencia.
El modelo de componentes de CORBA toma características del modelo de componentes de Java,
llamado EJB (Enterprise Java Bean), y del modelo de componentes de Microsoft llamado COM
(Component Object Model), pero a diferencia de ambos no está basado en un lenguaje particular o en un
único entorno como Windows.
CCM o CORBA Component Model (Modelo de Componentes CORBA) promueve un modelo de
desarrollo de aplicaciones, donde se oculta parte de la complejidad de CORBA. Los componentes de
CORBA tienen acceso a los servicios estándar de CORBA, tales como el de Transacciones, Seguridad,
Persistencia o Eventos.
Todo componente se instala en un contenedor de componentes. La especificación CCM
introduce el concepto de componente y un conjunto compuesto por interfases, técnicas para especificar
la implementación, empaquetado, y despliegue (deploy) o instalación de los componentes.
Los componentes implementan al menos una interfase, pero además permiten definir a los otros
componentes que se requieren para que un componente pueda funcionar. Un componente también
puede exponer atributos que permiten configurarlos en el contenedor donde sean instalados. Además, se
define un modelo de conexión de componentes mediante eventos, que permite independencia de
interfases de los componentes conectados. Este modelo de conexión sigue el patrón de diseño Observer
[GA0].
Todas estas nuevas características son definidas mediante un nuevo lenguaje llamado CIDL (Component Implementation Definition Language), que extiende al lenguaje PSDL [PSS01] y al IDL versión 3.0 de CORBA.
Introducción a CORBA Pablo M. Ilardi
29
REFERENCIAS
Tanenbaum01 – “Distributed Operating Systems”, Andrew, S. Tanenbaum, Practice-Hall Inc 1996.
ISBN 0-13-219908-4
TariBukhers01 – “Fundamentals of Distributed Object Systems, the CORBA Perspective”, Zahir Tari,
Omran Bukhers, Willey Inc. 2001. ISBN 0-471-35198-9
CR0 – Object Management Group, “Common Object Request Broker Architecture”, version 3.0.3,
OMG document number formal/04-03-01. http://www.omg.org/cgi-bin/doc?ptc/04-03-01
OM0 – Richard Mark Soley, “Object Management Architecture Guide”, June 13, 1995, Ph.D. (ed.)
Christopher M. Stone
COS0 – CORBAservices: Common Object Services Specification, Updated: November 1997. ftp://ftp.omg.org/pub/docs/formal/98-07-05.pdf CCF0 – CORBAfacilities: Common Facilities Architecture. ftp://ftp.omg.org/pub/docs/formal/97-06-08.pdf NS01 – Object Management Group, “Naming Service Specification”, version 1.1. February 2001. OMG document number formal/01-02-65. http://www.omg.org/cgi-bin/doc?formal/01-02-65 PSS01 – Object Management Group, “Persistent State Service”, version 2.0. September 2002. OMG document number formal/02-09-06. http://www.omg.org/cgi-bin/doc?formal/02-09-06 TSS01 – Object Management Group, “Transaction Service Specification”, version 1.4. September 2003. OMG document number formal/03-09-02. http://www.omg.org/cgi-bin/doc?formal/03-09-02 IDJ0 - Object Management Group, “Java™ to IDL Language Mapping Specification”, version 1.3. September 2003. OMG document number formal/03-09-04. http://www.omg.org/cgi-bin/doc?formal/03-09-04 GA0 – Erich Gamma, “Design Patterns: Elements of Reusable Object-Oriented Software”, Addison-
Wesley Professional Computing Series, Richard Helm, Ralph Johnson, John Vlissides, January 15,
1995.
BgVaDk0 – Gerald Brose, Andreas Vogel, Keith Duddy, “Java Programming with CORBA, Advanced
Techniques for Building Distributed Applications”, 3rd Edition, Wiley 2001.
HeVi0 – Michi Henning, Steve Vinoski, “Advanced CORBA Programming with C++”. Addison Wesley.
February 12, 1999. ISBN: 0-201-37927-9
CCM01 – CORBA “Component Model Specification”, OMG Available Specification Version 4.0
formal/06-04-01. http://www.omg.org/cgi-bin/doc?formal/06-04-01
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
31
SERVICIO DE ESTADO PERSISTENTE CORBA - PSS
Conceptos Fundamentales en el Servicio ................................................................. 33
Identificación ........................................................................................................... 35
Tipos y Herencias. .................................................................................................... 35
Claves ....................................................................................................................... 36
Uso del Servicio de Persistencia ............................................................................... 36
Acceso al Servicio de Persistencia ............................................................................ 37
Transacciones .......................................................................................................... 38
Construcciones en PSDL ........................................................................................... 38
Características de las construcciones básicas PSDL ............................................... 39
AbstractStorageObject ...................................................................................... 39
AbstractStorageHome ....................................................................................... 40
StorageObject ................................................................................................... 40
StorageHome .................................................................................................... 41
Mapeo del lenguaje PSDL a un lenguaje concreto ................................................... 41
Diferencias entre C++ y Java del servicio de persistencia ...................................... 41
Críticas a la especificación ....................................................................................... 42
Notas finales sobre el servicio de estado persistente .............................................. 43
Alternativas a PSS en Java ....................................................................................... 43
Referencias .............................................................................................................. 45
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
33
n 1997 OMG creó un RFP (Request for Proposal) para la creación de un servicio CORBA. Se trató de
un servicio de persistencia de objetos [PSS01]; Una RFP consiste en el llamado a entidades a
participar en la creación de una nueva especificación.
El servicio de persistencia de CORBA tiene el objeto de facilitar y unificar la forma de hacer
persistente el estado de los servants [CORBA01] y fue denominado: Servicio de Estado Persistente
(Persistent State Service) PSS.
El hacer persistente el estado del objeto significa que su estado sobrevive o se mantiene sin
importar cuantas veces el objeto sea encarnado o destruido. No debe confundirse el persistir referencias
a servants, con el hacer persistente el estado de un objeto. El hacer persistente las referencias a los
servants significa guardar en forma persistente una referencia a un servant y no el estado del mismo.
En el año 2000, PSS reemplazó una especificación anterior del año 1994, denominada POS
Persistent Object Service [POS01], que fue muy poco aceptada y ampliamente criticada [KjPfTp].
Actualmente está definida la revisión 2 de PSS que data de Septiembre del 2002.
PSS no intenta ser una interfase a una base de datos orientada a objetos OODBMS, sino el de
proveer una forma estándar de hacer persistentes a los objetos. No define los conceptos fundamentales
en OODBMS como transacciones y consultas [BgVaDk]. Su fundamento es la separación de intereses
(separation of concerns) que está presente en todas la arquitectura y especificaciones de servicios CORBA
[CORBA01]. De esta forma, si se tienen requerimientos transaccionales, éstos no serán implementados
por este servicio sino que este servicio se conectará o comunicará con el servicio de transacciones CORBA
[TS01]. Una de las características que fue mas ampliamente criticada a POS fue no usar los otros servicios
que CORBA provee [KjPfTp].
PSS está basado en dos lenguajes de programación: C++ y Java. Aunque su modelo tiene mas
similitudes con el segundo que con el primero.
CONCEPTOS FUNDAMENTALES EN EL SERVICIO
Según la especificación, los clientes de los servants son ajenos a este servicio y no tienen forma
de saber si se está usando el mismo. El PSS es visible solo dentro de la implementación de los servants.
Según muestra el Gráfico 1, el cliente de la ORB que accede al servant, no accede al PSS directamente
sino que lo hace a través del Servant.
Interfa
ce P
SS
(PS
DL
)
Dominio de ORB Dominio de PSS
Servant
Interfa
ce OR
B e
xtern
a (ID
L)
ClienteORB
Objeto deldominio
PSS
GRÁFICO 1 - INTERACCIÓN CON LA ORB
En PSS, la información persistente se presenta como objetos almacenados (Storage Objects) en
almacenes (Storage Homes), que a su vez se encuentran ubicados en repositorios de datos (DataStores)
tales como una base de datos o un conjunto de archivos. Este modelo está representado en el Gráfico 2.
E
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
34
Data Store o Repositorio de Datos
ObjetoAlmacenado
Almacen o StorageHome
ObjetoAlmacenado
ObjetoAlmacenado
Almacen o StorageHome
ObjetoAlmacenado
GRÁFICO 2 - MODELO LÓGICO
Dentro de un repositorio de datos pueden existir múltiples almacenes y a su vez dentro de los
almacenes pueden existir múltiples objetos almacenados. Conceptualmente, un repositorio de datos es
un conjunto de almacenes, cada almacén contiene objetos de un tipo definido dentro del repositorio.
Dentro del repositorio, un almacén es un Singleton [Gamma01], es decir que existe a lo sumo una única
instancia de un almacén por tipo de objeto almacenado.
Los objetos almacenados se manipulan a través de instancias de una clase definida en el lenguaje
de implementación del programa o proceso que utilice el servicio. Estas instancias representan en un
momento dado un objeto existente en el repositorio de datos, mediante el cual se accede al estado
persistido. Las instancias de objetos que estén ligadas a un objeto en el repositorio se llaman
encarnaciones, al cambiar un atributo de una de estas instancias se está cambiando el estado del objeto
almacenado en el repositorio.
Los almacenes de objetos también son manipulados mediante instancias de los mismos. Estas
instancias son provistas por catálogos. Para acceder a las instancias de los objetos almacenados desde del
proceso o programa se requiere de una conexión con el repositorio de datos, denominada sesión. El
Gráfico 3 define la relación entre el modelo lógico y el modelo real de la aplicación.
Data Store o Repositorio de Datos
Almacenes oStorage Homes
ObjetosAlmacenados
ObjetosAlmacenados
Programa o Proceso
Catálogo
sesión
Encarnaciones
Instancia deAlmacen
Encarnaciones
Instancia deAlmacen
GRÁFICO 3 - SESIÓN PARA ACCEDER AL DATASTORE
Muchos de los conceptos existentes en PSS, fueron tomados del estándar SQL3 o SQL 1999
[EisenbergMelton01]. SQL3 fue una extensión propuesta al estándar SQL en el año 1999, que agrega
conceptos de los lenguajes de programación orientados a objetos. Las bases de datos que lo soportan son
llamadas Object-Relational DBMS u ORDBMS [Ramakanth01]. Las tablas en SQL3 permiten tipos de datos
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
35
más ricos llamados ADT (Abstract Data Type) que soportan herencia. Los almacenes de PSS son el
análogo a las tablas de SQL3, mientras que los objetos almacenados son los registros o tipos de datos de
SQL3.
Todos los objetos definidos por el servicio de persistencia implementan la interfase
LocalInterface [CORBA01]. Según la especificación de CORBA, todos los objetos que implementen dicha
interfase tienen su ciclo de vida limitado al proceso en el que fueron engendrados. De esta forma, es
imposible para un servant exportar un objeto definido en el dominio del servicio de persistencia. Esta es
otra forma de encapsular el servicio de persistencia sólo dentro de la implementación de los servants.
IDENTIFICACIÓN
Dado que existen múltiples instancias de objetos dentro de un repositorio y de un almacén, es
necesaria una forma de diferenciarlos.
Cada objeto almacenado posee un número único que lo identifica dentro del almacén al que
pertenece denominado short-pid (Short Process Identifier o Identificador corto de Proceso). Además
posee un identificador único dentro del repositorio en el que existe, que es denominado pid (Process
Identifier o Identificador de Proceso). El alcance de este pid está limitado a todas las instancias que
pueden ser accedidas por el mismo catálogo.
El concepto de pid es análogo al de oid dentro de CORBA. Una forma simple de relacionar un
objeto CORBA con un objeto del servicio de persistencia (un objeto persistente) es directamente por sus
identificadores (oid igual al pid).
TIPOS Y HERENCIAS.
Los tipos estén separados en dos grandes categorías abstractos y concretos, los primeros
implican definiciones que no pueden ser instanciadas, las segundas son las construcciones que serán
instanciadas en el programa que utilice el servicio.
custom Herencia Diamante
«interface»
A
«interface»
B
«interface»
C
«interface»
D
«interface»
A'
«interface»
B'
«interface»
C'
«interface»
D'
«interface»
E'
GRÁFICO 4 - HERENCIA DIAMANTE DE INTERFASES EN CORBA
Este modelo es muy similar al modelo de herencia del lenguaje de programación Java, los tipos
abstractos son análogos a las interfases de Java, mientras que los concretos son análogos a las clases. Al
igual que las interfases, los tipos abstractos admiten herencia múltiple, mientras que los concretos solo
pueden heredar a lo sumo de un único tipo concreto. De igual forma que las clases, los tipos concretos
pueden “implementar” múltiples interfases. La herencia múltiple incluye el modelo de herencia diamante.
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
36
La herencia diamante (gráfico 4) está definida en la especificación de CORBA [CORBA01], se
utiliza principalmente para la definición de interfases.
AlmacenTipoA TipoA
TipoBAlmacenTipoB
TipoCAlmacenTipoC
AlmacenConcreto TipoConreto
almacena
almacena
almacena
almacena
ImplementaImplementa
ImplementaImplementa
Hereda Hereda Hereda
Hereda
GRÁFICO 5 - TIPOS Y MODELO DE HERENCIA EN PSS
Tanto los objetos almacenados como los almacenes son tipados con tipos abstractos o concretos,
según si son abstractos o concretos respectivamente.
Cada objeto almacenado tiene un tipo asociado, este tipo define el estado y las operaciones que
poseen todas las instancias de este tipo. A su vez un tipo puede ser derivado de otros tipos definidos en el
repositorio.
Cada almacén a su vez está definido por un tipo, el cual define los tipos de objetos que el almacén puede contener o manejar junto con las operaciones que pueda realizar. De igual forma que los tipos de objetos, los tipos de almacenes también pueden derivar de otros tipos de almacenes definidos dentro del repositorio.
Dentro de un repositorio, un almacén puede manejar tanto las instancias de sus objetos
almacenados como las de sus subtipos, que se denomina familia de almacenes.
En el gráfico 5 se muestra la herencia de tipo diamante soportada por el servicio de persistencia
CORBA.
CLAVES
Un almacén puede definir que una serie de atributos de los objetos que maneja, definen una
clave que representa un identificador único para cualquier instancia de los objetos existentes en el
almacén. Los almacenes pueden definir tantas claves como deseen. Hay una clave implícita que es el
short-pid. Un concepto muy importante, es que las claves no están definidas por los objetos
almacenados, sino por el almacén donde se encuentran. Esto permite que diferentes almacenes definan
diferentes claves. Una implicación importante es que la unicidad de los objetos sólo es válida dentro del
almacén en el que existen.
USO DEL SERVICIO DE PERSISTENCIA
Para hacer uso del servicio de persistencia, se requiere que el usuario defina los tipos que usará en un programa, la especificación propone dos formas para definir los tipos de objetos.
- Mediante el lenguaje de definición llamado PSDL (Persistent State Definition Language); o
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
37
- mediante el uso del lenguaje programación en el que se quiera acceder al servicio.
PSDL es una ampliación al IDL estándar de CORBA, que agrega cuatro construcciones: abstract
storage object, abstract storage home, storage object y storage home. En el caso de utilizar la primera
forma de definir los tipos, la implementación del servicio deberá proveer una herramienta o compilador
que transforme el código definido en PSDL al código fuente que utilice el servicio.
En el caso de utilizar la segunda, también llamada persistencia transparente, el usuario será
responsable de programar las clases que usará el servicio de persistencia.
Ambas alternativas son igualmente válidas, la segunda no requiere que el usuario entienda el
lenguaje PSDL. Tiene algunas limitaciones ya que no hay un estándar para cada lenguaje, que defina cómo
hacer uso de algunas de las funcionalidades que provee el servicio, tales como definir claves adicionales a
las implícitas. Este problema surge porque el servicio requiere que el usuario sea quien defina y provea
los tipos y no alcanza con solo llamar a una rutina del servicio. A cualquier usuario acostumbrado a
trabajar con CORBA le es natural utilizar el lenguaje IDL y por consiguiente le debiera ser relativamente
sencillo aprender a utilizar PSDL.
Según la especificación del servicio, las aplicaciones que utilicen el servicio de persistencia solo
deberían tratar con los tipos abstractos, de esta forma se puede lograr una aplicación aislada o separada
de la implementación del servicio. Por otro lado, los tipos concretos son mapeados a construcciones
definidas en el lenguaje de implementación del servicio (Java o C++). Algunas de estas construcciones
están definidas en el mapeo de PSDL al lenguaje concreto, pero otras no, y en este punto es donde se
pierde la independencia de la implementación del servicio de persistencia.
ACCESO AL SERVICIO DE PERSISTENCIA
La puerta de entrada al servicio es el ConnectorRegistry (o registro de conectores). Este objeto es
un Singleton registrado en la ORB como una referencia inicial [CORBA01]. El nombre de la referencia
depende de la implementación del servicio, pero generalmente es PSS. Como su nombre lo indica, el
ConnectorRegistry tiene un registro de los conectores que tiene registrados el servicio de persistencia.
ConectorRegistry Connector CatalogBase
Session SessionPool
TransactionalSession
ORBRefencia inicial
Tieneregistrados Provee
GRÁFICO 6 - ACCESO AL SERVICIO
El conector (Connector) determina el tipo de servicio de persistencia que se usará. Se dice que el
conector define la implementación del servicio de persistencia a utilizar. Un conector típico podría ser uno
que se conecte a una base de datos relacional o uno que utilice un sistema de archivos para almacenar el
estado de los objetos. Los conectores son responsables de crear los catálogos (CatalogBase) que se
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
38
usarán para trabajar con el servicio de persistencia. El tipo de catálogo que se cree, será dependiente del
conector y el método que se utilice para crearlo.
La sesión, o sea la conexión lógica entre el o los repositorios y el proceso, tiene credenciales de
uso, como por ejemplo, solo lectura o lectura-escritura. Las sesiones pueden a su vez ser de tipo
transaccional (TransactionalSession) mediante el uso del servicio de transacciones CORBA, o sesiones
donde la atomicidad de las operaciones dependa del conector utilizado, por ejemplo, con acceso de tipo
archivo (Session), donde cada operación se hace inmediatamente persistente.
El acceso a las sesiones puede ser en forma explícita o implícita. Cuando se trabaja con sesiones
implícitas (ya sean transaccionales o no) el usuario del servicio de persistencia es el encargado de
crearlas, escribiendo código en el lenguaje utilizado. Cuando se maneja en forma implícita, un pool de
sesiones es el encargado de administrarlas (SessionPool). Un pool de sesiones tiene el objeto de permitir
la reutilización de las conexiones o sesiones a un repositorio, ya sea dentro de la misma aplicación o no.
Por lo general, su existencia tiene dos justificaciones: la creación de una sesión puede ser costosa en
términos de recursos involucrados para crearla, y cómo y dónde conectarse puede ser centralizado en un
sólo punto, permitiéndole a la aplicación desentenderse de ello.
El servicio de persistencia puede ser utilizado también mediante un contenedor de componentes
CORBA [CCM01, CCM02], tanto en forma explícita como implícita.
TRANSACCIONES
Las transacciones en el servicio de persistencia están presentes a través del servicio de
transacciones CORBA [TS01]. Esto implica que los objetos almacenados pueden ser accedidos en el
contexto de una transacción. Para interactuar con el servicio de transacciones es necesario registrar en él
los recursos que serán utilizados en la transacción.
En el caso de que se trate de sesiones explícitas, las instancias de los objetos almacenados están
relacionadas con una transacción por medio de una sesión transaccional. Esta sesión está asociada a una
transacción de datos (datastore transaction), que es el recurso que se registra con el servicio de
transacciones. En el modo de trabajo explícito, la sesión transaccional provee operaciones explícitas para
interactuar directamente con el servicio de transacciones.
El escenario anterior no es el más típico, por lo general, cuando se trabaja con transacciones se
utiliza un proveedor de las mismas, y es el pool de sesiones el que se comunica con este servicio. En el
caso de utilizar el pool (sesiones implícitas), éste es el encargado de verificar cuando es necesario iniciar
una transacción y no se tiene control programado del manejo de transacciones.
CONSTRUCCIONES EN PSDL
PSDL define el módulo CosPersistentState, que contiene todas las construcciones propias del
lenguaje. Las principales son:
CatalogBase, Connector, Session, SessionPool, TransactionalSession, AccessMode,
ParameterList, StorageHomeBase, StorageObjectBase, StorageObjectFactory, StorageHomeFactory,
SessionFactory, NotFound, TypeId, Pid, ShortPid, ConnectorRegistry.
A continuación, voy a hacer una breve descripción de las construcciones que agrega PSDL a IDL
no definidas hasta este punto.
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
39
- AccessMode define el modo de acceso que tendrá garantizado el usuario del servicio sobre una sesión al momento de crearla, mediante el uso de un conector. Puede ser solo lectura, o lectura – escritura.
- ParameterList es simplemente una lista de parámetros variables que le permiten al conector recibir parámetros propios de la implementación del mismo para crear una sesión. Por ejemplo, un conector podría requerir un nombre de usuario y clave para conectarse a un repositorio, si el repositorio es de tipo remoto, podría ser necesario indicarle la ubicación del mismo en la red.
- NotFound es una excepción utilizada para indicar que un objeto que se quiere localizar no existe o no está disponible. Es utilizada tanto por el registro de conectores para indicar que un conector no existe, como por una sesión cuando se quiere ubicar un objeto por su pid.
- TypeId es una cadena de caracteres que identifica un tipo PSDL. El formato de la misma es similar a los ids de los repositorios IDL [CORBA01 ver sección 10.7.1], pero en vez de comenzar con IDL lo hacen con PSDL. Al igual que los ids de repositorios, estos ids incluyen el número de versión al que corresponden. Estos ids son utilizados por los conectores para registrar las fábricas de objetos de un tipo PSDL. Por ejemplo, para un tipo persistido: ar.uba.fi.pmi.corba.pss.prueba1.Persona, en su primer versión podría tener asociado un tipo persistido: PSDL:ar.uba.fi.pmi.corba.pss.prueba1.Persona:1.0. Esta cadena de caracteres será utilizada por el usuario final para registrar la clase Persona en el conector donde quiera utilizarla.
CARACTERÍSTICAS DE LAS CONSTRUCCIONES BÁSICAS PSDL En este punto se definen las características principales de las definiciones PSDL. La sintaxis de las
mismas se podrá consultar en la especificación del servicio [PSS01].
ABSTRACTSTORAGEOBJECT
La sintaxis para definir un objeto de este tipo es prácticamente la misma que para una interfase
IDL, salvo que esta no puede contener constantes ni definiciones de subtipos. Un objeto almacenado
abstracto puede heredar de múltiples objetos almacenados abstractos, pero sólo puede ser definido
como supertipo directo una única vez en la misma definición. No se permite heredar de tipos que
contengan estados u operaciones con el mismo nombre, salvo para el caso de la herencia diamante
explicada anteriormente. Se permiten definiciones hacia adelante (forward), pero las mismas tienen que
estar completas dentro de la misma especificación PSDL (dentro del mismo documento).
Estas definiciones pueden contener definiciones de estados que representan a los atributos
persistentes de los objetos almacenados. Los estados pueden ser definidos como lectura – escritura o
sólo lectura (explícitamente). Los estados son tipados y los tipos válidos son: un tipo básico (número o
carácter), una cadena de caracteres, una referencia a un tipo persistido abstracto, u otra definición local
(interfase u objeto almacenado).
Cuando el estado tiene definido un tipo que sea otro tipo persistido, se dice que el objeto está
empotrado (embedded) en el otro. Este objeto que representa el estado, no posee identidad propia y no
puede ser referenciado fuera del contexto del objeto que lo contiene.
Cuando el tipo del estado es una referencia a otro objeto almacenado mediante el uso de la
palabra strong (fuerte), se puede indicar que cuando el objeto almacenado que lo referencia sea
destruido también lo sea el referenciado.
La definición puede contener operaciones locales (local operations), que podrán utilizar
parámetros de entrada, salida o entrada - salida tal como cualquier operación IDL. Además, podrán
utilizar como parámetros, cualquier definición IDL válida o algún objeto almacenado abstracto definido
previamente. Las operaciones pueden ser definidas además como constantes, en cuyo caso no se les
estará permitido modificar el estado del objeto al que pertenecen. Se les denomina operaciones locales
dado que los objetos almacenados definen interfases locales en CORBA.
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
40
Toda definición de objeto almacenado abstracto que no herede de otra definición
explícitamente, heredará implícitamente del objeto almacenado abstracto StorageObject definido en el
módulo CosPersistentState.
ABSTRACTSTORAGEHOME
La definición de un almacén de objetos almacenados debe especificar que tipo de objeto
almacenado abstracto es el que puede contener (un único tipo). Al igual que los objetos almacenados
abstractos, se admiten definiciones hacia delante, que deben ser completadas en el mismo documento.
Los almacenes abstractos pueden heredar de múltiples almacenes abstractos, con la única
condición de que los tipos de objeto persistidos manejados por los almacenes heredados, deben ser un
supertipo o el mismo que el manejado por el almacén. Tampoco se permite redefinir operaciones con el
mismo nombre, salvo para el caso de la herencia diamante, que también es soportada por los almacenes
abstractos.
Los almacenes abstractos pueden definir claves (keys), que son simplemente una lista de
nombres de estados (al menos uno), que están identificados por un nombre. Una clave determina que
para el almacén donde está definida, sólo puede existir un único objeto almacenado con los mismos
valores de los estados definidos en ella. Los estados que conforman la clave no deberán estar repetidos y
los tipos de los mismos deberán ser comparables (tipos nativos, cadenas de caracteres, o estructuras de
tipos comparables).
La definición de una clave implica la definición implícita de dos operaciones locales en el
almacén, que permitirán ubicar un objeto almacenado dentro del mismo. Estas operaciones serán
find_by_nombreclave, y find_ref_by_nombreclave, ambas tomarán como parámetro los valores de los
estados que conformen la clave, pero la primera devolverá el objeto almacenado, mientras que la
segunda, una referencia al objeto. Si dicho objeto no existe, se lanza una excepción NotFound en el
primer caso y se devolverá NULL en el segundo.
Además, se permite una definición extendida de operación, llamada factory method (operación
de fábrica) [Gamma01], que permite construir un objeto almacenado en el almacén. Los parámetros
serán una lista de estados presentes en el objeto almacenado (por lo menos uno).
Factory Method es un patrón de diseño que permite encapsular la construcción de un objeto. El
tipo o clase será definido por el objeto que implemente dicho factory method en función de su estado
interno y de los parámetros de dicho método. En el contexto de un almacén de objetos, según la
configuración del servicio de persistencia, se definirá la clase que implementa el objeto almacenado. De
esta forma, el usuario del servicio de persistencia puede tratar siempre con las interfases de los objetos,
sin necesidad de conocer las implementaciones de los mismos, que por lo general, se encuentran ligadas
a la implementación del servicio de persistencia.
Al igual que los objetos almacenados abstractos, los almacenes abstractos también admiten la
definición de operaciones locales.
STORAGEOBJECT
Se permite la herencia de a lo sumo un objeto almacenado por definición, mientras que es
posible implementar múltiples definiciones de objetos almacenados abstractos. Se dice que el objeto
almacenado implementa directamente una definición de un objeto almacenado abstracto cuando se
define en forma explícita que lo implementa. También puede implementarlo en forma indirecta, si
hereda de otro objeto almacenado que implemente la definición abstracta. Esta definición es importante,
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
41
ya que existen múltiples restricciones que se aplican cuando se implementa en forma directa otra
definición.
Por ejemplo, cuando un objeto almacenado implemente directamente un objeto almacenado
abstracto, que defina un estado que sea otro objeto almacenado abstracto o una referencia, la definición,
deberá declarar una directiva de almacenamiento (store directive) al compilador. Esta directiva indica
cual es el objeto almacenado concreto que será referenciado o utilizado en dicho estado, y
adicionalmente, si se trata de una referencia, cual será el almacén concreto donde se albergará a dicho
objeto almacenado. Esto le permite al compilador, por ejemplo, optimizar el espacio requerido para
almacenar el objeto conociendo de antemano de que tipo de objeto se trata.
Toda definición de objeto almacenado que sea la primera en la jerarquía, podrá indicarle al
compilador los estados que definen una referencia a un objeto almacenado de este tipo. Esta lista de
estados, además constituirá una clave única dentro del almacén de objetos almacenados que contenga
objetos de este tipo.
STORAGEHOME
La definición de un objeto almacenado requiere indicar que tipo de objeto almacenado concreto
será el que manejará este almacén. El almacén puede heredar de a lo sumo un almacén y el tipo
almacenado de dicho almacén deberá ser un supertipo del tipo almacenado definido por el almacén.
Además, dos almacenes dentro de la misma jerarquía no podrán tener definido el mismo tipo de objeto
almacenado.
El almacén puede implementar múltiples almacenes abstractos, siempre y cuando los tipos
almacenados de los almacenes abstractos sean implementados por el objeto almacenado definido por el
almacén.
Al igual que los objetos almacenados, para los almacenes también se define el concepto de
implementar en forma directa. Se dice que un almacén implementa en forma directa una definición del
almacén abstracto, cuando el almacén sea el primero dentro de la jerarquía que lo implemente.
También, se dice que un almacén implementa directamente un objeto almacenado abstracto
cuando éste sea el primero en su jerarquía en implementar dicha definición. Y en base a esto, el almacén
implementará en forma directa un estado, cuando implemente en forma directa el objeto almacenado
abstracto donde dicho estado esté definido.
Estas definiciones se aplican en restricciones a la jerarquía del almacén. Cada clave definida en
un almacén abstracto, que sea implementado directamente por un almacén, deberá tener como mínimo
un estado implementado directamente por el almacén. Esto evita que existan almacenes que definan
claves únicas sin contener estados definidos en él. Es como si en una base de datos relacional, una tabla
definiera una clave única que no tenga columnas de dicha tabla, sino todas columnas de una tabla con la
que se tiene una clave foránea (foreign key).
Todo almacén, que sea el primero dentro de la jerarquía, puede definir una de sus claves o la
representación de la referencia del tipo de objeto que almacena, como la clave primaria.
MAPEO DEL LENGUAJE PSDL A UN LENGUAJE CONCRETO
El servicio de persistencia define el mapeo de las entidades definidas en PSDL al lenguaje que
implemente el servicio. Algunos son de tipo nativo, o sea que su valor depende puramente del lenguaje
que se utilice.
DIFERENCIAS ENTRE C++ Y JAVA DEL SERVICIO DE PERSISTENCIA
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
42
El mapeo de PSDL a C++ requiere agregar estructuras que en Java ya se encuentran presentes.
Algunos de los mapeos de tipo nativo a Java se traducen en clases ya existentes en el lenguaje, mientras
que en C++, son construcciones nuevas. Por ejemplo, en Java, toda clase hereda implícitamente de la
clase Object, mientras que en C++ no existe una clase base a todas las clases, por lo que se requiere
agregar la construcción CosPersistentState::StorageObject. Otro ejemplo, es el de las fábricas (factories),
para instanciar o crear objetos de los tipos definidos por los usuarios (mapeo nativo): en Java se traduce
directamente en un objeto de tipo Class, mientras que en C++ se traduce a una clase Factory definida por
el servicio de persistencia, que deberá crear objetos en forma programática. Esta es una de las diferencias
de mapeo del servicio a los lenguajes de programación, donde se muestra que el servicio se adapta más
naturalmente al lenguaje Java. Dado que Java es un lenguaje más moderno que C++, provee de
herramientas que hacen más simple los programas escritos en este lenguaje.
Otro ítem importante, es el manejo automático de la memoria. En Java, el ciclo de vida es
manejado automáticamente por la máquina virtual, mientras que en C++, el programador es el encargado
de reservar y liberar la memoria requerida por los objetos. El servicio de persistencia provee métodos
para liberar referencias a objetos accedidos en una sesión y que ya no son utilizados. En C++ se requiere
de esta operación, ya que es el programador el encargado de liberar la memoria que ha alocado, en Java
ésta operatoria es automática.
CRÍTICAS A LA ESPECIFICACIÓN
El servicio de persistencia define un tipo persistido llamado StorageObject, que tiene una serie
de métodos, tales como get_pid() o get_short_pid(), entre otros. Según la especificación (sección 3,
página 8[PSS01]), toda definición de un objeto persistido abstracto que no herede explícitamente de otro,
hereda de la clase StorageObject. Sin embargo, de acuerdo a la especificación, todos los objetos
almacenados concretos heredan implícitamente de la clase StorageObjectBase. En Java, esta clase está
representada por la clase java.lang.Object, mientras que en C++, al no existir una clase base de todos los
objetos, se define una nueva clase CosPersistentState::StorageObjectBase. Ninguna de estas dos clases
(tanto en Java como en C++) definen los métodos que tiene definidos la clase abstracta StorageObject.
Dado que no es obligatorio que la definición de un objeto almacenado concreto, implemente
alguna definición de un objeto almacenado abstracto, lo expresado en el párrafo anterior sobre
StorageObject y StorageObjectBase, no constituye una contradicción.
Esto implica que no está definido que ocurre cuando un objeto almacenado concreto
implementa alguna definición abstracta. ¿Qué pasa con las operaciones definidas en StorageObject?
Queda completamente libre y depende puramente de la implementación del servicio de persistencia.
Tampoco está claro, por qué es necesaria la existencia de la definición de StorageObject, ya que todas las
operaciones del servicio de persistencia están basadas en la clase StorageObjectBase.
La existencia de StorageObject, hace que los usuarios del servicio de persistencia estén obligados
a atarse a una implementación del servicio, si es que quieren utilizar las definiciones abstractas. Esto
contradice uno de los principales objetivos de PSDL, que es el de liberar al usuario final del servicio de
usar una implementación particular del mismo.
Gran parte de las inconsistencias surgen de tratar de dar soporte de persistencia transparente y
por PSDL, en forma simultánea. Si bien la idea de la persistencia transparente es potencialmente muy
atractiva, al tener que coexistir con el mundo de PSDL, crea un modelo híbrido difícil de compatibilizar.
Las excepciones, códigos de error, o condiciones de error no están especificadas en todos los
casos, cada implementación es libre de generar los tipos de error que considere apropiados. Por ejemplo,
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
43
¿qué sucede cuando se trata de registrar una clase como factory de un tipo de objeto almacenado que no
es compatible con la implementación del servicio o conector?
Otro tipo de errores que también son muy importantes, son los relacionados con la integridad
referencial y el ciclo de vida de los objetos. ¿Qué pasa cuando se trata de eliminar un objeto que esta
referenciado en otro como un estado del mismo?. ¿Qué pasa si se quiere asignar un objeto empotrado
como un atributo de otro objeto almacenado? De acuerdo con la especificación, los estados no pueden
ser compartidos. Esto implica que cuando se asignan objetos empotrados a otros objetos, se debería
realizar una copia implícita de dicho objeto, el modo e implicancia de la copia no se encuentra definido,
por lo que cada implementación puede tener formas distintas de realizarlo.
Existe también una indefinición en cuando a los tipos de datos que soportan los estados. Un
estado puede ser de una interfase o estructura IDL definida dentro del archivo PSDL compilado. Como se
almacena dicha interfase o estructura no se encuentra definido en la especificación.
Otro punto problemático, son los mapeos de los estados al lenguaje Java. En Java existe una
convención que la definición de los métodos que permiten acceder a los atributos que definen el estado
de los objetos. Si un atributo se llama “nombre”, el método para acceder al nombre, se debe llamar
getNombre(), y el método para modificarlo debe llamarse setNombre(nombre). En el mapeo de PSLD a
Java, se define que un estado se lee por un método generado por el compilador, que para el caso de un
estado llamado nombre, será nombre(), y para modificarlo el método deberá llamarse nombre(nombre).
Esta diferencia entre Java estándar, y el mapeo de PSDL, puede traer problemas al integrar los objetos
generados por el compilador con otras herramientas que esperen métodos estándar.
La especificación es amplia en muchos sentidos, dejando puntos libres que pueden ser resueltos
de diferentes formas en distintas implementaciones del servicio. Este tipo de situaciones, hacen más
complicada la portabilidad entre distintas implementaciones del servicio, y van en contra del objetivo
inicial de lograr una interfase común con la que se pueda lograr independencia de la implementación del
servicio.
NOTAS FINALES SOBRE EL SERVICIO DE ESTADO PERSISTENTE
La especificación de PSS está definida solamente para dos lenguajes de programación: C++ y
Java, por lo que el campo de trabajo está limitado a estos dos lenguajes y no a múltiples lenguajes como
se promueve a CORBA.
Algunos conceptos que promueve PSS, tales como los identificadores globales de objetos y los
almacenes, no pertenecen a POO, esto se debe a que el modelo está basado en SQL3 y no en el modelo
de Objetos. Para una persona que no conoce SQL3, estos conceptos resultarán desconocidos e
incrementarán la complejidad de uso del servicio. PSS tampoco respeta algunas de las buenas prácticas
reconocidas en Java, tal como la forma de acceder a los atributos de los objetos. Estas características que
hace que el modelo de PSS se aleje del modelo de objetos incrementan la complejidad de uso del
servicio.
El servicio de estado persistente de CORBA es una solución parcial para obtener soporte de
persistencia de objetos, tiene sus limitaciones y no es necesariamente la opción más simple de utilizar,
pero sin embargo dentro del mundo CORBA, es una estándar lo cual le aporta un gran valor, y para el
objetivo de obtener estado persistente dentro de un servant es completo y funcional.
ALTERNATIVAS A PSS EN JAVA
Al utilizar el lenguaje de programación Java existen varias alternativas para obtener persistencia
de los objetos, algunas de ellas serán analizadas en la siguiente sección. Entre las alternativas se
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
44
encuentran Hibernate [KingG01], JDO [RoodsR01], entre otras. Estas dos herramientas forman parte de
un gran grupo herramientas similares que permiten persistir objetos en una base de datos Relacional,
llamadas genéricamente ORM (Object Relation Mapping o Mapeo objeto - relación). Este tipo de
herramientas no están directamente relacionadas con CORBA, por lo que su integración con CORBA
requiere de trabajo específico del programador sobre la aplicación. Por ejemplo, la integración con el
servicio de transacciones de CORBA deberá ser programada directamente.
Existe otra alternativa en Java denominada J2EE (Java2 Enterprise Edition o edición empresarial
de Java2). J2EE es una especificación de SUN que extiende a Java agregando una serie de servicios entre
los que se encuentran un modelo de componentes (en el que se base CCM [CCM01]) que permiten lograr
un estado persistente. Este modelo de componentes está integrado con CORBA y el servicio de
transacciones. Permiten funcionalidades mucho más avanzadas que PSS, como por ejemplo, acceso
remoto a los objetos almacenados mediante diferentes protocolos entre los que se encuentran GIOP. La
única desventaja de este modelo es que se requiere utilizar un contenedor de componentes, no es
suficiente con la máquina virtual misma. Este modelo debe ser comparado con el modelo de
componentes CORBA CCM.
También sería posible utilizar una base de datos orientada a objetos tal como DB4O [DBO401]
directamente sin PSS, pero nuevamente esta alternativa queda fuera del alcance de CORBA, por lo que la
integración debe ser implementada explícitamente por el programador. Esta y otras alternativa será
analizada en más detalle en la siguiente sección.
Servicio de Estado Persistente CORBA (PSS) Pablo M. Ilardi
45
REFERENCIAS
PSS01 – Persistence State Service V2.0 OMG 2002. http://www.omg.org/cgi-bin/doc?formal/02-
09-06
CORBA01 – Common Object Request Broker Architecture, Core Specification OMG.
http://www.omg.org/technology/documents/corba_spec_catalog.htm
POS01 – Persistent Object Service POS, OMG 2000.
http://www.omg.org/technology/documents/corbaservices_spec_catalog.htm
TS01 – Transaction Service Specification, OMG 2003.
http://www.omg.org/technology/documents/corbaservices_spec_catalog.htm
BgVaDk – Gerald Brose, Andreas Vogel, Keith Duddy – Java Programming with CORBA, Advanced
Techniques for Building Distributed Applications. 3rd Edition.- Wiley 2001.
KjPfTp – Jan Kleindienst, František Plášil, Petr Tuma – What we are missing in the CORBA
Persistent Object Service Specification.
EisenbergMelton01 – SQL:1999, formerly known as SQL3. Andrew Eisenberg, Sybase, Concord,
MA 01742 [email protected]; Jim Melton Sandy UT 84093 [email protected]
Ramakanth01 – Object-Relational Database Systems - The Road Ahead; Ramakanth S.
Devarakonda; http://www.acm.org/crossroads/xrds7-3/ordbms.html
Gamma01 – Design Patterns: Elements of Reusable Object-Oriented Software - Addison-Wesley
Professional Computing Series - by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides,
January 15, 1995
CCM01 – CORBA Component Model (CCM) Specification, OMG
http://www.omg.org/technology/documents/formal/components.htm
CCM02 – CORBA Component Model Tutorial, Yokohama OMG Meeting Wednesday, April 24th,
2002. http://www.omg.org/docs/ccm/02-04-01.ppt
KingG01 – Hibernate in Action, Christian Bauer, Gavin King; Manning, 2005. ISBN: 1932394-15-X.
http://www.hibernate.org
RoodsR01 – Java Data Objects, Robin M. Roods; Addison-Wesley, 2003. ISBN 0-321-12380-8.
http://java.sun.com/javaee/technologies/jdo
DBO401 – Database for Objects. http://www.db4o.com/about/productinformation
DEVQTC01 – Developing Quality Technical Information. A handbook for Writers and Editors.
Second Edition. G. Hargis, M. Carey, A. Hernandez, P. Hughes, D. Longo, S. Rouiller, E. Wide. ISBN
0-13-1477490.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
47
IMPLEMENTACIÓN DEL SERVICIO DE ESTADO PERSISTENTE CORBA - PSS
Consideraciónes iniciales .......................................................................................... 51
Partes ....................................................................................................................... 51
Persistencia Transparente..................................................................................... 51
El lenguaje de definición PSDL .............................................................................. 52
Compilador PSDL ..................................................................................................... 52
Bases para el Compilador...................................................................................... 52
Partes del Compilador .......................................................................................... 53
Entender el código de Entrada .............................................................................. 53
Generación del Parser por JAVACC ................................................................... 54
Estructura del árbol generado ........................................................................... 56
Errores a nivel del Parser .................................................................................. 57
Generar el código de Salida .................................................................................. 57
El patrón de diseño Visitor ................................................................................ 57
Identificadores...................................................................................................... 58
Proceso de Compilación........................................................................................ 59
Cómo funciona el compilador ............................................................................... 60
Clases fundamentales que definen al compilador ............................................. 60
PSDLCompiler ............................................................................................... 60
PSDLParser .................................................................................................... 60
NodeProcessor .............................................................................................. 60
CompileProcess............................................................................................. 61
FullTypeDefinition ......................................................................................... 61
BaseStorageElementBuilder .......................................................................... 61
TargetCompiler ............................................................................................. 61
IdentifierProcessor ........................................................................................ 61
AbstractIdentifierProcessor .......................................................................... 62
ModuleIdentifierProcessor ........................................................................... 62
HolderGenerator y HelperGenerator............................................................. 62
CompilerDefinitionWriter ............................................................................. 62
Primera Fase – Procesamiento .......................................................................... 62
Primer ejemplo de generación de una definición: ......................................... 64
Pasos requeridos ....................................................................................... 65
Segunda Fase – Validación ................................................................................ 67
Fallos en el proceso de validación ................................................................. 67
Validación del primer ejemplo de generación de una definición: .................. 68
Tercer Fase – Generación. ................................................................................. 69
¿Qué generar?, generación de código fuente ................................................ 69
“Propio del Servicio”, configuración del Compilador ..................................... 71
Lo que es común a todo servicio ................................................................... 72
Generación de código para el primer ejemplo ............................................... 73
Pasos ......................................................................................................... 73
Conclusiones del Compilador ................................................................................ 74
Conexión del servicio de persistencia con la ORB .................................................... 74
Configuración de la ORB para el acceso a sus referencias iniciales. ....................... 75
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
48
Registro de Conectores para el servicio de persistencia ........................................ 76
Implementación del servicio provisto por diferentes conectores ............................ 77
El conector del Servicio de Persistencia ................................................................ 77
Operaciones del conector ................................................................................. 79
Problemas que presenta la creación de un conector ............................................. 81
Creación de objetos .......................................................................................... 81
El patrón de diseño Template Method .......................................................... 81
Almacenes y Objetos almacenados ............................................................... 82
Almacenes ................................................................................................ 83
Modelo de Delegación .......................................................................... 84
Objetos Almacenados ............................................................................... 84
StorageObjectSpec ................................................................................ 86
StorageObjectIdentifier ......................................................................... 86
Operaciones de búsqueda de Objetos ............................................................... 87
Validación de Claves ......................................................................................... 88
Destrucción de objetos almacenados ................................................................ 89
Diferentes Conectores .......................................................................................... 90
Conector de persistencia temporal ................................................................... 90
Funcionamiento ............................................................................................ 91
Manejo de identificadores ........................................................................ 91
Almacenes temporales .............................................................................. 92
Operaciones de búsqueda ......................................................................... 92
Conector de persistencia durable...................................................................... 92
Requerimientos para un conector de persistencia durable ........................... 92
Distintas alternativas para obtener persistencia durable ............................... 93
Persistencia mediante archivos ................................................................. 93
Persistencia mediante una base de datos relacional .................................. 93
Persistencia mediante una base de datos orientada a objetos .................. 95
DB4O .................................................................................................... 95
Conector persistente con soporte de DB4O .................................................. 99
Registro del Conector ................................................................................ 99
Funcionamiento ........................................................................................ 99
Relación con DB4O ...............................................................................100
Objetos con estado compartido ...........................................................100
Identificadores .....................................................................................101
Operaciones atómicas ..........................................................................101
Almacenes ............................................................................................103
Operaciones de consulta ......................................................................104
Creación de objetos almacenados ........................................................104
Acceso remoto al repositorio ...............................................................105
Transacciones .......................................................................................106
Construcciones Resultantes del Trabajo .................................................................107
Un ejemplo concreto ..............................................................................................108
Código PSDL.....................................................................................................108
Resultado de la compilación del código PSDL ...................................................108
Persona ........................................................................................................109
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
49
PersonaHome ..............................................................................................109
PersonaBase ................................................................................................110
PersonHomeBase .........................................................................................112
PersonaBaseImpl .........................................................................................112
Uso del modelo en una aplicación CORBA .......................................................113
Referencias .............................................................................................................117
Referencias a Bibliotecas ........................................................................................119
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
51
CONSIDERACIÓNES INICIALES
Mi idea original para este trabajo fue utilizar C++, pero después de un tiempo de trabajar en un
prototipo del servicio, desistí y comencé a utilizar Java. Las razones fueron sencillas ya que la
especificación se adapta mucho más fácil a Java que a C++ y el modelo de objetos es análogo al de Java.
Java provee de muchas más herramientas de uso libre que C++. Por ejemplo, por el sólo hecho de utilizar
Java, se cuenta con una ORB lista para ser utilizada que es parte de la especificación del lenguaje.
El servicio de estado persistente de CORBA tiene como objetivo principal proveer estado
persistente a los servants. Mi implementación cumple con ese cometido, pero también plantea la forma
de compartir ese estado desde distintas ubicaciones y servants.
PARTES
El trabajo se puede dividir en dos partes principales. La primera se fundamenta en que el servicio
de persistencia requiere que el usuario sea quién defina los tipos o clases que estarán disponibles. Y la
segunda es el servicio de persistencia propiamente dicho. Para la primer parte, el estándar define las dos
alternativas: Persistencia Transparente y uso del lenguaje PSDL.
PERSISTENCIA TRANSPARENTE La persistencia transparente es atractiva ya que parece más simple, sólo es necesario definir los
objetos tal cual se haría en cualquier aplicación. Sin embargo no es completa, tiene algunas limitaciones
tales como la definición de las claves que definen la unicidad de los objetos, la definición de operaciones
que pueden realizar los almacenes de objetos (storage homes), etc. El otro inconveniente que trae
acarreado esta alternativa es el cómo se integra en tiempo de ejecución.
Según la especificación [PSS01] existen cuatro alternativas para integrar la persistencia
transparente:
1- Un pre procesador que agregue las construcciones necesarias del servicio de persistencia al código fuente Java. Por ejemplo, tome un atributo de la clase y modifique el código donde se utilice dicho estado.
2- Un compilador Java especial que haga algo similar al pre procesador antes mencionado. 3- Un pos procesador que realice modificaciones similares al código binario resultante de la
compilación (llamado bytecode en Java), pero en forma binaria. 4- O una máquina virtual Java especial que intercepte los accesos a los atributos de las clases.
El problema de las primeras dos opciones es que requieren que se tenga a mano el código fuente
de las clases, lo cual no es siempre posible. Por consiguiente, las únicas dos opciones posibles son la 3 y 4.
A partir de la versión 5 de Java se han incorporado una serie de alternativas que le permiten a la
máquina virtual exportar puntos de control, que podrían ser utilizados por un posible servicio de
persistencia que implemente persistencia transparente.
Uno de ellos se denomina JVMTI o JVM Tool interface [JVMTI01] (interfase de herramienta para
la máquina virtual), que consiste en que la máquina virtual permite configurar en el momento del inicio
de la misma, una biblioteca nativa llamada agente, que implementará dicha interfase. A grandes rasgos
dicha interfase exporta puntos de control de ejecución del código que se ejecuta dentro de la máquina
virtual. De esta forma, se podría seguir utilizando la misma máquina virtual para lograr lo mismo que lo
descrito en el punto 4. La desventaja es que se trata de una biblioteca implementada en código nativo,
que debe entre otros, soportar las convenciones de las llamadas a funciones definidas en C y C++. Esto
agrega complejidad a la solución ya que no solo se trata de una solución escrita en Java.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
52
Otra alternativa también agregada a partir de a versión 5 de Java se denomina JVM
Instrumentation (Instrumentación) [JVMI01]. Esta alternativa le permite a la maquina virtual utilizar una
biblioteca externa que modificará el código binario de las clases cargadas antes de ser utilizadas. Este
método, entre otros, sirve para interceptar las llamadas a todos los métodos de las clases que se
requiera. La ventaja es que se trata de una herramienta que permite utilizar el mismo lenguaje Java para
modificar el código a ejecutar en tiempo de ejecución. Pero al igual que JVMTI, tiene que ser indicada al
momento de iniciar la máquina virtual y la modificación al código binario o bytecode deberá hacerse cada
vez que se inicie la máquina virtual. La desventaja que comparten estas dos alternativas mencionadas
anteriormente, es que requieren utilizar una máquina virtual que sea compatible con la versión 5 de Java
o superior.
El principal inconveniente de cualquiera de las soluciones planteadas, es que requieren tener
control de la ejecución de la máquina virtual. Las bibliotecas tienen que ser pasadas como parámetro en
la línea de comando que inicie la máquina virtual. No siempre se tiene control de ejecución de la máquina
virtual. Por ejemplo, en el ámbito de ejecución de las aplicaciones web, por lo general, se utilizan los
llamados contenedores web, como pueden ser el Tomcat o Jetty. En dichos entornos coexisten múltiples
aplicaciones para una misma máquina virtual. Y dependiendo de donde se ejecute dicha máquina virtual
es posible que por diversas razones no se tenga el control de la misma o que ni siquiera se conozca el
servidor donde se ejecuta la máquina virtual.
EL LENGUAJE DE DEFINICIÓN PSDL Esta segunda alternativa para la definición de los tipos a utilizar por el servicio de persistencia
requiere de la construcción de un compilador de PSDL que genere las clases en el lenguaje de
construcción del servicio. Es probable que requiera de mucho más trabajo que cualquiera de las
soluciones planteadas en la persistencia transparente, pero es más útil a los efectos del usuario final del
servicio, dado que provee muchas más funcionalidades. Otro punto a favor que tiene, es que podría ser
utilizada en cualquier entorno, sin importar si se tiene el control de la ejecución de la máquina virtual o
no. Además, vale notar que ninguna de las dos alternativas invalida a la otra, es decir, que se puede tener
un servicio de persistencia que provea las dos alternativas o incluso la segunda podría incluir a la primera.
Haciendo un balance de todas las alternativas posibles, la construcción del compilador PSDL es la
que aporta mayor valor agregado, dado que brinda más funcionalidad y tiene menos limitaciones.
Por esta razón, la primer parte del trabajo consiste en la construcción del compilador, y la
segunda en la implementación del servicio de persistencia.
COMPILADOR PSDL
El compilador del servicio de persistencia tiene que transformar el código fuente PSDL en código
fuente Java que cumpla con la especificación del servicio y además debe hacer que las construcciones
concretas construidas por éste, cumplan con alguna implementación del servicio.
Según el objetivo de mi trabajo, el compilador debe proveer la funcionalidad necesaria para
interactuar con el servicio de persistencia. Si bien, un compilador PSDL podría ser utilizado para compilar
archivos IDL, mi compilador se limitará a compilar las construcciones específicas del PSDL junto con
algunas otras del IDL que podrían ser requeridas por PSDL. Si se quiere utilizar IDL siempre está
disponible el compilador IDL de la ORB que se utilice. De todas formas el compilador necesita entender
las construcciones IDL para cumplir con el estándar, aunque no genere código Java para las mismas.
BASES PARA EL COMPILADOR
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
53
El lenguaje PSDL es una extensión al lenguaje estándar de CORBA, IDL. La especificación del
servicio define completamente el mapeo de las construcciones abstractas como almacenes y objetos,
mientras que las concretas son más abiertas y dependen de la implementación del servicio.
La especificación del servicio contiene la gramática del lenguaje. Dicha definición utiliza la
notación EBNF (Extended Backus-Naur form), la cuál es una extensión a BNF (Backus-Naur form).
BNF es la notación formal estándar, definida por John Backus, utilizada para definir la gramática
de los lenguajes [Garshol00]. Algol 60 fue el primer lenguaje popular en utilizarla. EBNF agrega pocos
cambios, pero que simplifican mucho su uso, entre los que se destacan los operadores: ‘?’, ‘*’ y ‘+’ que
reducen la necesidad de utilizar definiciones recursivas.
Debido a que BNF realiza una definición formal del lenguaje (con fundamentos matemáticos y sin
ambigüedades), es posible a partir de ella construir de forma mecánica un parser del lenguaje sin hacer
prácticamente cambios a la definición.
Un parser es una herramienta que toma una entrada de texto realizando un análisis gramatical
del mismo y validando su sintaxis.
PARTES DEL COMPILADOR El proceso de compilación que realiza el “compilador” se puede dividir en dos pasos que tienen
objetivos muy distintos [CooperRice00]. El primer paso es el de entender el código de entrada, y el
segundo es el de traducirlo al código de salida.
Entender Código deEntrada
Generar Código deSalida
RepresentaciónInterna
CódigoFuente
Código SalidaErrores
COMPILADOR 1 - PROCESO DE COMPILACIÓN
Estos dos pasos se comunican mediante una “Representación Interna” del código fuente que
ambos conocen, el primero la genera y el segundo la utiliza. Ambos pasos pueden generar errores que
impidan o no, la generación del código de salida. Estos errores son, por lo general, de distinta naturaleza,
el primero detecta errores de sintaxis mientras que el segundo detecta errores semánticos que no son
visibles sintácticamente.
Dependiendo de la complejidad del compilador, se pueden agregar más pasos intermedios al proceso. Por ejemplo, un paso que realice una optimización de la representación interna que permita generar un código de salida más óptimo que la solución trivial.
ENTENDER EL CÓDIGO DE ENTRADA Para entender el código de entrada es necesario leer el código e interpretarlo para así poder
generar la “Representación Interna”. Este es un proceso común a cualquier compilador y existen formas
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
54
estándar de realizar este paso [CooperRice00]. Se realiza con dos herramientas, la primera es un
analizador lexicográfico y la segunda es un parser.
Un analizador lexicográfico, descompone una secuencia de caracteres en sub-secuencias de los
mismos llamadas tokens (símbolo en inglés) y a su vez clasifica estos tokens según la categoría a la que
correspondan. A esta herramienta también se lo denomina scanner.
El parser toma estas secuencias de tokens, y las analiza para determinar la estructura del código
fuente. El resultado del parser es un árbol que refleja esta estructura.
CódigoFuente
Errores
Analizador Léxico Parser
Entender Código de Entrada
RepresentaciónInterna
COMPILADOR 2 - PRIMER PASO
Tanto el parser como el analizador lexicográfico son responsables de generar los errores cuando el código fuente de entrada no respete las reglas sintácticas del lenguaje.
Existen dos técnicas ampliamente utilizadas en la construcción de parsers, TOP-DOWN y BOTTOM-UP. La mayoría de los parsers utilizan alguna de estas dos técnicas.
- TOP-DOWN: (de arriba hacia abajo en inglés) estos parsers tienen la estrategia de separar la secuencia de entrada en grupos de caracteres grandes, para luego ir separándolos sucesivamente en los elementos fundamentales o tokens. Es básicamente un juego de prueba y error donde se plantea la hipótesis de que una secuencia de caracteres representan una determinada construcción del lenguaje.
- BOTTOM-UP: (de abajo hacia arriba) estos parsers tratan de ubicar los elementos fundamentales o tokens, y en base a ellos tratan de enmarcarlos en las construcciones más grandes definidas en el lenguaje. La complejidad de los parsers radica en como tomar la decisión de que construcción es la que
corresponde a una secuencia de caracteres determinados.
Por lo general, la construcción tanto del parser como del analizador lexicográfico no es una tarea
simple y puede llevar mucho tiempo. Ambas estrategias de procesamiento, se pueden escribir en forma
de algoritmo y en base a él construir un programa que realice el proceso sobre un archivo.
Existen herramientas que tienen el objeto de generar el analizador lexicográfico y el parser en
base a una definición del lenguaje de forma más o menos automática. Esta definición se realiza en
notación EBNF, con algunos agregados propios de la herramienta.
GENERACIÓN DEL PARSER POR JAVACC
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
55
Para la construcción del parser decidí utilizar una herramienta denominada javacc (Java Compiler
Compiler) [Javacc00]. Javacc es una herramienta de uso libre bajo licencia BSD (Berkely Software
Distribution), publicada en la comunidad abierta: java.net, que es patrocinada por la empresa SUN,
creadora del lenguaje Java. Al tener licencia BSD se puede construir y distribuir cualquier tipo de software
construido mediante esta herramienta (tanto comercial como libre) sin tener que pagar por ello.
Javacc es una herramienta que permite generar un parser de tipo TOP-DOWN a partir de una
definición gramatical. Javacc toma como entrada un archivo fuente (generalmente con extensión jj) y lo
transforma en uno o más archivos Java que representan un programa que es capaz de procesar y
entender archivos fuente escritos mediante el lenguaje que defina la gramática. Este programa no tiene
utilidad práctica, tan solo se limita a reportar si existe un error en el archivo fuente procesado.
Javacc utiliza la notación EBNF junto con agregados que le indican a su pre procesador algunos
parámetros que son utilizados para generar las dos rutinas a construir: el parser y el analizador
lexicográfico [JavaccTu00].
Si bien teóricamente con la gramática del lenguaje es posible definir un parser de forma
sistemática [Garshol00], en la práctica es posible que el tiempo en determinar la validez o no de un
archivo o secuencia de caracteres relativamente grande, sea prohibitiva para un parser generado en
forma genérica. Un parser genérico utiliza habitualmente la técnica de backtracking [Dasgupta00] para
analizar la validez de una secuencia de caracteres. Backtracking se aplica cuando al analizar una
secuencia de caracteres se llega a un punto donde se debe tomar una decisión sobre dos o más caminos a
tomar. Se toma la decisión de seguir un camino, luego de un tiempo de seguirlo se llega a un punto donde
se determina que la secuencia de caracteres es inválida, pero entonces es necesario considerar que la
decisión tomada originalmente podría haber sido incorrecta. Se debe volver a dicho punto para
considerar la otra alternativa. Básicamente, cada punto de bifurcación debe ser registrado para poder
volver a él en caso de que se llegue a un punto que determine un error. Esta técnica requiere de mucho
tiempo de procesamiento y memoria, y en muchos casos eso lleva a un programa o sistema que consume
recursos inadmisibles.
Por esto los parsers generados por javacc no utilizan la técnica de backtracking, lo que hacen es
tratar de recaudar la mayor cantidad de información para tomar la decisión correcta, de forma tal de no
tener que volver atrás. Javacc lee los N tokens siguientes en la secuencia para determinar si es válido o no
el árbol. La cantidad de niveles del árbol a procesar por cada nodo, es determinada por el parámetro
LOOKAHEAD del pre procesador. Un valor muy bajo puede determinar que el parser de cómo inválido
algo que no lo es, en cambio, uno muy alto puede hacer que el parser consuma mucha memoria y tiempo
en analizar y procesar el archivo. Por lo general, se define un LOOKAHEAD global y para ciertas
definiciones se pueden aumentar este valor, esto es necesario sobre todo en las definiciones recursivas.
Javacc toma como entrada un archivo de texto que contiene las directivas al pre procesador, la
notación EBNF que define el lenguaje y código Java que será ejecutado por cada construcción que el
parser detecte en tiempo de ejecución. Al ejecutar javacc sobre el archivo, se generan algunas clases Java
que serán utilizadas internamente por el parser y una clase Parser, que es la base para la construcción del
compilador.
La clase Parser generada solamente valida la sintaxis del archivo. Una forma práctica de utilizar el
Parser es utilizar javacc combinado con otra herramienta denominada jjtree. JJtree es un pre procesador
para javacc, que toma como entrada un archivo fuente javacc, y lo adapta de forma tal que el parser
generado por javacc genere un árbol con la estructura del archivo fuente procesado. Este pre procesador
inserta acciones que construirán el árbol a medida que el documento de entrada con código fuente es
procesado.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
56
PSDL.jjtjjtree javacc
PSDL.jj
Node*,javaPSDLParserVisitor.java
PSDLParser,javaUsa
PrimerPaso
SegundoPaso
COMPILADOR 3 - JAVACC
El árbol generado por jjtree estará compuesto por nodos. El tipo de nodo variará en función de la
estructura que el parser haya detectado en el archivo fuente. Se le puede indicar a la herramienta que
genere una clase para cada tipo de nodo que pueda existir en el árbol, este es el caso de mi compilador.
Por lo general, el tipo de nodo se corresponde uno a uno con las definiciones de la gramática del
lenguaje. Por ejemplo:
<psdl_state_type_spec> ::= <base_type_spec>
| <string_type>
| <wide_string_type>
| <abstract_storagetype_ref_type>
| <scoped_name>
Es la definición en notación BNF del tipo de atributo de un estado de un objeto almacenado. En
la definición del árbol, esto se traduce como un nodo de tipo: psdl_state_type_spec, que puede contener
como nodo hijo, algún nodo de los siguientes tipos: base_type_spec, string_type, wide_string_type,
abstract_storagetype_ref_type, o scoped_name. Entonces, jjtree generará una clase para cada uno de
estos nodos.
A veces no es útil tener en el árbol algunos nodos que no serán utilizados por el compilador, en
esos casos se le puede definir al pre-procesador de jjtree que los ignore, por lo que éste no generará una
clase para dicho nodo y obviamente no existirá en tiempo de ejecución un nodo que tenga como hijo a un
nodo de este tipo.
Volviendo al esquema inicial del compilador en dos fases, se puede pensar que el árbol resultado
del procesamiento del parser, puede ser la “Representación Interna” con la que se comunicarán las dos
partes del compilador.
ESTRUCTURA DEL ÁRBOL GENERADO
Como ya mencioné anteriormente la estructura del árbol depende fundamentalmente de:
- La definición que se le haya otorgado a jjtree, y javacc para que generen el parser. - El archivo fuente procesado
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
57
Según mi definición del lenguaje PSDL en notación EBNF, todo árbol generado por el parser tendrá
como raíz un nodo de tipo: Nodepsdl_specification. Este nodo contendrá nodos hijos con las
construcciones encontradas en el archivo procesado. En el caso de PSDL, las construcciones serán las
básicas definidas por el lenguaje más las existentes en IDL.
Todos los nodos generados por jjtree heredan de una clase base generada llamada SimpleNode.
Esta clase tiene como atributos el nodo padre y los nodos hijos junto con operaciones que permitirán
construir el árbol a medida que se procese el archivo de entrada. Además, se le pueden agregar atributos
que podrán ser utilizados cuando se procese el árbol para reportar errores, por ejemplo, los tokens que
definieron la creación del nodo. Los tokens aportan información importante como el número de línea y
columna donde se encuentran en el archivo de entrada.
ERRORES A NIVEL DEL PARSER
La clase Parser se encarga de detectar los errores lexicográficos y sintácticos. Los errores
lexicográficos se traducen en excepciones de tipo TokenMgrError, y los sintácticos en excepciones de tipo
ParseException. Estas dos clases de excepciones pueden reportar precisamente la ubicación del error
detectado en el archivo de entrada, además generan un mensaje de error que describe la causa del
mismo, por ejemplo, “se esperaba un token {...} y no X”.
Los errores lexicográficos ocurren cuando el parser encuentra un carácter no esperado en una
secuencia. Por ejemplo, dada la siguiente gramática para definir un número:
DEFINICION := NUMERO (+ NUMERO)*
, la siguiente secuencia de entrada:
45 – 12
, dará como resultado un error lexicográficos diciendo que se esperaba un carácter “+” después del 12 .
Los errores de tipo sintáctico ocurren cuando los caracteres de entrada son válidos, pero sin
embargo se encuentran ubicados de forma tal que no corresponden a una construcción válida. Por
ejemplo, dada la misma definición anterior de un número, ante una secuencia de entrada de tipo:
45 + + 12
, el parser generará un error sintáctico diciendo que se esperaba un token de tipo NUMERO después del
símbolo “+”.
El proceso de parsing puede detectar sólo esta clase de errores descriptos. Existen otros errores
que no son derivados de la definición gramatical del lenguaje y que no serán detectados por el parser. Por
ejemplo, que un identificador único en el documento se encuentre duplicado. Esta clase de errores
tendrán que ser detectados en la segunda fase del proceso de compilación.
GENERAR EL CÓDIGO DE SALIDA Generar el código de salida en lenguaje Java es el último paso del compilador. La salida será el
resultado de procesar el árbol generado por el parser. La herramienta que provee jjtree para simplificar
este proceso es una opción que se basa en el patrón de diseño Visitor [Gamma01]. Cuanto la opción es
activada el procesador generará una interfase Visitor (en inglés visitante), que permitirá recorrer el árbol.
EL PATRÓN DE DISEÑO VISITOR
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
58
El patrón de diseño Visitor permite que un árbol de nodos sea recorrido o visitado por diferentes
objetos sin que los nodos tengan que estar al tanto de ello. Un ejemplo, sería recorrer el árbol para
generar una representación del mismo en texto o para validar la estructura y la relación entre los nodos,
o generar el código de salida en base al mismo.
Este patrón es utilizado para recorrer estructuras, en este caso un árbol y su beneficio radica en
que la estructura o los elementos que la conforman no tienen que estar al tanto de que se hace con ese
procesamiento. Por otro lado, quien visita la estructura tampoco requiere conocer como está compuesta.
Podrían existir diferentes implementaciones del visitante, sin que los nodos sean afectados por ellos. El
compilador representa una implementación del visitante, y a la par podrían existir otras
implementaciones, por ejemplo, un optimizador del árbol o para buscar errores en el mismo.
class Visitor
compiler::PSDLCompiler
«interface»
parser::PSDLParserVisitor
parser::PSDLParser
«interface»
parser::Node
parser::SimpleNode
parser::Nodepsdl_module
parser::Nodepsdl_directiv e
«realize»
-parser -parser
-parent-chi ldren
«real ize»
COMPILADOR 4 - DIAGRAMA GENERAL DEL COMPILADOR
Básicamente, existe un objeto Visitor, que visitará los nodos del árbol. Todos los nodos heredan
de una clase común o implementan la misma interfase, que define un método aceptar(Visitor), el cual
será implementado por cada tipo de nodo invocando al método visitar en el Visitor pasándose a sí mismo
como parámetro: visitor.visitar(this). El Visitor tendrá que implementar un método visitar distinto por
cada tipo de nodo que quiera manejar de forma distinta o particular.
En el contexto del compilador, el Visitor está representado por la interfase PSDLParserVisitor, la
clase PDLCompiler es una implementación del mismo. La interfase Node define todo aquello que puede
ser visitado. Para cada nodo que sea visitado, existirá una clase concreta que implementará el método
aceptar, que solamente deberá llamar al método visitar del Visitor que reciba como parámetro.
IDENTIFICADORES La gramática de PSDL contiene múltiples definiciones que referencian identificadores. Este
concepto es compartido o heredado de la definición del IDL y por consiguiente del PSDL. Muchos tipos de
construcciones del IDL y PSDL utilizan el concepto de identificador, pero la definición del mismo varía en
función del lugar donde se lo utilice. Por ejemplo, un identificador puede identificar a un tipo de objeto
almacenado o a un método en un almacén. En otros casos, como por ejemplo, cuando se hace referencia
a una entidad definida anteriormente en el código fuente (como en la declaración de un estado), es
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
59
posible utilizar múltiples identificadores para identificarla, dado que la referencia puede ser dada tanto
en forma absoluta como relativa.
Existen múltiples reglas que determinan cuando un identificador es válido y cuando no. Las
mismas están asociadas a la definición que utilice el identificador, y no son explícitas en la definición
gramatical del lenguaje.
El hecho de que el concepto de identificador esté presente, es un buen punto para definir un
diseño que permita un grado alto de reutilización de funcionalidades, ya que será utilizado prácticamente
en todas las construcciones que procese y/o genere el compilador.
PROCESO DE COMPILACIÓN La recorrida del árbol generado plantea algunos obstáculos que tuvieron una gran influencia en
mi diseño del compilador:
1 Era necesario detectar aquellos errores que no fueran detectados por el parser 2 El árbol puede requerir incluir archivos externos, que se definen como “#include” en el código
fuente de entrada. Este proceso puede ser recursivo y además puede llevar a ciclos, que deben ser contemplados.
3 El código a generar para las construcciones concretas depende fuertemente de la implementación del servicio de persistencia, mientras que el código a generar para las construcciones abstractas es independiente de la implementación (según la especificación del servicio).
4 Las referencias a otros elementos puede hacerse tanto en forma absoluta como relativa mediante en uso de los identificadores. Por ejemplo, en Java se puede referenciar a la clase String por java.lang.String o simplemente por String. El compilador de Java determina las referencias relativas en base a los imports de la clase. En PSDL, las referencias relativas se resuelven en base al módulo en el que se esté referenciando a esta otra entidad.
5 Las definiciones pueden hacerse en forma directa, o en dos pasos mediante las declaraciones forward o hacia adelante. Las definiciones forward permiten que la definición sea referenciada, sin estar completa su definición, por ejemplo, para objetos almacenados que tengan declarados estados de su mismo tipo.
Los errores que no detecta el parser son diversos y a veces no son simples de detectar. Algunos
de ellos son:
- Contemplar la existencia de identificadores repetidos en el mismo archivo y / o en los archivos incluidos.
- Validar que las referencias a los identificadores estén disponibles - Validar que la jerarquía tanto de los objetos almacenados como de los almacenes sea
consistente, tanto para las construcciones concretas como para las abstractas. - Validar algunos requerimientos de la definición de los objetos en PSDL, que no están expuestos
en la definición gramatical.
Para procesar los archivos incluidos por el archivo fuente original, es necesario procesar cada
uno de ellos de forma tal que se recaude la información necesaria para las validaciones antes descriptas.
Este proceso debe ejecutar el parser sobre estos archivos y no debe generar el código de salida, además
el proceso ejecutado por el parser debe lidiar con referencias circulares de archivos incluidos.
La existencia de definiciones forward, requiere al menos de dos pasos para poder recolectar toda
la información necesaria para determinar cuando una definición es válida y cuando no.
Por último, pero no menos importante, el código de salida depende fuertemente del servicio de
persistencia, se requiere que el compilador o una parte de él conozca el servicio de persistencia para el
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
60
que generará código. Por ende, se requiere tener un servicio de persistencia totalmente definido para
poder construir el compilador. Como este no era mi caso, yo fui construyendo el compilador y la
implementación del servicio en paralelo, por consiguiente tuve que desacoplar las partes del compilador
que requerían generar código particular de la implementación del servicio.
CÓMO FUNCIONA EL COMPILADOR El gráfico siguiente muestra una división arbitraria de los pasos en los que se divide el proceso de
compilación. El paso 0 es procesar el archivo de entrada, dado que esta es una tarea que es realizada
mayormente por javacc, descripto anteriormente.
De esta forma, el proceso de compilación será descripto en tres fases, la primera fase
“Procesamiento”, la segunda “Validación” y por último “Generación”. Cada una de estas fases requiere
de múltiples clases. A continuación haré una breve descripción de algunas de las más importantes.
composite structure CompilePhases
ArchivoFuente
Objetos Almacenados Almacenes
1 Procesamiento 3 Generación2 Validación
0 Parseo
COMPILADOR 5 - FASES DEL COMPILADOR
CLASES FUNDAMENTALES QUE DEFINEN AL COMPILADOR
Definí algunas clases fundamentales que están encargadas de resolver los problemas
anteriormente descriptos. Algunas de ellas son abstractas y tienen múltiples clases que heredan de ella,
por lo general, me limitaré a describir los objetivos de cada una y no los detalles de su implementación.
PSDLCOMPILER
Esta clase modela al compilador en sí mismo, es la cara visible. Toma como entrada un archivo
fuente que será compilado y da como resultado los archivos fuentes generados. El archivo fuente es
procesado por el parser, el cual genera el árbol que describe el archivo fuente original. Este árbol será
procesado por esta clase mediante el patrón Visitor descripto anteriormente, dado que PSDLCompiler
implementa la interfase PSDLParserVisitor.
PSDLPARSER
Esta clase es la encargada de procesar el archivo de entrada generando el árbol que lo
represente. El compilador tiene una instancia de esta clase como atributo.
NODEPROCESSOR
Algunos nodos del árbol requieren de varios niveles del mismo para completar una definición, por
lo general, para cada nodo del árbol de este tipo que el compilador visite, éste creará una instancia del
procesador de nodo que es responsable de tomar acciones con la rama del árbol que se esté visitando.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
61
Estas clases implementan la interfase NodeProcessor. Cuando se termine de procesar este nodo, el
compilador se lo hará saber al procesador con un mensaje de tipo ‘finalizarProceso’.
COMPILEPROCESS
El compilador creará un CompileProcess inicial para procesar el árbol. Esta clase modela el
proceso de compilación de un archivo. El proceso tiene como objetivo recolectar las definiciones
declaradas en el árbol. Estas definiciones serán instancias de FullTypeDefinition. Existirá una instancia de
CompileProcess por cada archivo procesado, es decir, si se incluye un archivo en el archivo fuente
original, esto resultará en la creación en otro proceso de compilación para dicho archivo. Adicionalmente,
el proceso mantiene información de estado del procesamiento del árbol, que es muy importante para
realizar las validaciones.
FULLTYPEDEFINITION
Es una superclase que modela una construcción en el lenguaje Java. Existen dos clases concretas
que heredan de ella una es InterfaceDefinition, y la otra ClassDefinition. La primera modela una interfase
y la segunda una clase. Esta clase consta de los atributos básicos que puede tener un objeto Java, tales
como: definición de métodos y atributos.
Estas definiciones serán construidas a medida que se procese el árbol por objetos de tipo
BaseStorageElementBuilder. Los builders serán notificados de los atributos de las definiciones a medida
que el árbol sea procesado.
BASESTORAGEELEMENTBUILDER
Builder (constructor en inglés) es un patrón de diseño que permite encapsular la construcción de
un objeto complejo permitiéndole al objeto que lo utiliza deslindarse de la responsabilidad de cómo
construir dicho objeto [Gamma01].
Existen diferentes subclases de BaseStorageElementBuilder, una para cada tipo de construcción:
objetos almacenados abstractos que generan interfases Java, objetos almacenados concretos que
generan clases Java, almacenes abstractos que generan interfases y almacenes concretos que generan
clases.
Dichos builders tratan con el problema de la generación de definiciones que se correspondan con
la implementación del servicio de persistencia, para ello utilizan otra clase muy importante llamada
TargetCompiler.
TARGETCOMPILER
TargetCompiler es una interfase que define las operaciones requeridas para generar definiciones
concretas (clases Java) que dependen de la implementación del servicio. Por ejemplo, de cual clase debe
heredar una clase que represente un objeto almacenado concreto que deba ser utilizado por una
implementación del servicio de persistencia que se basa en archivos.
IDENTIFIERPROCESSOR
Cada proceso de compilación necesita de un objeto que será el encargado de procesar los nodos
que representen un identificador en el árbol. Esta clase es necesaria debido que los identificadores
pueden ser definidos tanto en forma absoluta como relativa. Cuando están en forma relativa, “qué
identifican”, estará dado por el módulo que el proceso esté navegando.
Cada proceso tiene un procesador de identificadores que lleva la cuenta de los identificadores que
no fueron procesados realmente, es decir, si este objeto tiene que procesar un nodo de identificador es
porque se produjo un error interno en el compilador, y alguna definición no fue contemplada o fue mal
procesada. Cuando el compilador visite un nodo que defina una entidad, el objeto que procese ese nodo
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
62
será responsable de indicarle al proceso cual es el objeto que procesará el siguiente nodo de identificador
en el árbol.
De esta forma cada vez que se visite un nodo que represente un identificador, el procesador que
esté definido en el proceso será notificado de ello.
ABSTRACTIDENTIFIERPROCESSOR
Existen múltiples nodos del árbol que dependen de un identificador para concretar una
definición válida. Esta clase base permite tomar del proceso el identificador para luego hacer con él lo
que corresponda al tipo de nodo.
MODULEIDENTIFIERPROCESSOR
Esta clase es una subclase de AbstractIdentifierProcessor y además implementa la interfase
NodeProcessor, cuando un nodo que identifique un módulo sea visitado, el compilador le indicará al
proceso que el procesador de identificadores a utilizar será una instancia de esta clase. Cuando esta clase
procese un identificador le indicará al proceso que el paquete de Java que se está procesando en ese
momento, es el anterior más el nuevo identificador. Cuando se termine de procesar el nodo que define al
módulo, se restaura en el proceso el paquete que se estaba procesando anteriormente.
HOLDERGENERATOR Y HELPERGENERATOR
Según la especificación, las interfases generadas deben generar sus respectivas clases Helper y
Holder acordes con la especificación de CORBA [IDL2Java]. Todas las definiciones abstractas del servicio
requieren de sus respectivas clases Holder y Helper. HolderGenerator y HelperGenerator se encargan, en
base a una instancia de FullTypeDefinition, de generar las clases Helper y Holder respectivamente. Una
clase holder se utiliza como objeto que permite pasar parámetros a métodos IDL, tanto de entrada como
de salida. Dicha clase almacena un objeto del tipo que transporta. Soporta operaciones para leer o
escribir el objeto que almacena en un stream de bytes (una secuencia de bytes que pueden definir un
conjunto de objetos en su representación binaria). Las clases Helper, se utilizan para interactuar con el
objeto genérico Any de CORBA. Any es un contenedor de cualquier objeto definido en IDL. Se necesita de
una clase Helper, para insertar y sacar un objeto de un tipo específico del contenedor.
COMPILERDEFINITIONWRITER
Esta clase es la responsable de plasmar en archivos fuente Java, las definiciones generadas por el
proceso de compilación. El compilador tiene un atributo de este tipo que será invocado una vez por cada
definición recolectada por el proceso al finalizar el proceso de compilación. Para generar el archivo
fuente, esta clase creará una instancia de un objeto de tipo FileBuilder, que procesará la definición a
construir interrogándola sobre cada uno de sus atributos.
PRIMERA FASE – PROCESAMIENTO
El objetivo de esta fase es generar un CompileProcess, que contendrá todas las definiciones que
tendrán que ser generadas en código fuente en la tercera fase, junto con otras que son necesarias para
generar y/o validar las definiciones. Para ello se parte del árbol generado por el parser, cuya raíz, en el
caso de mi definición del lenguaje, será siempre de tipo ‘Nodepsdl_specification’.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
63
class ClassDefinitions
parser::SimpleNode
+ childrenAccept(PSDLParserVisitor, Object) : Object+ dump(String) : void+ getFirst() : Token+ getLast() : Token+ j j tAddChild(Node, int) : void+ j j tClose() : void+ j j tGetChild(int) : Node+ j j tGetNumChildren() : int+ j j tGetParent() : Node+ j j tOpen() : void+ j j tSetParent(Node) : void+ setFirst(Token) : void+ setLast(Token) : void+ SimpleNode(PSDLParser, int)+ toString() : String+ toString(String) : String
parser::Nodepsdl_specification
+ j j tAccept(PSDLParserVisitor, Object) : Object+ Nodepsdl_specification(PSDLParser, int)
COMPILADOR 6 - NODOS DE ÁRBOL
Todos los nodos del árbol heredan de la clase SimpleNode. Las dos clases que se muestran en el
diagrama anterior, fueron generadas por el procesador jjtree. A los efectos de procesar el árbol, los
únicos métodos que importan son ‘jjAccept’ y ‘childrenAccept’. El primero tiene que estar implementado
en cada clase de nodo en particular, dado que así lo requiere el patrón Visitor. El segundo será invocado
por el compilador para procesar el siguiente nivel del árbol. La implementación es tan simple como tomar
cada nodo hijo e invocar al método jjAccept sobre él.
Entonces el proceso comienza cuando se invoca al método jjAccept sobre el nodo raíz, con dos
parámetros, el primero el compilador mismo y el segundo el proceso de compilación. Para cada nodo los
pasos son:
o Se visita el nodo. El nodo le indica al compilador que se está visitando un nodo del tipo que corresponde.
o El compilador realiza una operación en base al tipo de nodo, o Se visitan a los hijos del nodo, donde el proceso comienza nuevamente, hasta que no queden
nodos por visitar.
En este punto, el proceso de compilación habrá recolectado todas las definiciones generadas al
visitar nodos de definición.
El siguiente diagrama de objetos muestra la interacción entre los objetos para concretar la
primera fase, donde se tendrá como resultado el proceso de compilación.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
64
object SimpleCompileProcess
aCompiler :PSDLCompiler
aParser :PSDLParser
process :CompileProcess
endUser
«interface»target :
TargetCompiler
registrySingleton :TargetCompilerRegistry
w riter :CompilerDefinitionWriter
aSpecification :Nodepsdl_specification
identifierProcessor :CounterIdentifierProcessor
1: PSDLCompi ler(fi leName,targetName) :aCompiler
1.1: forName(name) :target
1.2: lookup
1.3: PSDLParser(fi leName) :aParser
1.4: CompilerDefinitionWriter(aCompi ler) :writer
2: compi le(targetFolder)
2.1: parse() :aSpeci fication
2.2: Nodepsdl_speci fication(aParser,id)
2.3: CompileProcess(aCompiler,idProcessor)
2.4: new
3: j jtAccept(visi tor,process)
3.1: visit(node,process) :Object
3.2: childrenAccept(aCompiler, process)
3.3: finishProcess(compi ler,process)
COMPILADOR 7 - PRIMERA FASE
En mi implementación existen cuatro tipos de nodos que agregan definiciones al proceso de
compilación, los cuales son: nodos de definiciones abstractas de objetos almacenados y almacenes, y los
nodos de las respectivas definiciones concretas.
Cada tipo de nodo que genere una definición tiene una subclase de BaseStorageElementBuilder,
que se encargará de acumular la información necesaria para construir la definición. Por ejemplo, en el
caso de un objeto almacenado abstracto, el builder que se crea es de tipo: AbstractStorageTypeBuilder.
PRIMER EJEMPLO DE GENERACIÓN DE UNA DEFINICIÓN:
A continuación voy a ilustrar la secuencia de ejecución para llegar a construir un proceso de
compilación que contenga la definición de un objeto almacenado. El código PSDL a procesar será el
siguiente:
En el código anterior se define un objeto almacenado dentro de un módulo con un único estado
llamado nombre y de tipo string (secuencia de caracteres).
module ejemploFaseUno { abstract storagetype Prueba { state string nombre; }; };
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
65
El árbol generado por el parser será el siguiente:
PASOS REQUERIDOS
1- El compilador genera un CompileProcess, inicialmente con un IdentifierProcessor que llevará la cuenta de los identificadores no procesados, una instancia de CounterIdentifierProcessor.
2- Se inicia el proceso de recorrido del árbol con el nodo raíz Nodepsdl_specification, llamando al método jjAccept con el proceso creado anteriormente.
3- El nodo Nodepsdl_specification le indica al compilador que se visita a este tipo de nodo llamando al método visit con sí mismo como parámetro y el proceso que recibió anteriormente. Como el compilador no necesita realizar otra tarea adicional, sigue el proceso de recorrido del árbol llamando al método childrenAccept en el nodo visitado.
4- El primer nodo hijo es Nodepsdl_module, el cual termina llamando al método visit del compilador. Al visitar este nodo el compilador instancia un objeto de tipo ModuleIdentifierProcessor. Cuando el ModuleIdentifierProcessor es creado, éste le indica al proceso que el nuevo procesador de identificadores será él. Luego de esto, se sigue procesando la rama del árbol debajo del módulo.
5- El primer nodo debajo del nodo del módulo es Nodeidentifier. Al visitar este tipo de nodo el compilador le pide al proceso, el procesador de identificadores para procesar este nodo, que en este caso será ModuleIdentifierProcessor (definido en el paso anterior). Esta clase toma el identificador y genera una definición de paquete Java indicándole al proceso actual cual es el paquete que corresponde, en este caso: será ejemploFaseUno. Luego restaura el procesador anterior y sigue procesando los nodos hijos. Como el nodo de identificador no tiene nodos hijos, se vuelve a procesar los nodos hijos del nodo que definió el módulo.
6- El siguiente nodo hijo es Nodeabstract_storagetype. Cuando el compilador visita este tipo de nodos, crea una instancia de AbstractStorageTypeBuilder. Cuando este tipo de nodo es creado, éste le pide al proceso actual el paquete Java procesado (definido en el paso anterior), inicializando la definición del objeto con él. Además le indica al proceso actual que él mismo será el procesador de identificadores. Luego de ello se procesan el nodo con sus hijos.
7- El primer nodo hijo será Nodeidentifier. Cuando se visite este nodo se terminará utilizando el identificador de procesos definido en el proceso, que es justamente el nodo Nodeabstract_storagetype, definido en el paso anterior. Este nodo tomará el identificador definido, en este caso Prueba, y lo asociará a la definición que se esté construyendo del objeto almacenado. Restaura nuevamente el procesador de identificadores anterior y sigue procesando los nodos hijos.
8- El siguiente nodo hijo de Nodeabstract_storagetype es Nodepsdl_state_dcl, en este caso el compilador creará una instancia de objeto de tipo StateMemberBuilder, y luego procesará los nodos hijos de éste.
9- El primer nodo hijo será Nodepsdl_state_type_spec, el cuál define el tipo de dato del estado que se está definiendo. En este caso, el compilador instancia un objeto de tipo StorageTypeStateDeclarationProcessor, que al ser creado le indica al proceso actual que él será
Nodepsdl_specification |-->Nodepsdl_module |-->Nodeidentifier(ejemploFaseUno) |-->Nodeabstract_storagetype |-->Nodeidentifier(Prueba) |-->Nodepsdl_state_dcl |-->Nodepsdl_state_type_spec | |-->Nodestring_type |-->Nodesimple_declarator |-->Nodeidentifier(nombre)
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
66
el objeto encargado de registrar los próximos identificadores a procesar del árbol. Luego de esto se procede a procesar los nodos hijos del nodo.
10- El único nodo hijo de Nodepsdl_state_type_spec, en este caso es Nodestring_type, que al ser procesado por el compilador lo único que hace es indicarle al builder actual que el tipo de dato es STRING.
11- Como no hay más nodos hijos del Nodepsdl_state_type_spec, se le indica a este nodo que se ha completado el procesamiento de sus hijos. En este punto, el nodo tiene la oportunidad de armar la definición completa del tipo de dato del estado. Como en este caso es un STRING no hay más nada que hacer, pero si se tratara de un tipo de dato, como por ejemplo, una referencia a otro tipo de objeto almacenado definida en forma relativa al paquete actual, se transformaría en una referencia a un tipo absoluto. Luego se restaura el procesador de identificadores anterior. Y se siguen procesando los nodos hijos de Nodepsdl_state_dcl.
12- El próximo nodo hijo es Nodesimple_declarator. Cuando el compilador detecta este tipo de nodos instancia un objeto de tipo SimpleDeclaratorIdentifierProcessor, que simplemente se pone a sí mismo como procesador de identificadores. Al procesar el siguiente nodo de identificador se almacenará el valor nombre.
13- Como no hay más nodos por procesar, StateMemberBuilder tomará el identificador generado por SimpleDeclaratorIdentifierProcessor, y definirá el nombre del estado. En este punto, el nodo puede realizar una validación simple, que es que no exista otro estado con ese nombre dentro de la lista de estados que se pueden definir agrupadamente. Este tipo de validaciones no pueden ser realizadas por el parser, ya que no son parte de la definición gramatical del lenguaje.
14- Luego de que se procesaron todos los nodos por AbstractStorageTypeBuilder, éste tomará como resultado de visitar la rama, lo que define al StateMemberBuilder, y lo agregará como definición de un estado. Además, realizará una serie de validaciones, por ejemplo, si el tipo de estado es una referencia a otro tipo de objeto, éste debe estar definido previamente. Nuevamente estas validaciones se hacen en este punto ya que no es están definidas en la gramática del lenguaje que define al parser. De todas formas esta validación será parcial ya que es también necesario validar la unicidad de los estados dentro de la jerarquía a la que pertenezca el objeto almacenado. Esta validación será realizada en la siguiente fase, dado que en este punto no está definida dicha jerarquía.
15- En este punto AbstractStorageTypeBuilder terminó de procesar todos los nodos hijos y puede registrarse con el proceso actual como un tipo de objeto almacenado definido durante el proceso. Al procesar el registro, el proceso tiene la oportunidad de realizar algunas operaciones, como validar definiciones duplicadas o reemplazar una definición anterior parciales (forward). El caso de las definiciones parciales se da cuando en el archivo fuente se encuentra una definición con solamente un identificador, de forma tal que pueda ser referenciada en otras definiciones sin estar aún definida completamente en el documento.
16- Se han terminado de procesar todos los nodos. El compilador le indica al procesador de identificadores actual, que se ha completado el proceso. CounterIdentifierProcessor sabrá en este punto si alguno de los identificadores no ha sido considerado, en cuyo caso generará un error interno de compilación.
17- Finalmente, el proceso de compilación está completo y la primera fase del compilador.
El proceso de la fase anterior en el ejemplo da como resultado un objeto de tipo CompileProcess,
que contendrá un objeto que, en principio, representa la posibilidad de generar el código para la
definición del objeto almacenado ejemploFaseUno.Prueba. Este objeto es de tipo
AbstractStorageTypeBuilder y representa la posibilidad de genera código porque su uso depende de la
siguiente fase. En algunos casos, el proceso de compilación puede ser generado como resultado de la
inclusión de un archivo en el documento original que se esté compilando, y su utilidad es la de permitir
validar las definiciones que se declaren en el archivo original y no la de generar código fuente.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
67
Como puede verse, la compilación de una estructura simple como la descripta anteriormente
requiere de múltiples objetos e interacciones entre los mismos.
Resumiendo, el resultado siempre es un objeto de tipo CompileProcess. Dicho objeto almacena la
lista de definiciones existentes en el archivo original. Estas definiciones se concretan siempre en objetos
que heredan de la clase BaseStorageElementBuilder. Esta familia de clases sigue el patrón Builder
(constructor) definido en Gamma01. Estos objetos serán utilizados en la última fase para generar los
archivos fuente resultantes.
SEGUNDA FASE – VALIDACIÓN
Esta fase se caracteriza por realizar todas las validaciones necesarias que no pudieron ser
concretadas en las fases anteriores. A medida que se va procesando el árbol se va recolectando
información que sirve tanto para construir el resultado, como para realizar todas las validaciones que
describo a continuación.
Al invocar el método validate sobre el proceso de compilación generado en la fase anterior se
inicia esta fase. Cada una de las definiciones acumuladas será validada. Cada definición generada hereda
de la clase abstracta BaseStorageElementBuilder, la cual define un método validate que será invocado
para cada definición registrada en el proceso. Cuando la fase sea exitosa el resultado de la validación del
proceso será la lista de nombres de las definiciones que deberán ser generadas. Esta lista será la entrada
para la siguiente fase. Cuando la fase falle, su resultado será una excepción de tipo
PSDLCompilerException.
FALLOS EN EL PROCESO DE VALIDACIÓN
Existen múltiples circunstancias por las cuales esta fase puede fallar. Cada tipo de falla tiene su
representación en una subclase de PSDLCompilerException, a continuación enumeraré las más comunes:
- DefinitionNotFoundException: esta excepción indica que en alguna definición PSDL se hace referencia a otra definición, y ésta no pudo ser encontrada. Las causas pueden ser múltiples, por ejemplo hacer referencia a una definición que se encuentra más adelante en el archivo sin haberla declarado como forward, o simplemente un error de tipografía en el nombre.
- DuplicateDefinitionFoundException: ocurre cuando se trata de definir una entidad que ya se encontraba definida anteriormente, en el mismo archivo o en otro incluido anteriormente.
- IllegalFactoryMethodException: esta excepción representa que el compilador encuentra una definición inválida de un método factory. Esto ocurre cuando se definen nombres de estados no existentes como parámetros al método, o bien cuando se agrega más de una vez el nombre del estado al método.
- IllegalKeyDefinitionException: representa una definición invalida de una clave. Puede ser tanto por contener un estado no existente o tener más de una vez el mismo estado como parte de la clave.
- IllegalStateDefinitionFound: se trata de una excepción que se produce cuando la definición de un estado esta dada por un tipo inválido, como por ejemplo, un almacén en vez de un tipo de objeto almacenado.
- IncludeFileException: es la excepción que se genera cuando se trata de incluir un archivo y este no fue encontrado.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
68
class exceptions
DefinitionNotFoundException
+ DefinitionNotFoundException(ful lTypeDefinition, referencingDefinition)
DuplicateDefinitionFoundException
+ Dupl icateDefini tionFoundException(definition, message)
IllegalDefinitionFoundException
+ IllegalDefinitionFoundException(fullTypeDefinition, message)
IllegalFactoryMethodException
+ Il legalFactoryMethodException(factoryName, message, referencer)
IllegalKeyDefinitionException
+ Il legalKeyDefinitionException(keyName, message, referencer)
IllegalStateDefinitionFound
+ Il legalStateDefinitionFound(parent, stateDefinition, message)
IllegalStateException
+ IllegalStateException(message)+ IllegalStateException(message, e)
IncludeFileException
+ IncludeFileException(message)+ IncludeFileException(message, cause)
RuntimeException
PSDLCompilerException
+ PSDLCompilerException(message)+ PSDLCompilerException(message, cause)
COMPILADOR 8 - EXEPCIONES DE VALIDACIÓN
Existen dos clases de excepciones de validación que representan errores mas generales de la
definición, que son: IllegalDefinitionFoundException y IllegalStateException.
La primera excepción se genera cuando se llega a un punto del procesamiento en el que la
definición que se esta procesando debe ser marcada como inválida dado que viola alguna regla. Pero esto
no se debe a, por ejemplo, un estado mal definido o un estado duplicado, sino a que la suma de atributos
de la definición la hacen inválida. Por ejemplo, cuando en la definición de una herencia de almacenes se
detecta que el tipo almacenado de un almacén hijo no es un subtipo de tipo almacenado por alguno de
los almacenes padres. O cuando se trata de agregar una clave primaria a un nodo que no es la raíz de la
familia de almacenes. Por lo general, como este tipo de errores no se pueden atribuir a una parte de la
definición, el usuario final tendrá que verificar a que corresponde el error.
La segunda excepción IllegalStateException está asociada a potenciales errores internos del
compilador. Por ejemplo, tratar de construir definiciónes en base a definiciones no validadas, o tratar de
agregar una definición más de una vez a un proceso de compilación, etc. Este tipo de errores es probable
que se produzcan cuando se agreguen extensiones al compilador para generar código para otros servicios
de persistencia.
A continuación voy a ejemplificar la serie de pasos para la validación del ejemplo de la fase
anterior.
VALIDACIÓN DEL PRIMER EJEMPLO DE GENERACIÓN DE UNA DEFINICIÓN:
1- El compilador invoca al método validate del CompileProcess. 2- Para cada definición acumulada, el proceso ejecuta el método validate. En este caso la única
definición acumulada es de tipo AbstractStorageTypeBuilder. a. AbstractStorageTypeBuilder verifica que no se trate de una definición forward. Esto sería un
error ya que se referencia a una definición no existente en el documento. b. AbstractStorageTypeBuilder verifica que no existan operaciones o método declarados
dentro de la jerarquía del objeto. Para ello visita todas las definiciones que sean parte de su jerarquía recolectando todos los métodos definidos. Todas las definiciones de sus supertipos tienen que ser accesibles desde el proceso de compilación o alguno de los procesos de compilación resultantes de los archivos incluidos en el proceso.
c. AbstractStorageTypeBuilder verifica que no existan nombres de estados duplicados en la jerarquía. El proceso es similar al punto anterior. Como la definición de un objeto almacenado abstracto soporta la herencia en forma diamante, pueden existir estados o método duplicados en la jerarquía, pero lo importante es que estas definiciones duplicadas provengan del mismo origen, en este caso de la misma definición de objeto almacenado.
d. AbstractStorageTypeBuilder se marca como validada y el proceso de validación concluye.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
69
TERCER FASE – GENERACIÓN.
Una vez validado el proceso de compilación generado en la fase uno, se deben generar todos los
archivos fuente Java que resulten de compilar el archivo PSDL. Esta fase da comienzo cuando se invoca al
método generate del objeto CompileProcess resultante de la fase uno.
Como mencioné anteriormente según la especificación del servicio, las definiciones abstractas
debieran ser independientes de la implementación del servicio, mientras que para las concretas se
recomienda mantenerlas lo más separadas posible de los detalles de implementación del servicio. Esto
implica que el código a generar dependerá en algunas circunstancias de la implementación del servicio y
en otras será dependiente de la especificación del mismo.
Hasta este punto, ninguna parte del compilador dependía de la implementación del servicio. Es
una prioridad mantener la separación del compilador de la impementación del servicio de persistencia, ya
que me permite tener dos o más implementaciones del servicio de persistencia sin tener que modificar al
compilador. De igual forma le permite a otra persona utilizar el mismo compilador para su propia
implementación del servicio de persistencia. En definitiva, promueve un bajo acoplamiento entre el
compilador y la implementación del servicio.
Esto requiere que al menos una parte del compilador sea configurable o extensible en función del
servicio de persistencia que se quiera utilizar. Cualquiera de los posibles servicios de persistencia que
utilicen el compilador, requerirán también generar el código fuente resultante, parte del cuál será
compartido y otra será propia.
Al tener un único compilador para múltiples servicios, traté de lograr el mayor grado de
reutilización de código, de manera de minimizar el trabajo requerido. Para ello dividí esta fase en tres
partes, expresadas en el gráfico siguiente:
composite structure Generación
Propio del Serv icio Común a todo Serv icio
Generación de Código
COMPILADOR 9 - TRES PARTES
GENERACIÓN DE CÓDIGO FUENTE
Cualquier implementación del servicio requerirá generar código fuente. La forma de lograr un alto
grado de reutilización entre diferentes implementaciones del servicio es definir un modelo común que
puedan utilizar todas las implementaciones. El gráfico 10 muestra el “qué generar”, básicamente es un
modelo casi completo de cualquier elemento que se quiera definir en lenguaje Java, una clase, interfase,
etc.
Se trata de una serie de clases que modelan las distintas partes que podrían componer un archivo
fuente. Las clases representadas en el gráfico son la base para un generador de código fuente
independiente del compilador. A continuación, daré una breve descripción de algunas de las clases más
importantes.
� Type Definition: la definición de un tipo de objeto en Java está dada por un paquete y un nombre. � Import Declaration: todo archivo fuente Java puede importar otras definiciones para poder
utilizarlas directamente. Considero que siempre se importan tipos de objetos, por lo cual, una declaración de import es simplemente una referencia a una definición de un tipo. También se podrían importar paquetes enteros pero no creo que sea algo necesario, por lo que esta definición no lo soporta.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
70
� Instance Definition: es simplemente una definición de un tipo nombrada, de forma tal que puede ser utilizada como parámetro a un método o en la declaración de un atributo.
class generationLite
NestedGenerator
AccessibilityDefinition
AttributeDefinition
ClassDefinition
ConstructorDefinition
DefinitionFactory
ElementTypeKeyw ord
FullTypeDefinition
Comparable
ImportDeclaration
InstanceDefinition
InterfaceDefinition
Keyword
MethodDefinition
OperationDefinition
NestedGenerator
PackageDefinition
TypeDefinition
COMPILADOR 10 - QUÉ GENERAR
� Accessibility Definition: es la modelización de los niveles de accesibilidad que pueden definirse en Java, por ejemplo público, estático, etc.
� Attribute Definition: se define como la definición de una instancia con un nivel de accesibilidad. Esto permite, por ejemplo, modelar un atributo de una clase.
� Operation Definition: Una operación es una superclase, que sirve como base para definir métodos y constructores. Una operación está definida por una lista de instancias que serían los parámetros, junto con una visibilidad y una definición de un tipo que representa el tipo de valor que retorna la operación. Un método solo agrega un nombre a la superclase.
� FullTypeDefinition: Se trata de otra superclase que representa la definición completa de un tipo en Java. Es la base para definir una interfase o una clase. Se compone de una lista de atributos, una lista de métodos y la visibilidad del tipo. También tiene una lista de interfase que implementa o extiende. La definición de una clase agrega la posibilidad de heredar de otra clase.
� Keyword: es cualquier palabra clave reservada en el lenguaje. � DefinitionFactory: Es una clase basada en el patrón de diseño Factory [Gamma01]. Básicamente se
trata de una clase que permite generar definiciones basadas en clases reales de Java.
Cada implementación del servicio, será responsable de generar sus clases mediante el uso de
este modelo.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
71
Desde la perspectiva del CompileProcess, cada builder generado en la primer fase generará al
menos un FullTypeDefinition cada uno. Cada una de estas definiciones de tipos genera un archivo fuente.
Como se construyen estos tipos es transparente para el compilador y será la tarea del TargetCompiler.
class fileBuilding
«interface»
Generator
+ addTo(TypeDefinitionAppender) : void+ wri teYouSelfInto(Fi leBui lderColaborator) : void
TextAppender
«interface»
FileBuilderColaborator
+ attributeEnd() : void+ attributeStart() : void+ bodyEnd() : void+ bodyStart() : void+ getFileProperties(Generator) : Map<String, Object>+ getStringRepresentation(TypeDefini tion) : String+ methodEnd(boolean) : void+ methodStart(boolean) : void+ wri teAccesibi l ity(Accessibi l i tyDefinition) : void+ wri teBlock(String, Generator, Map<String, Object>, boolean) : void+ wri teExceptions(List<TypeDefini tion>) : void+ wri teInstance(InstanceDefini tion) : void+ wri teKeyword(Keyword) : void+ wri teLiteral(String) : void+ wri teOperationWithParameters(String, List<InstanceDefini tion>, boolean) : void+ wri teSuperClass(TypeDefini tion) : void+ wri teSuperInterfaces(Keyword, List<TypeDefinition>) : void+ wri teTemplateText(URL, Map<String, Object>, boolean) : void+ wri teTypeDefini tion(TypeDefinition) : void
T:extends FullTypeDefinition
FileBuilder
+ bui ld(URL, Fi le) : void+ bui ld(Fi le) : void+ Fi leBui lder(T , Map<String, Object>)+ with(D) : Fi leBui lder<T>
FileBuilder::LocalFilebuilderColaborator
generation::CompilerDefinitionWriter
+ COMPILER_KEY: String = "compi ler" {readOnly}- headerTmpl: URL {readOnly}- prop: Map<String, Object> {readOnly}
+ Compi lerDefini tionWri ter(PSDLCompi ler)# getCompi lerProperties() : Map<String, Object># getFileHeaderTmpl() : URL+ wri te(T, Fi le) : void
FullTypeDefinition
compiler::CompileProcess
«real ize»
«real ize»
#types
«instantiate»
«instantiate»
«instantiate»
COMPILADOR 11 - GENERACIÓN DE ARCHIVOS
El gráfico anterior define como a partir del modelo, se procede a generar los archivos fuentes.
Cada parte del modelo implementa la interfase Generator. Esta interfase define que el objeto que la
implementa sabe como agregarse a sí mismo a un objeto encargado de construir un archivo, mediante el
método writeYourSelfTo. Este método recibe como parámetro un objeto que implementa la interfase
FileBuilderColaborator. Una instancia de FileBuilder, será la encargada de tomar cada una de las
definiciones construidas en el CompileProcess y mediante una llamada al método writeYourSelfTo,
construirá el archivo fuente para cada tipo definido.
“PROPIO DEL SERVICIO”, CONFIGURACIÓN DEL COMPILADOR
La implementación del servicio de persistencia debe proveerle al compilador una clase que le
permita a éste generar código propietario. Esta clase deberá implementar la interfase TargetCompiler.
Esta interfase tiene definidas todas las operaciones que requieran construir definiciones particulares para
la implementación del servicio. Por ejemplo, el método generateRead, indica que debe generarse la
implementación de un método para leer un estado de un objeto almacenado. El compilador conoce los
TargetCompilers que existen por medio de la clase TargetCompilerRegistry, que modela un registro al
que se accede por nombre del target. Cuando el compilador es creado se le indica a éste cual es el
servicio de persistencia para el que se generará el código. Con este nombre se accederá al registro del
compilador para recuperar el TargetCompiler necesario.
¿Cómo se definen los posibles targets para el compilador?. TargetCompilerRegistry es un
Singleton (Gamma01), cuya única instancia de esta clase es creada por demanda. Dicha instancia es
configurada mediante un archivo de propiedades llamado pss_pmi_compiler.properties. En Java todos
los archivos de propiedades son simples archivos de texto. Dichos archivos representan un mapa con
entradas definidas por una clave y valor. Cada línea que no sea un comentario, comienza con el valor de
la clave, seguida por un carácter ‘=’, finalizado con el valor de dicha clave. En este archivo debe existir
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
72
una entrada con el nombre “registered”, cuyo valor debe contener la lista de “targets” registrados en el
compilador. Por ejemplo: “registered=transient, persistent”. Por cada una de los targets registrados, tiene
que haber otra entrada en dicho archivo que contenga el nombre completo de la clase que implementa la
interfase TargetCompiler, para dicho target.
class compileGenerationLite
AbstractGenerator
AbstractTargetCompilerGenerationTemplates
CompilerTemplates{leaf}
HelperGenerator HolderGenerator
«interface»
TargetCompiler
+ generateConstraintValidations(process, homeBui lder, defini tion, keyIdentifier, stateNames) : void+ generateCreateFour(process, homeBuilder, definition, methodDefini tion) : void+ generateCreateOne(process, homeBui lder, defini tion, methodDefinition) : void+ generateCreateThree(process, homeBuilder, definition, methodDefini tion) : void+ generateCreateTwo(process, homeBui lder, defini tion, methodDefinition) : void+ generateFactoryMethod(process, homeBuilder, factoryMethodBuilder, definition, methodDefini tion) : void+ generateFinderMethod(process, homeBuilder, definition, methodDefinition, keyBui lder) : void+ generateLifeCycleListener(process, storageHome, definition, stateBuilder) : void+ generateRead(process, builder, definition, attribute) : void+ generateReadRef(process, bui lder, defini tion, attribute) : void+ generateReadWri te(process, storageTypeBuilder, bui lder, defini tion, attribute) : void+ generateRefFinderMethod(process, homeBui lder, defini tion, methodDefini tion, keyBuilder) : void+ generateStorageHome(process, storageHome, definition, isAbstract) : void+ generateStorageObject(process, storageType, defini tion, isAbstract, properStates) : void+ generateWrite(process, storageTypeBui lder, bui lder, definition, attribute) : void+ generateWriteRef(process, storageTypeBuilder, bui lder, defini tion, attribute) : void+ getCompi leInstance() : TargetCompi ler+ getDescription() : String+ getName() : String+ getTemplates() : Compi lerTemplates+ ini t(name, registry) : void
TargetCompilerRegistry
+ defaultOne() : TargetCompi ler+ forName(name) : TargetCompiler+ getInstance() : TargetCompilerRegistry+ getProperty(key) : String+ getRegistered() : Set<String>
memory::TransientTargetCompiler
persistent::PersistentTargetCompilerpss_pmi_compiler.properties
-instance
«realize»
-templates
configures«flow»
COMPILADOR 12 - CÓMO GENERARLO
LO QUE ES COMÚN A TODO SERVICIO
El modelo presentado anteriormente para la representación un archivo fuente, permite que las
distintas implementaciones de TargetCompiler, no tengan que tratar directamente con la generación de
los archivos. Dicho modelo es parte de módulo que permite generar código fuente mediante la utilización
de plantillas (templates) que en realidad no es parte del compilador. Este módulo podría ser utilizado por
cualquier aplicación Java que requiera generar archivos de código fuente en forma dinámica.
En el gráfico 12 se muestra la clase AbstractTargetCompiler. Su único objeto es el de tener un
punto común donde poner todas las partes que podrían ser comunes a todo TargetCompiler, como por
ejemplo, todos los métodos para acceder a los atributos que definen el estado en los objetos
almacenados. Todos estos métodos comparten la definición de los mismos, pero varían en su
implementación. En este punto es donde entra en juego la clase CompilerTemplates.
CompilerTemplates, es una especie de registro donde se configura cual archivo de plantilla o
template, se utiliza para determinada operación o método. A simple vista podría pensarse que es
suficiente con tener diferentes plantillas para diferentes implementaciones de servicio, pero no es así. Por
ejemplo, muchas partes de las clases a generar requieren diferentes implementaciones de los métodos en
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
73
función del tipo y cantidad de atributos persistentes que tengan los objetos almacenados. Esto requiere
que los métodos sean construidos dinámicamente en función de lo que se este compilando.
GENERACIÓN DE CÓDIGO PARA EL PRIMER EJEMPLO
Para mostrar como interactúan todas estas partes, voy a continuar con el ejemplo de la fase
anterior, pero esta vez para esta tercera y última fase.
En el código PSDL anterior, se define un objeto almacenado abstracto. Según la especificación
PSDL, en Java esto se traduce a una interfase Prueba, en el paquete ejemploFaseUno y con dos métodos
que permitirán acceder al estado nombre, tanto como para lectura como escritura. Adicionalmente al
tratarse de una interfase, se deben generar los clases Helper y Holder, para la misma.
Esta fase parte de un proceso de compilación (CompileProcess) ya validado, que se encuentra
listo para generar el código resultante.
PASOS
1- Para cada uno de los builders registrados en el CompileProcess de llama al método generate. En este caso sólo existe un builder registrado que representa al objeto almacenado abstracto. Y se trata de un AbstractStorageTypeBuilder.
2- AbstractStorageTypeBuilder construirá una instancia de InterfaceDefinition. a. Para cada tipo heredado, se agregará una extensión a la definición de la interfase construida.
Como en este caso, no se hereda de ninguna otra definición, se agregará automáticamente la extensión de la interfase StorageObject.
b. Para cada estado definido en el tipo, se llamará al método addTo, pasando como parámetro la definición de la interfase construida. Sólo existe una definición de estado “nombre”, que está representada por un objeto StateMemberBuilder. i. StateMemberBuilder en el método addTo, como primer medida, determinará si se trata de
una definición concreta o abstracta (como en este caso) para saber que tipo de métodos se deben generar. Construye una definición de atributo (AttributeDefinition) con el nombre del estado, luego llamará al método addReadAccessorFor con el atributo construido.
ii. El método addReadAccessor construye una instancia de MethodDefinition, en base al tipo de objeto del atributo construido anteriormente. Además le indica al TargetCompiler del proceso que se está ejecutando, que le agregue un encabezado al método construido. Este encabezado será a modo de comentario en sintaxis Java Doc. Si el método debiera ser implementado por tratarse de un tipo concreto, se llamaría al método generateRead del TargetCompiler. De esta forma, TargetCompiler es el único objeto que puede referenciar partes dependientes del servicio de persistencia para el cual se está compilando. Finalmente, StateMemberBuilder agrega el método a la definición.
iii. StateMemberBuilder llama al método addWriteAccessorFor. De manera análoga a addReadAccessor, se construye una definición de método. TargetCompiler le agrega un encabezado y finalmente se agrega el método a la definición que se está construyendo.
c. Para cada operación definida en el tipo almacenado se llama al método addTo del builder (OperationBuilder). En este caso no se han definido operaciones.
d. Finalmente, AbstractStorageTypeBuilder agrega al proceso la definición de la interfase construida, con el método addGenerated, indicándole que se deben generar la clase Holder para la interfase.
module ejemploFaseUno { abstract storagetype Prueba { state string nombre; }; };
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
74
e. El método addGenerated registra la definición construida y le indica al HolderGenerator del compilador que genere la clase Holder registrándola de igual forma en el proceso.
3- El CompileProcess toma cada una de las definiciones registradas y le indica al CompilerDefinitionWriter del compilador que las escriba en el directorio destino mediante el método write. En el caso de este ejemplo, las definiciones registradas son dos, la interfase y la clase Holder para la misma.
4- CompilerDefinitionWriter crea una instancia de FileBuilder con la definición a escribir, llamando al método build.
a. El método build de FileBuilder construye un PrintStream, en base a la definición a construir y el directorio donde se crearán las definiciones. En este caso, apuntará a un archivo llamado Prueba.java, en el sub directorio ejemploFaseUno. El primer paso a escribir será el encabezado para el archivo con información del compilador y la fecha de generación.
b. Luego se escribirá el paquete Java al que corresponda el archivo, mediante la definición de paquete (PackageDefinition) del tipo a escribir.
c. Los imports del archivo son generados dinámicamente mediante el uso del patrón Visitor (Gamma01). Los imports de un archivo en Java permiten utilizar referencias a clases sólo con su nombre sin tener que indicar el paquete. De esta forma si se importa el paquete java.util, se podrá hacer referencia a cualquier clase que esté contenida en él, como por ejemplo Collection. Con el patrón Visitor lo que se hace es recorrer todos los generadores de código (que implementen la interfase Generator) que constituyan la definición a construir, pasando como parámetro un objeto de tipo TypeDefinitionAppender. Cada generador de código que utilice una definición de tipo, la registrará en este objeto. Al final del proceso, este objeto conoce todos los tipos de objetos utilizados en el archivo y puede generar en forma ordenada y sin duplicados todos los imports que requiera la clase. El otro beneficio, es que el builder del archivo puede determinar en base a esto si necesita utilizar el nombre con el paquete de un tipo de objeto o solo el nombre. Esto permite que los archivos generados por el compilador contengan solo los imports requeridos por la clase, y además que sólo utilicen referencias absolutas a clases cuando existan ambigüedades con los nombres simples de las mismas (por ejemplo que se use una clase String que esté en otro paquete que no sea java.lang).
d. Finalmente, sólo resta escribir la definición en el PrintStream. Para ello todo Generator tiene un método writeYouSelfInto, que le permite escribirse a sí mismo dentro del archivo.
CONCLUSIONES DEL COMPILADOR El compilador cumple con el objetivo inicial de generar código Java en base a un archivo PSDL de
entrada. Por la forma en que está construido, permite reutilizar el mismo núcleo para diferentes
implementaciones del servicio de estado persistente, tal como lo ejemplificaré en la siguiente sección.
El grado de reutilización que propone el compilador, obedece principalmente a dos razones: la
primera es lograr un alto grado de reutilización. Esta es una buena práctica cuando se desarrolla software,
dado que permite optimizar el trabajo. En segundo lugar, porque al momento de la construcción del
compilador no tenía definido el servicio de estado persistente y al plantearlo como procesos separados
me permitió construir ambos en paralelo.
Antes de plantearme la posibilidad de construir el compilador, analicé la posibilidad de extender
algún compilador IDL, pero la mayoría de los compiladores (incluido el mío) tienen el mismo problema.
Los parsers son construidos con herramientas como Javacc que generan código Java que es
prácticamente imposible extender o modificar (por su elevado costo de hacerlo manualmente) si no es
por medio de la herramienta, por lo que esta posibilidad quedó descartada.
CONEXIÓN DEL SERVICIO DE PERSISTENCIA CON LA ORB
En CORBA todo servicio perteneciente a la especificación se accede de la misma forma, mediante
una referencia inicial a la ORB. Por ejemplo, uno de los servicios más utilizados, se llama NameService
(servicio de nombres), para acceder a este servicio se llama al método resolve_initial_references del
objeto ORB, pasándole como parámetro el nombre del servicio que se quiere acceder:
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
75
El nombre del servicio es, por lo general, una cadena de caracteres bien definida por la
especificación del servicio, en este caso se trata de NameService, mientras que en el caso del servicio de
persistencia se trata de PSS. A esta cadena de caracteres se la denomina objectId (identificador de
objeto).
La razón por la que los servicios son referenciados o accedidos por su nombre y no por un
método es simple: los servicios que están disponibles en la ORB dependen de la configuración de la
misma y no de la definición de ella. Los servicios de CORBA son contribuciones adicionales a la ORB que
pueden estar o no presentes en ella. Existen identificadores de objetos reservados por CORBA para
referenciar a los servicios bien conocidos (pág. 4-29 de CORBA01 de la especificación) entre los que se
encuentran NameService y PSS, pero mediante este mecanismo se puede acceder a cualquier objeto que
esté registrado como referencia inicial a la ORB.
La pregunta que se plantea aquí es ¿cómo sabe la ORB que objeto le corresponde a cada
nombre?. La respuesta es parte de la especificación de la ORB [CORBA01].
CONFIGURACIÓN DE LA ORB PARA EL ACCESO A SUS REFERENCIAS INICIALES. La ORB provee referencias a estos objetos que serán recolectadas durante el proceso de
inicialización. Los objetos pueden ser tanto referencias a objetos locales como remotos. En el caso del
servicio de persistencia, la referencia que se provee es una instancia de la clase
org.omg.CosPersistentState.ConnectorRegistry, que al implementar la interfase
org.omg.CORBA.LocalInterface, se trata de una referencia local. Las referencias remotas son objetos que
residen en otra ORB que en general está en otra máquina. Básicamente se resuelve pasándole la
representación en forma de cadena de caracteres (string) a la ORB.
Las referencias locales son construidas mediante el uso de los interceptors (interceptores). Un
interceptor es un mecanismo que permite que los servicios de CORBA y otros objetos, puedan intervenir
en el procesamiento de la ORB. Estos interceptores son registrados en la ORB durante el proceso de
inicialización de la misma. La interfase org.omg.PortableInterceptor.ORBInitializer permite definir un
interceptor que participará del proceso de inicialización de la ORB. Existen otros interceptores que
permiten intervenir en otros procesos de la ORB, como la invocación remota a un método.
En la especificación de la ORB para Java (CorbaJava00), este tipo de interceptores se registran
pasándole una propiedad como parámetro a la máquina virtual, con el siguiente formato:
org.omg.PortableInterceptor.ORBInitializerClass.XXX
, donde XXX será el nombre completo de la clase que implementa ORBInitializer. Las propiedades se
pasan a la máquina virtual de Java anteponiendo –D a la propiedad en la línea de comando. También se
admite definir estas propiedades como entradas en el archivo orb.properties.
El archivo orb.properties es la forma más simple de configurar una instalación Java que requiera
CORBA. La máquina virtual Java (JVM) contiene una implementación completa de la ORB. Mediante el
archivo orb.properties se puede configurar, tanto la implementación de ORB a utilizar, como por ejemplo
JacORB, como los servicios que estarán disponibles en la misma.
ORB orb = .....; Object objSrv = orb.resolve_initial_references(“NameService”); NamingContextExt context = NamingContextExtHelper.narrow(objSrv);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
76
void pre_init (org.omg.PortableInterceptor.ORBInitInfo info); void post_init (org.omg.PortableInterceptor.ORBInitInfo info);
class ORBInitialization
pss::ConnectorRegistryImpl
+ ConnectorRegistryImpl()+ find_connector(String) : Connector+ is_registered(String) : boolean+ post_init(ORBInitInfo) : void+ pre_init(ORBInitInfo) : void+ register_connector(Connector) : Connector+ unregister_connector(String) : void
LocalInterface
«interface»
pss::ConnectorRegistryInitializer
+ post_init(ConnectorRegistry, Properties) : void+ pre_init(ConnectorRegistry, Properties, String) : void
Object
«interface»
PortableInterceptor::ORBInitializer
_ConnectorLocalBase
connector::ConnectorBase
pss_pmi_registry.properties
ConnectorRegistryOperationsorg.omg.CORBA.LocalInterface
org.omg.CORBA.portable.IDLEntity
«interface»
CosPersistentState::ConnectorRegistry
org.omg.CORBA.LocalObject
CosPersistentState::_ConnectorRegistryLocalBase
+ _ids() : String[]
orb.properties
«realize»
configures
«flow»
«realize»
configures
«flow»
DISEÑO DEL SERVICIO 1 - CONFIGURACIÓN DE LA ORB
En el gráfico anterior se muestran las partes que interactúan para que el servicio de persistencia
de CORBA esté disponible en la ORB. El proceso que permite al servicio de persistencia estar disponible
en la ORB es el siguiente:
REGISTRO DE CONECTORES PARA EL SERVICIO DE PERSISTENCIA La ORB leerá de la línea de comandos o del archivo orb.properties la propiedad que identifica a
un inicializador:
, en este caso: ar.uba.fi.pmi.corba.pss.ConnectorRegistryImpl es la clase que representa al registro de
conectores del servicio de persistencia y es a su vez la puerta de entrada al servicio.
ConnectorRegistryImpl implementa la interfase ORBInitializer de CORBA. No es necesario que sea el
mismo registro el que implemente dicha interfase, podría ser cualquier objeto que sea capaz de generar
el registro, para luego registrarlo en la ORB, como una referencia inicial. Para simplificar opté por utilizar
el mismo registro como inicializador. ORBInitializer expone dos métodos:
, el primero identifica el comienzo de la etapa de inicialización de la ORB y el segundo su fin. El
parámetro que reciben ambos métodos es una instancia de la clase ORBInitInfo, la cuál expone un
org.omg.PortableInterceptor.ORBInitializerClass.ar.uba.fi.pmi.corba.pss.ConnectorRegistryImpl
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
77
método register_initial_reference(String, org.omg.CORBA.Object), que es el que permite indicarle a la
ORB que objeto representa la referencia inicial.
El registro de conectores permite acceder a los distintos conectores que estarán disponibles en
función de la configuración de la instalación. El proceso de registración de un conector es similar al que
permite a la ORB registrar las referencias iniciales en forma dinámica. Por ello, opté por definir un
mecanismo muy similar para configurar el registro.
El registro es configurado, o bien, por variables pasadas por la línea de comandos o por un
archivo de propiedades, que en este caso se llama: pss_pmi_registry.properties. El registro tomará las
propiedades que comiencen con el prefijo: PSSPMIConnector.:
, en este caso se identifica el conector Transient. Cada vez que se le pida al registro, un conector
identificado por la cadena de caracteres Transient, se hará referencia al conector provisto por la clase
ar.uba.fi.pmi.corba.pss.connector.TransientConnector. La única condición para estas clases es que
implementen la interfase:
ar.uba.fi.pmi.corba.pss.ConnectorRegistryInitializer:
, como se aprecia, al igual que ORBInitializer expone dos métodos que representan la fase de
inicialización, en este caso, del registro de conectores. De forma análoga, el objeto que hace las veces de
inicializador del registro, no tiene por que ser el mismo que representa al conector. Sin embargo, para
simplificar opté por utilizar el mismo. Se espera que en la implementación del método post_init, se
registre un conector, mientras que el método pre_init se utilice para recolectar y configurar toda la
información requerida por el conector a registrar. Existe una implementación parcial de
ConnectorRegistryInitializer que puede ser utilizada como clase base para la implementación de
cualquier conector.
De esta forma mi servicio de persistencia propone un mecanismo de configuración que permite a
cualquier implementación de conector, incorporarse al registro por medio de una configuración.
IMPLEMENTACIÓN DEL SERVICIO PROVISTO POR DIFERENTES CONECTORES
Toda implementación del servicio requiere de dos partes por lo menos. La primera es su
contribución al compilador, que permita generar código fuente para el servicio. La segunda es la parte
interna al servicio que será utilizada en tiempo de ejecución y que se presenta al usuario final como el
conector (org.omg.CosPersistentState.Connector). En esta sección me focalizaré en el conector
propiamente dicho.
EL CONECTOR DEL SERVICIO DE PERSISTENCIA
PSSPMIConnector.ar.uba.fi.pmi.corba.pss.connector.TransientConnector=Transient
public void pre_init(ConnectorRegistry registry, Properties initialzationProperties, String creationProperty); public void post_init(ConnectorRegistry registry, Properties initialzationProperties);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
78
class connector
«interface»
CosPersistentState::ConnectorOperations~ create_basic_session(access_mode, additional_parameters) : org.omg.CosPersistentState.Session~ get_pid(obj) : byte[]~ get_short_pid(obj) : byte[]~ implementation_id() : java.lang.String~ register_session_factory(session_factory) : Class~ register_storage_home_factory(storage_home_type_name, storage_home_factory) : Class~ register_storage_object_factory(storage_type_name, storage_object_factory) : Class
org.omg.CORBA.Local Interfaceorg.omg.CORBA.portable.IDLEnti ty
«interface»
CosPersistentState::Connector
_ConnectorLocalBase
ConnectorBase
+ create_basic_session(access_mode, additional_parameters) : Session+ get_pid(obj ) : byte[]+ get_short_pid(obj ) : byte[]+ getStorageHomeFactory(storage_home_id) : Class+ getStorageObjectFactory(storage_object_id) : Class+ implementation_id() : String+ onUnregistered(registry) : void+ post_init(registry, properties) : void+ pre_init(registry, properties, creationProperty) : void+ register_session_factory(session_factory) : Class+ register_storage_home_factory(storage_home_type_name, storage_home_factory) : Class+ register_storage_object_factory(storage_type_name, storage_object_factory) : Class+ toString() : String
PersistentConnector
+ getConnection(session, connectionParameters) : DataStoreConnection+ PersistentConnector()
TransientConnector
+ T ransientConnector()
Local Interface
«interface»
UnregistrableConnector+ onUnregistered(registry) : void
«realize»
DISEÑO DEL SERVICIO 2 - CONECTORES
El gráfico anterior muestra las principales clases que están relacionadas con el conector. Mi
modelo de registro dinámico de conectores extiende la interfase Connector de CORBA mediante la
interfase: ar.uba.fi.pmi.corba.pss.connector.UnregistrableConnector. La cual permite que los conectores
registrados tengan un callback (llamada) representado por el método onUnregistered, que les permite a
los conectores liberar los recursos que tengan utilizados, cuando la ORB donde se registró el conector,
finalice.
El hecho de que en Java las clases están representadas por objetos de tipo java.lang.Class
simplifica enormemente el desarrollo. En Java solo se necesita de un objeto de tipo Class para poder
construir un objeto de dicha clase, mientras que en C++, la única forma de construir una clase es
mediante una llamada explicita a un constructor de la misma mediante código escrito por el
programador. Por esto, las definiciones IDL nativas que hacen referencias a Factory se traducen a objetos
Class en Java, mientras que en C++, se traducen a una clase concreta para cada factory.
La simplicidad del modelo de Java, me permitió crear una implementación básica de la interfase
Connector, que es la clase ar.uba.fi.pmi.corba.pss.connector.ConnectorBase y que es funcionalmente
completa. Sin embargo, esta clase está declarada como abstracta, dado que para poder funcionar
correctamente se requiere que el conector conozca el tipo de sesiones que debe crear. Aunque no es
obligatorio heredar de ella para implementar un conector, si es conveniente.
En el diagrama se muestran dos implementaciones del conector TransientConnector y
PersistentConnector, las cuales explico en detalle más adelante. Básicamente se trata de las dos
implementaciones que provee mi servicio de persistencia. La primera con soporte de persistencia en
memoria y la segunda en un repositorio persistente ante el reinicio del servidor.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
79
Ambos conectores heredan de la implementación básica del conector, agregándole el registro
automático del tipo de sesión que utiliza cada una. Por registro automático se entiende que el usuario no
necesita registrar la clase que se utilizará para crear sesiones.
OPERACIONES DEL CONECTOR
El conector es la puerta de entrada para el servicio de persistencia. Sus operaciones están
definidas en la interfase ConnectorOperations. En el conector se deberán registrar los tipos o clases que
estarán disponibles para el servicio en tiempo de ejecución.
Estos métodos permiten registrar clases para diferentes propósitos en el conector. El primer
método registra la clase que lo representa para un nombre de objeto almacenado. Para estos nombres se
adopta una nomenclatura similar que los nombres de IDL, o sea, la cadena de caracteres comienza con
PSDL:, es seguida por el nombre completo de la clase y finalmente por la versión de la misma. Por
ejemplo, para registrar la clase ar.fi.uba.pmi.corba.pss.test.Persona se deberá utilizar un nombre:
PSDL:AR.FI.UBA.PMI.CORBA.PSS.TEST.PERSONA:1.0
, en este caso se registrará la versión 1.0 de la clase Persona.
Para simplificar la generación de estos nombres proveo la clase ar.uba.fi.pmi.corba.pss.psdl
.PSDLUtils:
class connector
psdl::PSDLUtils{leaf}
+ getReposi toryID(entityClass) : String+ getReposi toryID(entityClass, majorVersion, minorVersion) : String+ getReposi toryID(packageName, entityName, majorVersion, minorVersion) : String+ getReposi toryID(entityName, majorVersion, minorVersion) : String~ PSDLUtils()+ versionToString(majorVersion, minorVersion) : String
DISEÑO DEL SERVICIO 3 - PSDLUTILS
, que permite generar los nombres en base a la clase que se quiera registrar.
El segundo método register_ del conector es análogo al primero, permitiendo registrar la clase
que representa un almacén para un nombre dado.
Y finalmente, el tercero de ellos permite registrar la clase que representa una sesión en el
conector. En general, se puede suponer que el conector conoce el tipo de sesiones que puede manejar,
sin tener que indicárselo programáticamente. Sin embargo, no es cierto para las clases que representan
almacenes y objetos almacenados. En mi esquema, donde cada tipo de conector tiene su clase que lo
representa, este método no sería necesario. Probablemente sería necesario, si se tiene una única clase
Connector para todo conector. Al tratarse de un detalle de implementación del mismo, no debería ser un
Class register_storage_object_factory(java.lang.String storage_type_name, Class storage_object_factory);
Class register_storage_home_factory(java.lang.String storage_home_type_name, Class storage_home_factory);
Class register_session_factory(Class session_factory);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
80
método público del conector dado que el usuario final del servicio no tiene por que conocer cual es la
clase que representa la sesión.
El conector tiene, además, las siguientes cuatro operaciones:
El primer método (implementation_id) devuelve el identificador con el que se encuentra
registrado el conector en el registro de conectores.
El segundo y tercer método (get_pid, get_short_pid) permiten obtener a partir de un objeto
almacenado, el identificador que lo representa globalmente y localmente respectivamente, dentro del
almacén. Estos dos métodos se utilizan generalmente en el esquema de trabajo de persistencia
transparente, donde los objetos que se persisten no tienen ninguna relación con el servicio de
persistencia.
class Parameters
org.omg.CORBA.portable.IDLEntity
Parameter{leaf}
+ name: java.lang.String = ""+ val : org.omg.CORBA.Any
+ Parameter()+ Parameter(java.lang.String, org.omg.CORBA.Any)
connector::SessionParametersExtractor
+ getFileName() : String+ getHostName() : String+ getParams() : Map<String, Object>+ getPort() : Integer+ getUserName() : String+ getUserPassword() : String+ hasFi leName() : boolean+ hasHostName() : boolean+ hasPort() : boolean+ hasUserName() : boolean+ hasUserPassword() : boolean+ isEmpty() : boolean+ isRemote() : boolean+ SessionParametersExtractor(Parameter[])+ toMessage() : Message+ toString() : String
pss::SessionParameterBuilder
# appendParameter(String, Serializable) : SessionParameterBui lder+ build(ORB) : Parameter[]# createAny(ORB, DynAnyFactory, Map.Entry<String, Serializable>) : DynAny+ withFileName(String) : SessionParameterBui lder+ withHostName(String) : SessionParameterBuilder+ withPort(Integer) : SessionParameterBuilder+ withPropertiesFi le(String) : SessionParameterBui lder+ withUserName(String) : SessionParameterBuilder+ withUserPassword(String) : SessionParameterBuilder
DISEÑO DEL SERVICIO 4 - PARAMETERS
Y por último, el método más importante del conector (create_basic_session) es el que permite
generar una sesión de trabajo en el servicio de persistencia. Este método requiere de dos parámetros, el
primero es el tipo de sesión a crear, que puede ser READ_ONLY o READ_WRITE. La primera sólo permitirá
lecturas sobre los objetos, y la segunda lectura y escritura. El segundo parámetro es un array de objetos
Parameter.
La función de estos parámetros es permitirle al conector acceder a la información necesaria para
generar la sesión. Por ejemplo, si se trata de una sesión que se guarda en disco, un parámetro podría ser
el nombre del directorio y archivo donde se almacena la sesión. Si el conector se conecta a un repositorio
remoto, podría ser el nombre de usuario y password necesarios para conectarse.
java.lang.String implementation_id(); byte[] get_pid(Object obj); byte[] get_short_pid(Object obj); org.omg.CosPersistentState.Session create_basic_session(
short access_mode, org.omg.CosPersistentState.Parameter[] additional_parameters);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
81
La clase Parameter está definida por dos atributos, el primero es el nombre del parámetro, y el
segundo es su valor, que está representado por un objeto de tipo org.omg.CORBA.Any. La clase Any se
define en CORBA como un contenedor de cualquier tipo de dato u objeto. Esta clase provee operaciones
para transportar el objeto que contenga como valor [CORBA01]. Para trabajar con los objetos de tipo Any
se requiere utilizar algún servicio de CORBA, como por ejemplo, el servicio DynAnyFactory [CORBA01]
(que es una referencia inicial a la ORB). Para mantener encapsulada la creación y lectura de estos
parámetros proveo dos clases, la primera ar.uba.fi.pmi.corba.pss.SessionParameterBuilder, que
representa la realización del patrón de diseño builder de Gamma [Gamma01]. Esta clase captura los
parámetros necesarios para la creación de la sesión y finalmente con el método build construye un array
de objetos Parameter que podrá ser pasado como parámetro, al método create_session del conector.
La otra clase es ar.uba.fi.pmi.corba.pss.connector.SessionParametersExtractor, que permite
recuperar los valores de un array de objetos Parameter. Esta clase es usada internamente por el conector
para extraer los parámetros necesarios para la creación de la sesión.
Es el tipo de conector quien define cuales nombres de parámetros aceptará (el atributo name de
la clase Any). Este tipo de pasaje de parámetros no restringe cuales son los parámetros válidos y
requeridos por cada conector. Cada conector deberá validar en tiempo de ejecución, si los parámetros
recibidos son correctos o no, en caso de que no lo sean, lanzará una excepción de tipo
org.omg.CORBA.PERSIST_STORE.
Tanto la clase que construye la lista de parámetros (SessionParameterBuilder), como la que la
procesa (SessionParametersExtractor), restringen los nombres y tipos de parámetros a los dos
conectores que conocen. Otro conector deberá, o bien extender estas dos clases, o proveer otra forma de
generar los parámetros para sí mismo.
PROBLEMAS QUE PRESENTA LA CREACIÓN DE UN CONECTOR Antes de entrar en el detalle de cada uno de los conectores, voy a describir algunos de los
problemas generales que plantea la construcción de un conector.
CREACIÓN DE OBJETOS
El primer objeto que se necesita crear es la sesión de trabajo y el conector es el encargado de
crearla. En mi implementación base del conector, la sesión se crea utilizando la clase registrada para tal
efecto. Se requiere solamente que dicha clase tenga un constructor sin parámetros. La clase registrada
debe heredar de la clase ar.uba.fi.pmi.corba.pss.catalog.SessionBase, también provista por mí. Dicha
clase provee un método initialize, que es llamado inmediatamente luego de la creación del objeto por el
conector.
EL PATRÓN DE DISEÑO TEMPLATE METHOD
Todas las clases base que proveo exponen el comportamiento que deben proveer pero no lo
implementan completamente, es por ello que están definidas como abstractas. El diseño de estas clases
sigue el patrón de diseño Template Method [Gamma01]. Dicho patrón permite definir un esqueleto de
una clase dejando métodos que las subclases deberán implementar para completar el comportamiento.
Estos métodos son los llamados template. En el caso concreto de la clase SessionBase, el método
initialize está declarado como final. Internamente initialize llama a otro método protegido de la clase
denominado do_initialize (el template method). Las subclases deben sobrescribirlo, si lo requieren, para
poder realizar alguna inicialización adicional tal como una conexión a la base de datos. El principal
beneficio del uso de este patrón es que permite un alto grado de reutilización de código. En mi caso, me
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
82
permitió definir varias clases base que pueden ser utilizadas en distintas implementaciones de conectores
del servicio de persistencia.
En el diagrama que se muestra a continuación se observan todos los template methods que
provee la clase SessionBase. Son todos aquellos que comienzan con el nombre do_xxx.
class ConnectorBases
_SessionLocalBase
C:extends ConnectorBase
catalog::SessionBase
# log: Log = LogFactory.getL... {readOnly}
+ access_mode() : short# assertExist(Object, byte[]) : void+ canWrite() : boolean# check_can_write(String) : void# check_not_closed(String) : void# checkInitial ized() : void+ close() : void# do_close() : void# do_find_by_pid(byte[]) : Object# do_find_storage_home(String) : StorageHomeBase# do_flush() : void# do_free_all() : void# do_initial ize(C, Parameter[]) : SessionBase# do_refresh() : void+ find_by_pid(byte[]) : Object+ find_storage_home(String) : StorageHomeBase+ flush() : void+ free_al l() : void# getAccessMode() : AccessMode+ getConnector() : C+ initial ize(AccessMode, C, Parameter[]) : SessionBase+ isClosed() : boolean+ refresh() : void+ SessionBase()
_ConnectorLocalBase
connector::ConnectorBase
+ create_basic_session(short, Parameter[]) : Session+ get_pid(Object) : byte[]+ get_short_pid(Object) : byte[]+ getStorageHomeFactory(String) : Class+ getStorageObjectFactory(String) : Class+ implementation_id() : String+ onUnregistered(ConnectorRegistry) : void+ post_ini t(ConnectorRegistry, Properties) : void+ pre_ini t(ConnectorRegistry, Properties, String) : void+ register_session_factory(Class) : Class+ register_storage_home_factory(String, Class) : Class+ register_storage_object_factory(String, Class) : Class+ toString() : String
C:extends ConnectorBaseH:extends AbstractStorageHomeBase
catalog::CommonSessionBase
# homes: Map<String, H> = CollectionFacto...
# after_ini tial ize(H) : H# afterCreateSO(SO) : void# createStorageObjectIdenti fier(H, byte[], byte[]) : StorageObjectIdentifier# do_find_storage_home(String) : StorageHomeBase# getHomes() : Map<String, H># next_pid() : byte[]+ register(SO, StorageObjectSpec, byte[], H) : SO
DISEÑO DEL SERVICIO 5 - CONECTOR Y SESIONES
ALMACENES Y OBJETOS ALMACENADOS
Una vez creada la sesión se puede comenzar a trabajar con ella. Para poder trabajar con los
objetos, es necesario poder crearlos. Dentro del servicio de persistencia, los tipos de objetos que definen
los usuarios están divididos en dos categorías, los almacenes y los objetos almacenados. La creación de
estos dos tipos de objetos siempre está ligada a la sesión de trabajo. Una instancia de objeto pertenece
siempre a la sesión en la que fue creada.
En el modelo PSDL, los almacenes son siempre los encargados de la creación de los objetos
almacenados. Entonces el siguiente problema a resolver, es: ¿cómo obtener una referencia a una
instancia de un almacén dentro de una sesión?. La interfase org.omg.CosPersistentState.CatalogBase de
PSDL provee el método:
, que en base a un identificador de almacén (o nombre) permite obtener una referencia al almacén que lo
representa. Dos o más llamadas consecutivas con los mismos parámetros al método find_storage_home
deben devolver la misma instancia de almacén, dado que los almacenes son singletons [Gamma01]
dentro de una sesión. Esto requiere que la sesión además de crear los almacenes, lleve un registro de
aquellos almacenes que fueron creados. Esta funcionalidad está provista de forma simple por una clase
llamada ar.uba.fi.pmi.corba.pss.catalog.CommonSessionBase, que contiene un mapa interno de los
almacenes creados en ella. Este comportamiento no está implementado en SessionBase, dado que es un
StorageHomeBase find_storage_home(String storage_home_id) throws NotFound;
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
83
detalle de implementación de la sesión y es muy probable que algunos conectores requieran de
mecanismos más complejos para resolverlo.
Como el conector conoce la clase que le corresponde a un nombre, la clase SessionBase provee
una referencia al conector donde fue creada, permitiéndole a CommonSessionBase acceder a las clases
de almacenes para poder construirlos.
ALMACENES
En este punto cabe plantearse, ¿qué implica crear un almacén? Siendo que los almacenes son
objetos que están ligados lógicamente al repositorio donde fueron creados originalmente, ya que
almacenan objetos que pertenecen a un único repositorio. Los almacenes creados en el contexto de una
sesión, son en realidad vistas de los almacenes existentes en el repositorio. Si dos sesiones están
conectadas con el mismo repositorio deberán obtener cada una, una instancia distinta del mismo
almacén. Entonces los almacenes creados son vistas que están conectadas en tiempo de ejecución a la
sesión donde fueron creados y mediante ella al repositorio que conecta la sesión.
Existe otra característica de los almacenes que hacen complicada su creación. Los almacenes
tiene una estructura jerárquica, o sea, dentro de una familia de almacenes, el primer almacén (la raíz) en
la jerarquía es capaz de manejar cualquier objeto almacenado dentro de la misma jerarquía.
class storagehome hierarchy
S:extends SessionBaseDH:extends ExtendedStorageHome
storageHome::AbstractChildStorageHome
# AbstractChildStorageHome()# afterDestroy(StorageObject) : void# beforeDestroy(StorageObject) : void+ destroy(StorageObject) : void# destroyReference(StorageObject, String, byte[]) : void+ exist(StorageObject) : boolean+ find_by_short_pid(byte[]) : Object+ find_by_short_pid(ExtendedStorageHome, byte[]) : Object# find_ref_by_spec(StorageObjectSpec) : byte[]# getDelegatee() : DH+ home_fami ly_id() : String# initial izeChildHome(S, String, String, String) : AbstractChi ldStorageHome# initial izeMe(S, String, DH, String) : AbstractChi ldStorageHome# initial izeRootHome(S, String, String) : AbstractChildStorageHome# newSpec() : StorageObjectSpec
«interface»
storageHome::ExtendedStorageHome
+ belongs(Class<T>) : boolean+ destroy(StorageObject) : void+ exist(StorageObject) : boolean+ find_by_short_pid(ExtendedStorageHome, byte[]) : Object+ getStorageType() : Class<I>+ home_family_id() : String+ ini tial ize(Session, String) : StorageHomeBase+ registered_storage_home_id() : String
S:extends SessionBase
storageHome::AbstractRootStorageHome
_StorageHomeBaseLocalBase
S:extends SessionBase
storageHome::AbstractStorageHomeBase
StorageHomeBaseOperationsorg.omg.CORBA.Local Interface
org.omg.CORBA.portable.IDLEnti ty
«interface»
CosPersistentState::StorageHomeBase
«realize»
DISEÑO DEL SERVICIO 6 - MODELO BASE DE ALMACENES
Lo anteriormente dicho y el hecho de que los almacenes son clases totalmente definidas y
construidas por el compilador PSDL, me llevó a definir el siguiente modelo base para todo almacén que
pueda existir dentro de mi servicio de persistencia.
org.omg.CosPersistentState.StorageHomeBase es la interfase definida por CORBA como
almacén, la cual he extendido agregando operaciones adicionales mediante la interfase
ar.uba.fi.pmi.corba.pss.storageHome.ExtendedStorageHome. Estas operaciones me permitieron definir
el comportamiento requerido para cualquier almacén que exista dentro de mi servicio de persistencia. Se
trata de una interfase y no de una clase, ya que cualquier conector es libre de implementarla como
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
84
corresponda. De todas formas, yo he implementado algunas clases base de esta interfase para maximizar
la reutilización de código entre mis dos implementaciones de conector. La clase
AbstractStorageHomeBase, es una implementación base de dicha interfase. Además, proveo otras dos
clases, que resuelven el problema de la jerarquía de almacenes y que son:
ar.uba.fi.pmi.corba.pss.storageHome.AbstractChildStorageHome
ar.uba.fi.pmi.corba.pss.storageHome.AbstractRootStorageHome
MODELO DE DELEGACIÓN
Estas dos clases resuelven el problema de la herencia de almacenes mediante la delegación. La
jerarquía de los almacenes está parcialmente definida por el compilador. Si es el usuario quien define
algún almacén con operaciones, es él mismo quien es responsable de crear una clase que herede de la
clase provista por el compilador, quedando así fuera del control del compilador. Por otro lado, dentro de
una jerarquía pueden existir
múltiples nodos hijos, tal como
muestra el gráfico a la izquierda.
En mi modelo, todos los
nodos de la jerarquía están
conectados mediante el atributo
delegatee, de la clase
AbstractChildStorageHome.
Cualquier almacén definido por el
usuario hereda de esta clase en
algún punto. El método
getDelegatee de la misma clase
devuelve el almacén que estará
encargado de resolver las
operaciones que requieran del
conocimiento del almacén raíz.
Cual es objeto delegatee, será
definido en tiempo de ejecución
cuando se cree el almacén. El único requisito para dicho objeto es que implemente la interfase extendida
de almacén propuesta por mi servicio: ExtendedStorageHome.
Este modelo de delegación simplifica el proceso de compilación, ya que gran parte de las
operaciones que deben implementar los almacenes son delegadas y el compilador sólo debe preocuparse
de generar las llamadas al almacén donde se delega. La diferencia entre un almacén que sea raíz y otro
que no lo es, está dada por como es inicializado cuando es construido y por el almacén donde se delega.
En el caso que el almacén sea raíz de la jerarquía, el almacén donde se delega es una instancia de
AbstractRootStorageHome. Por consiguiente, lo único que debe considerar el compilador a la hora de
generar el mecanismo de inicialización de cada almacén, es el método de la superclase que debe llamar
para inicializar dicho almacén.
Este modelo contribuye a la simplicidad del compilador, ya que las clases que deben conocer los
conectores, están limitadas a las provistas por ellos mismos y no a las clases definidas por los usuarios
mediante el lenguaje PSDL. La delegación es una técnica de POO, distinta de la herencia, que permite
también un alto grado de reutilización [BM0].
OBJETOS ALMACENADOS
class Home Incomplete Hierarchy
UserDefinedClasses
CompilerDefinedClasses
PersonStorageHome
ClientStorageHome
ACMEPersonStorageHome
VendorStorageHome
XXXVendorStorageHome YYYClientStorageHome
DISEÑO DEL SERVICIO 7 - DELEGACIÓN
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
85
Los almacenes permiten acceder a los objetos almacenados, ya sea creándolos o recuperando
aquellos que fueron creados anteriormente. Dentro del servicio, los almacenes son los únicos objetos que
permiten crear objetos almacenados que puedan ser persistidos en un repositorio.
El mecanismo de delegación es necesario también para la creación de objetos, dado que el
compilador no sabe cuales clases serán las registradas por el usuario del servicio, como la que represente
al objeto almacenado para un determinado almacén. Se evita que el compilador genere código para
construir objetos, generando solamente los métodos que recolectarán los atributos necesarios para la
construcción del objeto almacenado.
Cuando se construye un objeto almacenado también existe el problema de su identidad. CORBA
provee dos formas de identificar un objeto almacenado. La primera es un identificador global dentro del
repositorio (Pid) y la segunda es un identificador dentro de la familia de almacenes a la que pertenece
(ShortPid). Los objetos almacenados pueden ser creados como entidades primarias, pero también existen
aquellos objetos almacenados que son parte de otros objetos almacenados que los contienen (Embedded
Storage Objects) como atributos (estado) de los mismos. En este último caso, según la especificación de
CORBA, estos objetos almacenados no tendrán identidad propia, es decir que no tienen Pid ni ShortPid.
Dichos objetos se denominan Embedded o encastrados. Estos objetos embedded serán objetos creados
manualmente por el usuario y no por medio de un almacén.
class Extended Storage Obj ect
«interface»
CosPersistentState::StorageObject
+ destroy_object() : void+ get_pid() : byte[]+ get_short_pid() : byte[]+ get_storage_home() : StorageHomeBase+ object_exists() : boolean
«interface»
storageObject::ExtendedStorageObject
+ get_identi fier() : StorageObjectIdentifier+ ini tia lize(StorageObjectIdenti fier, StorageObjectSpec) : void+ is_ini tia lized() : boolean
Serializable
«interface»
storageObject::StorageObjectIdentifier
+ attach(CatalogBase) : void+ embed(String, ExtendedStorageObject) : void+ equals(StorageObject, StorageObject) : boolean+ equals(byte[], byte[]) : boolean+ get_pid() : byte[]+ get_short_pid() : byte[]+ get_storage_home() : StorageHomeBase+ is_destroyed() : boolean+ is_embedded() : boolean+ refToStorageObject(byte[]) : StorageObject+ set_destroyed() : void+ storageObjectToRef(StorageObject) : byte[]
«interface»
storageObject::StorageObjectSpec
+ addState(String, O) : void+ count() : int+ forEach(EntryProcessor) : void+ get_boolean(String) : boolean+ get_byte(String) : byte+ get_float(String) : float+ get_int(String) : int+ get_long(String) : long+ get_reference(String) : byte[]+ get_short(String) : short+ get_storageObject(String) : S+ get_string(String) : String+ getSpeci ficationType() : Class<? extends StorageObject>+ getState(String) : O+ hasValue(String) : boolean+ isAssignable(StorageObject) : boolean+ set_boolean(String, boolean) : void+ set_byte(String, byte) : void+ set_float(String, float) : void+ set_int(String, int) : void+ set_long(String, long) : void+ set_reference(String, byte[]) : void+ set_short(String, short) : void+ set_storageObject(String, S) : void+ set_string(String, String) : void
storageObj ect::BaseStorageObj ectIdentifier
storageObj ect::EmbeddedStorageObj ectIdentifier
storageObj ect::StorageObj ectSpecImpl
«real ize»«realize»
-parent
«real ize»
DISEÑO DEL SERVICIO 8 - OBJETOS ALAMACENADOS
Entonces los objetos almacenados presentan dos problemas, el primero es la creación mediante
almacenes o como objetos embedded y el segundo es su identidad. Para tratar con estos problemas
propongo el esquema expuesto en el diagrama anterior.
En el centro del diagrama se muestra la interfase StorageObject provista por CORBA, la cual
debe ser implementada por todo objeto almacenado abstracto definido en PSDL. Yo he extendido dicha
interfase mediante: ar.uba.fi.pmi.corba.pss.storageObject.ExtendedStorageObject. Cualquier objeto
almacenado, ya sea embbeded o no, debe implementar StorageObject y en el caso de los embedded
prácticamente todos los métodos de la interfase carecen de sentido. Mi interfase extendida agrega tres
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
86
boolean is_initialized(); void initialize(StorageObjectIdentifier identifier, StorageObjectSpec specification); StorageObjectIdentifier get_identifier();
operaciones que permiten trabajar con objetos almacenados que puedan ser utilizados tanto como
embedded o no. La única restricción que agrega mi servicio de persistencia para poder persistir objetos
almacenados, es que implementen esta interfase extendida, quedando a criterio de cada conector del
servicio, como hacerlo. Las operaciones extendidas que agrega mi interfase son:
Estas operaciones presuponen que la creación de un objeto en el contexto del servicio de
persistencia tiene dos etapas. La primera es la construcción física del objeto y la segunda una fase de
inicialización del mismo. Cualquier objeto almacenado de mi servicio de persistencia, así sea embedded o
no, debe pasar por estas dos etapas. La diferencia radica en cómo y quién es el que inicializa el objeto.
El segundo método (initialize) tiene dos parámetros. El primero parámetro es un identificador de
objeto y el segundo, una especificación del objeto a inicializar. A continuación voy explicar estos dos
conceptos fundamentales de mi servicio.
STORAGEOBJECTSPEC
Esta interfase representa una especificación de un objeto almacenado y se utiliza tanto para la
creación como para la búsqueda de dichos objetos. La búsqueda será explicada más adelante. Es una
interfase de uso interno al servicio y los usuarios nunca deben lidiar con ella. Esta interfase provee la
información necesaria para construir o buscar un objeto almacenado, lo cuál incluye tanto al tipo de
objeto (la clase) como los valores de los atributos que lo definen (estados). La clase
AbstractStorageHomeBase provee un método newSpec, que permite crear un objeto que implementa
esta interfase. Este objeto puede ser
utilizado por los métodos internos
generados por el compilador. El
segmento de código a la izquierda
representa un factory method
[Gamma01] llamado createNamed de un
almacén generado por el compilador. Este
objeto almacenado tiene dos atributos
nameTwo y one que son pasados como parámetro al método. La primer línea crea una nueva
especificación que en las siguientes líneas es cargada con los parámetros de entrada del método.
Finalmente la creación del objeto es delegada en el método create que recibe tan solo la especificación
de que debe crear. Esta forma de comunicación simplifica y generaliza la lógica del compilador, ya que
toda la complejidad de la creación del objeto está resuelta en la superclase y no en la clase generada por
el compilador.
STORAGEOBJECTIDENTIFIER
La idea de un identificador de objetos es un concepto que agrega el servicio de persistencia de
CORBA. En el modelo de objetos de Java, los objetos no tienen identificadores. La identidad de los
objetos sólo está garantizada a nivel de instancia. Para el operador == de Java, un objeto es igual a otro, si
y solo si, se trata de la misma instancia. Aunque Java también provee un método equals (iguales en
inglés), que permite comparar dos objetos por algún criterio distinto de la instancia misma. De todas
formas para la clase Object de Java, el método equals, solo devuelve verdadero cuando se trata de la
misma instancia. Si se quiere utilizar otro criterio de comparación, las clases deben redefinir dicho
método.
public Two createNamed(String nameTwo, One one) { StorageObjectSpec spec = this.newSpec(); spec.set_string("nameTwo", nameTwo); spec.set_storageObject("one", one); return (Two)this.create(spec); }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
87
La interfase StorageObjectIdentifier está encargada de tratar con la identidad de los objetos,
tanto para los embedded como para los comunes, dentro del contexto de una sesión. Se trata igualmente
de una interfase interna al servicio y que los usuarios nunca utilizarán directamente.
Es recomendable que los compiladores para cada conector, generen un método equals en los
objetos raíz de la familia que delegue en su identificador la comparación con otro objeto.
En mi interfase extendida ExtendedStorageObject, se puede apreciar que el primer parámetro
del método initialize es una instancia de dicha interfase. Cada conector decide como implementar la
interfase. Por lo general, este objeto pasará a ser un atributo del objeto almacenado luego de la llamada
al método initialize, mientras que la especificación será descartada, ya que sólo se utiliza para inicializar
los valores de los atributos que son los estado del objeto almacenado que se está inicializado.
Algunas de las operaciones que expone esta interfase son:
Los primeros cinco métodos son importados de la interfase StorageObject. Esto permite que el
compilador delegue la implementación de la interfase StorageObject en el atributo que represente al
identificador del objeto almacenado. Los siguientes dos métodos, permiten tratar a un objeto
almacenado como embedded. El primero indica si el objeto identificado por este objeto es un objeto
almacenado embedded. El segundo le permite a un objeto almacenado inicializar a otro objeto
almacenado que representa un atributo embedded en él. Luego de llamar a este método, el objeto
storageObject, quedará marcado como embedded dentro del objeto identificado.
Los últimos tres métodos son útiles para tratar con copias/instancias de los objetos que fueron
persistidos en el repositorio. Los dos primeros métodos comparan objetos almacenados por sus
identificadores y el último permite conectar una instancia de objeto almacenada con el catalogo o sesión
donde existe o será utilizado. Esta última operación es necesaria, ya que cuando una instancia es
recuperada de un repositorio persitente es cuando se puede determinar a que catálogo pertenece. Esta
información solo es útil para una instancia de objeto activa dentro de un catálogo, por lo que no debe ser
persistida en el repositorio.
OPERACIONES DE BÚSQUEDA DE OBJETOS
CatalogBase:: StorageHomeBase find_storage_home(in string storage_home_id) raises (NotFound); StorageObjectBase find_by_pid(in Pid the_pid) raises (NotFound); StorageHomeBase:: StorageObjectBase find_by_short_pid(in ShortPid short_pid) raises (NotFound);
public byte[] get_pid(); public byte[] get_short_pid(); public StorageHomeBase get_storage_home(); public void set_destroyed(); public boolean is_destroyed(); public boolean is_embedded(); public void embed(String attribute_name, ExtendedStorageObject storageObject); public boolean equals(StorageObject source, StorageObject target); public boolean equals(byte[] sourceReference, byte[] targetReference); public void attach(CatalogBase catalog);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
88
El otro problema que tienen que resolver los conectores y también el servicio en sí mismo, es la
búsqueda de objetos. La búsqueda implica encontrar los objetos almacenados y los almacenes que ya
fueron creados previamente en el servicio. La especificación de CORBA define algunas operaciones de
búsqueda estándar, tales como las expuestas en el cuadro anterior. Estas operaciones son relativamente
simples de resolver. La búsqueda de almacenes se limita a buscar en un índice de almacenes creados
dentro de una sesión y si no existe, crearlo acorde al registro de almacenes. Esta funcionalidad está
implementada en la clase CommonSessionBase vista anteriormente. Las dos operaciones siguientes
dependen del conector.
Existe un tipo de operaciones de búsquedas más complejas. Son aquellas que se definen
automáticamente al definir claves dentro de un almacén. Cada definición de clave define implícitamente
dos operaciones de búsqueda por los atributos que conforman la clave. La primera busca el objeto
almacenado y la segunda una referencia al objeto que esté identificado por la clave.
Estas operaciones requieren que los conectores provean una forma eficiente de realizar
búsquedas genéricas por atributos de los objetos almacenados dentro de un almacén particular. Deben
ser genéricas, ya que los conectores no conocen las claves que pueden definir los almacenes en tiempo
de diseño. Más adelante explicaré como cada conector en particular, resuelve estas operaciones.
Estas operaciones de búsqueda son necesarias en el momento de creación de los objetos
almacenados para poder garantizar la unicidad de las claves.
VALIDACIÓN DE CLAVES
La especificación de CORBA define el concepto de clave, su significado y el código que se genera
en cada lenguaje. No especifica el comportamiento esperado. Por ejemplo, las claves definen la unicidad
de los objetos dentro de un almacén o familia de ellos, pero no se especifica que sucede cuando no se
cumple la restricción que impone alguna de las claves, ni tampoco, cuando deben verificarse estas
restricciones.
Dado que la especificación es particularmente amplia en este sentido, he optado por definir un
mecanismo de validación de las claves que puede ser disparado en diferentes momentos. Los conectores
pueden optar por redefinir o extender este comportamiento según sus necesidades, definiendo cuando
será utilizado. El modelo base obliga a que se verifiquen las claves cada vez que se construya un objeto,
generalmente, serán necesarias verificaciones adicionales.
Las cuatro clases representadas en el diagrama siguiente, modelan el comportamiento necesario
para la validación de las claves definidas por una familia de almacenes. StorageHomeKeySet es una clase
abstracta cuyo objeto es el de recolectar el conjunto de claves, cada una representada por la interfase
StorageHomeKey, definidas por una familia de almacenes. Para ello cuentan con el soporte de la clase
StorageHomeKeyBuilder (patrón de diseño Builder [Gamma01]), que permite construir las claves definida
en los almacenes.
La clase AbstractStorageHomeBase define el método:
, que se deberá ser sobrescrito en cada uno de los almacenes que defina al menos una clave.
protected void collectKeys(StorageHomeKeySet keySet)
S find_by_key_name(<parameter_list>) raises (CosPersistentState::NotFound); ref<S> find_ref_by_key_name(<parameter_list>);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
89
protected void collectKeys(StorageHomeKeySet keys) { super.collectKeys(keys); ///////// Key: name StorageHomeKeyBuilder keyname = keys.keyBuilder("name"); keyname.with("name"); keyname.build(); }
class storageHome
StorageObjectSpec
«interface»
StorageHomeKey
+ getName() : String
StorageHomeKeyBuilder
+ bui ld() : void+ with(state) : StorageHomeKeyBuilder
StorageObjectSpecImpl
StorageHomeKeyImpl
+ getName() : String+ StorageHomeKeyImpl(spec, name)+ StorageHomeKeyImpl(type, name)+ toString() : String
H:extends ExtendedStorageHome
StorageHomeKeySet
+ getOriginalSpec() : StorageObjectSpec+ keyBuilder(name) : StorageHomeKeyBuilder+ StorageHomeKeySet(spec)+ validate(operation, home) : void
«real ize»-set
DISEÑO DEL SERVICIO 9 - RESTRICCIONES DE CLAVES
El código que se muestra a
la izquierda representa
como un almacén agrega
una clave compuesta
únicamente por el atributo
‘name’. La implementación
del método collectKeys, es
generada por el compilador y el usuario nunca debe hacer uso de él directamente, por eso se trata de un
método declarado como protegido.
La clase StorageHomeKeySet no implementa el método valídate. Cada conector será
responsable de extender dicha clase proveyendo el mecanismo de validación para cada clave definida
dentro de la familia de almacenes.
En el caso concreto de la creación de un objeto, el método mencionado anteriormente, será
llamado por el mecanismo de creación de objetos almacenados dentro de cada almacén.
DESTRUCCIÓN DE OBJETOS ALMACENADOS
Otro punto donde la especificación es particularmente amplia, es que sucede en el momento de
destruir o eliminar objetos con los objetos que referencian al objeto que se está eliminando.
La especificación solamente indica que debe hacerse con las referencias de un objeto que es
eliminado. Las referencias a objetos de tipo embedded son directamente destruidas junto con su padre y
las referencias por identificadores son destruidas cuando están declaradas como strong o fuertes.
Siguiendo el modelo de delegación, he optado por centralizar las operaciones de destrucción de
los objetos en los almacenes, dado que es el almacén, en definitiva, quien tiene más información sobre el
estado de los objetos y es realmente la única clase que debe ser generada completamente por el
compilador. Por ello, el método destroy_object de la interfase StorageObject, es implementado por el
compilador mediante delegación en el almacén al que pertenece el objeto. La clase base
AbstractChildStorageHome define los siguientes métodos para la destrucción de los objetos
almacenados:
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
90
El método destroy está definido en mi interfase extendida de almacén. Se trata de un método
declarado final en esta clase, usando el patrón de diseño Template Method [Gamma01]. Los métodos
beforeDestroy y afterDesctroy son, en este caso, los template methods. Los almacenes que hereden de
esta clase, deberán redefinirlos para marcar como destruidas a aquellas referencias que estén declaradas
como fuertes en los objetos almacenados que manejen.
En el momento de la destrucción de los objetos, el identificador es marcado como destruido por
el almacén mediante la interfase StorageObjectIdentifier. Los identificadores de las referencias a objetos
de tipo embedded tienen su estado definido por medio del estado del identificador del objeto al que
pertenecen, por lo que estas referencias quedan automáticamente destruidas.
DIFERENTES CONECTORES Ya mencioné anteriormente que he construido dos conectores del servicio de persistencia. La
razón principal para ello es la forma que he desarrollado el servicio. El primer conector que provee
persistencia temporal, es la implementación más simple que cumple con todos los requerimientos del
servicio. Este conector se usó para construir el servicio y el compilador a la par.
CONECTOR DE PERSISTENCIA TEMPORAL
Este conector permite evaluar y comprender las funcionalidades de cualquier servicio de
persistencia. La limitación que tiene es que su estado es válido mientras la sesión esté activa. Sin
embargo, es funcional y puede ser usado siempre y cuando los requerimientos lo permitan.
class General Schema
ConnectorBase
connector::TransientConnector
+ TransientConnector()
CommonSessionBase
catalog::TransientSession
+ dettach(StorageObject) : void+ T ransientSession()
AbstractRootStorageHome
memory::TransientRootStorageHome
{leaf}
AbstractChildStorageHome
memory::TransientStorageHomeBase
+ do_after_create(SO, StorageObjectSpec, T ransientStoragetHome) : SO+ find_ref_by_spec(StorageObjectSpec, T ransientStoragetHome) : byte[]+ T ransientStorageHomeBase()
ExtendedStorageHome
«interface»
memory::TransientStoragetHome
+ do_after_create(SO, StorageObjectSpec, TransientStoragetHome) : SO+ find_ref_by_spec(StorageObjectSpec, TransientStoragetHome) : byte[]
StorageHomeKeySet
memory::TransientStorageHomeKeySet
+ val idate(String, T ransientStorageHomeBase) : void
ExtendedStorageObject
«interface»
memory::TransientStorageObject
+ matches(StorageObjectSpec, int) : boolean
AbstractTargetCompiler
memory::TransientTargetCompiler
«realize»«realize»
DISEÑO DEL SERVICIO 10 - ESQUEMA GENERAL DEL CONECTOR TEMPORAL
El diagrama anterior muestra el esquema general de las clases que componen al conector. Todo
conector debe proveer una contribución al compilador para que este pueda generar código para él. La
public final void destroy(StorageObject storageObject) protected void beforeDestroy(StorageObject storageObject) protected void afterDestroy(StorageObject storageObject)
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
91
contribución consiste en la clase TransientTargetCompiler, junto con las plantillas de código que utilizará
el compilador para generar el código fuente.
El compilador requiere, además, que la contribución sea registrada en su archivo de configuración
(pss_pmi_compiler.properties):
El conector debe tener su registro en el servicio de persitencia, con una clase que implemente la
interfase ConnectorRegistryInitializer. La interfase está implementada por el conector mismo, y es:
ar.uba.fi.pmi.corba.pss.connector.TransientConnector
El conector se registra en el archivo pss_pmi_registry.properties, mediante la siguiente línea:
Habiendo agregado esta última línea, el conector ya está listo para ser utilizado en tiempo de
ejecución.
FUNCIONAMIENTO
La funcionalidad del conector está provista mayormente por la sesión de trabajo que provee. La
clase que representa una sesión en el conector temporal es TransientSession. Esta clase hereda de la
implementación base de la sesión mencionada anteriormente: CommonSessionBase, que resuelve la
administración de almacenes de una manera simple, por lo que lo único que le queda resolver es la
construcción y manejo de objetos almacenados.
MANEJO DE IDENTIFICADORES
El primer problema que se necesita resolver, para la creación de objetos almacenados, es la
asignación de identificadores de objeto, tanto para el Pid como para el ShortPid. La especificación indica
que para Java los identificadores se traducen en un byte[], lo cual hace complicada su manipulación
directa. Para la generación de identificadores que están representados por un byte[], proveo la interfase
ByteArraySequencer, que permite obtener secuencias de identificadores en dicho formato.
class utils
BinaryUtils
+ bytesToLong(bytes) : long+ longToBytes(num) : byte[]
«interface»
ByteArraySequencer
~ next() : byte[]
SimpleByteArraySequencer
+ next() : byte[]+ SimpleByteArraySequencer()+ SimpleByteArraySequencer(initialValue)
«realize»
«use»
DISEÑO DEL SERVICIO 11 - GENERACIÓN DE IDENTIFICADORES
La implementación más simple de este secuenciador es SimpleByteArraySequencer, que se
limita a incrementar un número de tipo long, y traducirlo a su representación binaria mediante la clase
PSSPMIConnector.ar.uba.fi.pmi.corba.pss.connector.TransientConnector=Transient
registered=transient transient=ar.uba.fi.pmi.corba.pss.psdl.compiler.memory.TransientTargetCompiler transient.templateProperties=psdl_transient_templates.properties
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
92
BinaryUtils. La sesión del conector tiene una instancia de esta clase para generar los identificadores
globales (Pid) de los objetos almacenados y a su vez cada familia de almacenes que se use en la sesión,
tendrá otra instancia de este tipo de secuenciador.
ALMACENES TEMPORALES
El conector temporal extiende la interfase de almacén de CORBA mediante
TransientStoragetHome. Esto implica que todo almacén generado por este conector deberá implementar
dicha interfase. Existen dos clases base la implementan que son: TransientRootStorageHome y
TransientStorageHomeBase. La primera representa la raíz de una familia de almacenes y la segunda la
clase base para todo almacén generado por el compilador de este conector. El modelo de almacenes
temporales sigue el modelo de delegación propuesto inicialmente por mi servicio.
Cada almacén raíz de una familia de almacenes tiene un atributo SimpleByteArraySequencer,
que es utilizado para generar los identificadores objeto (ShortPid).
Para mantener los objetos almacenados dentro de la sesión cada almacén raíz tiene un mapa
donde almacena los objetos que fueron creados durante la sesión. Dicho mapa está indexado por el
identificador ShortPid.
OPERACIONES DE BÚSQUEDA
La clase AbstractChildStorageHome define el método:
, que permite buscar una referencia a un objeto almacenado que cumpla con la especificación dada. Los
conectores deben implementar dicha operación. En el caso de este conector, la implementación más
simple es iterar sobre todos los objetos almacenados dentro de la familia del almacén, representados por
los valores del mapa de la raiz y verificando si alguno de ellos cumple con la especificación dada. Para ello,
la interfase TransientStorageObject, define el método matches, que permite determinar si el objeto
almacenado cumple con la especificación dada.
Cualquier operación de búsqueda en este conector se traduce en construir una especificación
con los atributos que se estén buscando, para luego llamar al método find_ref_by_spec del almacén
donde se realizará el proceso anteriormente descrito.
La clase TransientStorageHomeKeySet es la encargada de realizar las validaciones de claves. Esta
clase utiliza el mismo mecanismo de búsqueda para buscar referencias a objetos que cumplan con la
definición de la clave.
CONECTOR DE PERSISTENCIA DURABLE
Si bien el conector temporal es funcional, muchas veces carece de utilidad práctica. En general,
lo que se busca con el concepto de persistencia es que sea prolongada en el tiempo e independiente de la
vida del proceso donde se creó al objeto. Por esto, mi servicio de persistencia provee otro conector que
cumple con este objetivo de persistencia durable.
Persistencia durable implica que la información debe ser almacenada en un repositorio
permanente y no volátil.
REQUERIMIENTOS PARA UN CONECTOR DE PERSISTENCIA DURABLE
El primer requerimiento es que permita almacenar objetos. Las operaciones deberán ser
perdurables, si se crea un objeto en una sesión, al cerrarla y abrir una nueva, se espera que el objeto siga
protected abstract byte[] find_ref_by_spec(StorageObjectSpec spec);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
93
existiendo en la nueva sesión. Todas las operaciones de consulta definidas por el servicio de persistencia
deben también estar soportadas. Y las claves definidas en el modelo deberán ser igualmente respetadas.
El repositorio deberá permitir más de una conexión o sesión concurrente.
Es importante recalcar que el servicio de persistencia de CORBA no tiene el objeto de reemplazar
una base de datos, por lo general los repositorios no contendrán una gran cantidad de objetos.
El acceso remoto al repositorio es un adicional, ya que el servicio de persistencia no lo impone
como requerimiento, aunque si constituye una cualidad muy útil.
Antes de entrar en los detalles de mi conector persistente, voy a realizar un análisis de las
distintas alternativas que consideré para lograr persistencia durable.
DISTINTAS ALTERNATIVAS PARA OBTENER PERSISTENCIA DURABLE
PERSISTENCIA MEDIANTE ARCHIVOS
La alternativa más primitiva es uno o más archivos almacenados en el sistema de archivos del
sistema operativo. Esta forma de lograr persistencia tiene como ventaja que no requiere nada más que el
conector para funcionar, dado que Java provee los mecanismos necesarios para utilizar archivos. Las
principales desventajas son: que requiere la construcción de mecanismos de indexación para acceder de
manera eficiente a el o los archivos, se debe hacer una traducción entre los objetos almacenados y el
formato de registro que se utilice en el archivo, se debe construir un mecanismo que permita el acceso
concurrente del archivo si es que se permite abrir más de una sesión por vez, y el acceso remoto a los
archivos requiere de algún soporte externo al servicio tal como el protocolo NFS (Network File System o
Sistema de Archivos en Red).
Java provee una forma estándar para almacenar objetos en archivos, denominada Serialization.
Este mecanismo permite almacenar en un archivo uno o más objetos junto con todas sus referencias.
Podría ser una alternativa válida, siempre y cuando se pueda garantizar que exista memoria suficiente
para mantener el respositorio de objetos, y no se requiera acceso concurrente al repositorio (en este
caso, el archivo serializado). Esta forma de almacenamiento podría pensarse como una extensión del
conector de memoria, donde la sesión de trabajo se almacena en un archivo, al momento de cerrase la
sesión.
Este modelo no requiere de mucho trabajo extra, ya que el único requerimiento que Java
impone para que un objeto puede ser serializado, es que el mismo implemente la interfase:
java.io.Serializable. Dicha interfase sigue el patrón de diseño Marker Interface [GrandM01] o interfase
marcadora. En este patrón, la interfase marcadora, que no declara métodos ni atributos, determina que
la clase que la implementa, cumple con una condición lógica (implementarla representa verdadero). En el
caso de Serialization, una clase que implemente Serializable, indica que el mecanismo de Serialization
debe considerar los objetos de dicha clase durante el proceso de serialización.
En conclusión, si sólo se necesita la funcionalidad del conector de memoria con persistencia
durable, Serialization es una buena alternativa.
PERSISTENCIA MEDIANTE UNA BASE DE DATOS RELACIONAL
Una forma de hacer un repositorio accesible en forma remota y concurrente, es mediante una
base de datos. Las bases de datos más difundidas son las relacionales.
PSS está basado en SQL3, que es una extensión al modelo relacional soportado por el SQL
tradicional. El problema que surge al utilizar una base de datos relacional, es que lo que se quiere
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
94
almacenar en este caso, no son datos relacionales, sino un modelo de objetos. Se requiere una
transformación bidireccional entre los objetos y el modelo relacional de tablas y registros.
Las bases de datos relacionales utilizan el lenguaje de consulta SQL. Las consultas deben
realizarse sobre un modelo que las soporte. Dicho modelo deberá ser generado por el compilador y más
específicamente, por la contribución del conector persistente relacional al compilador. Si bien el lenguaje
de consultas es generalmente estándar, para todas las bases de datos, el lenguaje de definición del
modelo (DDL o Data Definition Language) no lo es. Esto implica que no sólo se deberá construir un
conector para bases de datos relacionales, sino que el conector deberá considerar cual es la base de
datos que se quiere utilizar.
Dado que el compilador será quien defina el modelo relacional a utilizar, se requiere entonces
que dicho modelo sea creado en la base de datos propiamente dicha. Hasta que no esté creado el
modelo, no se podrá utilizar el servicio de persistencia con el conector. Se agrega entonces un paso
posterior a la compilación, para poder utilizar el servicio de persistencia.
Existen herramientas o bibliotecas que simplifican el uso de bases de datos relacionales para la
persistencia de objetos. Tal es el caso de: Hibernate [KingG01], Castor[Castor01], Toplink[Toplink01],
JDO [RoodsR01], entre otras. Son denominadas genéricamente ORM (Object-relation Mapping) o mapeo
de objeto-relación.
En estos modelos, por lo general, una instancia de una clase particular representa un registro en
una o más tablas, dependiendo de la jerarquía de la clase. Cada atributo persistente de la clase está
representado por una o más columnas en una tabla. Todas ofrecen un mecanismo de consulta de alto
nivel, que termina generando consultas SQL con las que se interactúa con la base de datos. De esta
forma, el usuario no está obligado a tratar en forma directa con SQL. Algunas de estas herramientas
resuelven las diferencias de sintaxis particular de DDL para cada base de datos mediante mecanismos de
extensión por el usuario y otras sólo funcionan con algunas bases de datos fijas. La relación entre el
modelo de objetos y el modelo relacional, el mapeo de objetos, está definida, por lo general, en una
configuración en forma de archivo XML, aunque algunas de ellas soportan este mapeo a través de
anotaciones en las clases Java [Annotations01].
Algunas herramientas como Hibernate o Toplink, soportan persistencia transparente, de forma
tal que no imponen restricciones o requerimientos sobre las clases que se pueden persistir. A este tipo de
persistencia se lo llama genéricamente POJO (Plain Old Java Objects o “Viejos y simples objetos Java”)
dado que cualquier objeto común Java puede ser persistido. JDO, en cambio, requiere de un pos-
compilador, que modifica las clases generadas por el compilador para que puedan ser persistidas.
O bien mediante el uso de alguna herramienta como las mencionadas, o mediante la
construcción de otra herramienta, un conector para una base de datos relacional es perfectamente
viable.
El principal problema del uso de una base de datos relacional, es tener que coexistir con dos
modelos distintos, pero que representan lo mismo [NewardT01]. Es necesario crear otro modelo
(relacional) análogo al modelo de objetos. Si se cambia el modelo de objetos, se debe cambiar el modelo
relacional, cada vez que se guarda un objeto se debe traducir al modelo relacional y cuando se lee del
modelo relacional se debe volver a transformar en objetos.
Otro punto a considerar es que el modelo en la base de datos relacional es visible al usuario, esto
expone los detalles de la implementación del conector. Lo que también podría fomentar que el usuario
interactué con el modelo relacional directamente, creando así una dependencia con la implementación
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
95
del conector. Esta dependecia de la implementación viola las políticas de CORBA y de la especifcación del
servicio de estado persistence.
PERSISTENCIA MEDIANTE UNA BASE DE DATOS ORIENTADA A OBJETOS
Si lo que se quiere guardar son objetos: ¿por qué no guardar objetos directamente? ¿Por qué es
necesario transformar los objetos a otro modelo? ¿Por qué no simplemente utilizar una base de datos
orientada a objetos?
Cuando se comparan las bases de datos orientadas a objetos con las relacionales, comúnmente
se realiza la siguiente analogía:
“Si la base de datos fuera un garaje, y los datos fueran un auto, en una base de datos orientada a
objetos simplemente se estaciona el auto, mientras que en una base de datos relacional cada vez que se
quiere estacionar el auto, se requiere desarmarlo es sus piezas elementales”. [CKA01]
En el contexto del servicio de persistencia, son mínimos los requerimientos que se tiene sobre
una base de datos orientada a objetos. Básicamente, que permita almacenar objetos y para luego poder
buscarlos. Los requerimientos particulares del modelo de objetos que propone el servicio, tales como los
almacenes y objetos almacenados, junto con sus identificadores en forma de array de bytes, son
agregados al modelo de objetos de Java y no tienen porque estar soportados por una base de datos
orientada a objetos.
En conclusión se requiere: una base de datos que soporte una cantidad de objetos no tan
grande, consultas no tan avanzadas, junto con acceso remoto y que sea de uso libre. Esta base de datos
debe soportar el modelo de objetos Java, correr en diversos entornos (tal como lo hace Java),
preferiblemente estar desarrollada en Java, para no estar ligado a una plataforma específica.
No son muchas las bases de datos orientadas a objetos que cumplen con estos requerimientos.
La única que cumple con todos ellos es: DB4O [DB4O01]. Existe otra base que cumple parcialmente con
estos requerimientos llamada Caché [Cache01], pero no posee una licencia de código abierto y solamente
está disponible para algunas plataformas como Windows y Linux.
Considerando las dos alternativas de base de datos, he optado por utilizar DB4O.
DB4O
DB4O o Data Base For Objects, sus siglas en inglés, que significan Base de datos para objetos. Se
trata de una base de datos orientada a objetos nativa, dado que no está implementada sobre una base de
datos relacional. Está desarrollada con licencia de código abierto de tipo GPL (GNU General Public
License).
Existen dos versiones, una para Java, y otra para .NET. Ambas permiten ser incrustadas en el
sistema que las use. Esto hace que no imponga ningún requerimiento adicional a mi servicio de
persistencia, más allá de la propia biblioteca que será incrustada en el servicio. Soporta acceso remoto,
transacciones y consultas avanzadas.
ACCESO A LA BASE DE DATOS
El acceso a la base de datos se realiza mediante la clase com.db4o.Db4o. Esta clase hace las
veces de fábrica o factory [Gamma01], de conexiones a la base de datos. Todas las operaciones son
realizadas mediante métodos estáticos en dicha clase.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
96
class db4o
Db4o
+ configure() : Configuration+ licensedTo(String) : void+ main(String[]) : void+ openClient(String, int, String, String) : ObjectContainer+ openFile(String) : ObjectContainer+ openServer(String, int) : ObjectServer+ version() : String
«interface»
ObjectServer
+ close() : boolean+ ext() : ExtObjectServer+ grantAccess(String, String) : void+ openClient() : ObjectContainer
«interface»
ObjectContainer
+ activate(Object, int) : void+ close() : boolean+ commit() : void+ deactivate(Object, int) : void+ delete(Object) : void+ ext() : ExtObjectContainer+ get(Object) : ObjectSet+ query() : Query+ query(Class) : ObjectSet+ query(Predicate) : ObjectSet+ query(Predicate, QueryComparator) : ObjectSet+ rollback() : void+ set(Object) : void
opens
opens
starts
DISEÑO DEL SERVICIO 12 - MODELO DE DB4O
El método openFile abre una conexión directa a un archivo que representa la base de datos. Esta
forma de conexión no admite interacción remota con la base datos. Para permitir que clientes remotos
accedan a la base de datos, se debe iniciar un proceso servidor, representado por la interfase:
com.db4o.ObjectServer.
El método openServer de la clase Db4o crea una instancia de servidor, que escuchará en el
puerto indicado, permitiendo conexiones a la base de datos que represente el archivo indicado, tanto
locales como remotas. Para obtener acceso local a la base de datos, la interfase ObjectServer, provee el
método openClient.
La única diferencia entre el acceso local y remoto, es cómo se establece la conexión. Una vez
establecida la conexión, no existe diferencia alguna para el usuario. La conexión está representada por la
interfase: com.db4o.ObjectContainer. Esta interfase provee métodos para almacenar, buscar y eliminar
objetos.
GUARDANDO OBJETOS
Para indicarle a la base de datos que un objeto debe ser almacenado, ObjectContainer provee el
método set, que recibe como parámetro el objeto a almacenar. Cada vez que se llame a este método con
un nuevo objeto, o sea que no fue obtenido de la base de datos mediante una consulta, se estará
almacenando un nuevo objeto.
Durante la vida de la conexión, la identidad del objeto está garantizada únicamente por la
instancia del objeto. Si se llama a set, con el mismo objeto dos veces, la segunda vez, se le indicará a la
base de datos que actualice el estado del objeto previamente almacenado.
RECUPERANDO OBJETOS
Se proveen diferentes mecanismos para recuperar objetos de la base de datos en función de la
complejidad de la consulta.
La forma más simple, se la denomina “Consulta por Ejemplo” o QBE (Query By Example). La idea
es crear un objeto “prototipo”, con los atributos de aquellos objetos que se quieren recuperar, llamando
al método get de ObjectContainer. Dicha operación devolverá una instancia de la interfase ResultSet, que
es un conjunto de resultados. Para todo objeto dentro del conjunto resultado, se cumple que los
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
97
atributos del objeto prototipo son iguales a los del objeto del conjunto. La condición evalúa aquellos
atributos del objeto prototipo que tienen un valor distinto del valor por defecto del tipo de atributo. Se
entiende por valor por defecto, al valor que asigna la máquina virtual de Java al inicializar un atributo de
dicho tipo. Por ejemplo, para un atributo de tipo int, el valor por defecto es 0, mientras que para un
atributo de tipo java.lang.Integer, el valor es null. Este tipo de consultas sólo permiten concatenar
condiciones por positivo, donde todos los atributos del objeto prototipo deben coincidir con los
resultados.
Para realizar consultas mas avanzadas, se provee otro mecanismo llamado Consultas Nativas o
NQ (Native Query). Se las denomina nativas, porque permite realizar consultas a la base de datos
mediante condiciones escritas en el lenguaje de programación, en este caso Java [CookWR01]. El
principal beneficio de este tipo de consultas, es que están escritas en el mismo lenguaje. Lo que implica
que son compiladas, haciendo que los cambios en el modelo produzcan un error de compilación cuando
la consulta no los refleja. A su vez, evitan introducir un nuevo lenguaje para expresar la consulta, tal como
en el caso de realizarse consultas SQL sobre una base de datos relacional, para obtener resultados que
luego serán traducidos a objetos. Las consultas nativas están representadas por la clase
com.db4o.query.Predicate (predicado).
Para realizar una consulta, tan sólo se requiere sobrescribir el método match de la clase
Predicate, que recibe como parámetro un objeto, el cual será comparado mediante las condiciones que
determine el predicado. En el ejemplo siguiente, se muestra un predicado en el que la condición evalúa
que las personas tengan un nombre igual a ‘Pablo’ o ‘PABLO’.
Esta forma de realizar consultas es muy atractiva en aquellos casos en los que la consulta esté
completamente definida, pero ¿qué pasa en aquellos casos en los que la consulta se define en tiempo de
ejecución, en función de diferentes parámetros?, donde se deben realizar consultas dinámicas. En estos
casos, se provee una interfaz más avanzada para la construcción de consultas dinámicas, basada en otro
proyecto de código abierto llamado S.O.D.A (Simple Object Database Access o acceso simple de objetos
en base de datos) [SODA01].
SODA trata de proveer una interfase orientada a objetos, que permite construir consultas
dinámicas. Tratando así de minimizar el uso de cadenas de caracteres para realizar consultas, y maximizar
las ventajas que el paradigma de objetos provee, como reutilización y encapsulamiento.
Las consultas están representadas por la interfase com.db4o.query.Query y son creadas por el
ObjectContainer. El método descend de Query, permite navegar los atributos de los objetos, de forma tal
de estableces restricciones (Constraint) sobre ellos. La interfase Constraint representa una restricción
que se aplica sobre un atributo de los objetos. El método constraint de Query recibe el valor del atributo
y alguna de las operaciones de Constraint determina el tipo de restricción que se aplica sobre el par
atributo-valor. Por ejemplo, el método greater (mayor) representa que el atributo navegado deberá ser
mayor que el valor utilizado como parámetro en la llamada a constraint (la cual creó originalmente el
objeto Constraint).
Predicate personasConNombrePablo = new Predicate<Persona>() { public boolean match(Persona persona) { return persona.getName().equals("Pablo") || persona.getName().equals("PABLO"); } };
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
98
class db4o query
«interface»
Query
+ constrain(constraint :Object) : Constraint+ constraints() : Constraints+ descend(fieldName :String) : Query+ execute() : ObjectSet+ orderAscending() : Query+ orderDescending() : Query+ sortBy(comparator :QueryComparator) : Query
«interface»
Constraints
+ toArray() : Constraint[]
«interface»
Constraint
+ and(with :Constraint) : Constraint+ contains() : Constraint+ endsWith(caseSensitive :boolean) : Constraint+ equal() : Constraint+ getObject() : Object+ greater() : Constraint+ identity() : Constraint+ like() : Constraint+ not() : Constraint+ or(with :Constraint) : Constraint+ smaller() : Constraint+ startsWith(caseSensitive :boolean) : Constraint
Serializable
«interface»
QueryComparator
+ compare(fi rst :Object, second :Object) : int
createssorts by
DISEÑO DEL SERVICIO 13 - MODELO DE CONSULTAS S.O.D.A
La interfase Constraint también permite construir condiciones más complejas de búsqueda, del
tipo (condicion1 o condicion2) y condicion3, mediante el uso de los métodos or y and.
El ejemplo de la consulta anterior, donde se buscaba personas con nombre Pablo o PABLO, se
escribiría:
ELIMINANDO OBJETOS
Cualquier objeto que esté almacenado en la base de datos puede ser eliminado mediante el
método delete de la interfase ObjectContainer. Eliminar un objeto no implica que sus referencias serán
eliminadas. Cuando un objeto tiene como atributos otros objetos (que no sean de tipo nativo o
java.lang.String), el manejo del ciclo de vida de las referencias debe ser indicado por el usuario. Esto
obedece a motivos de performance y de semántica de las referencias (¿cuándo un objeto se considera
parte de otro y cuándo es sólo una referencia?).
MANEJO DE REFERENCIAS
Las operaciones de borrado como las de actualización, no se propagan en forma automática a los
objetos referenciados (salvo por los objetos que sean de tipos nativos del lenguaje). Aunque en el caso de
la inserción la propagación se hace en forma automática. Si se tiene un objeto que tiene como atributo a
otro objeto, cuando se modifica un atributo del objeto referenciado y se llama al método set para el
objeto padre, la base de datos no registrará los cambios en el objeto hijo en forma automática, lo mismo
ocurre con el método delete. Para que así se haga, se le debe indicar explícitamente en la configuración
de la base, cuales operaciones deben propagarse para cada clase en particular.
Query cPersonas = objectContainer.query(); cPersonas.constrain(Persona.class); cPersonas.descend("nombre").constrain("Pablo").or( cPersonas.descend("nombre").constrain("Pablo") ); ObjectSet personasConNombrePablo = cPersonas.execute();
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
99
La configuración de la base se controla por medio del interfase: com.db4o.comfig.Configuration,
que se obtiene llamando al método configure, de Db4o.
class db4o Configuration
«interface»
ObjectClass
+ cal lConstructor(boolean) : void+ cascadeOnActivate(boolean) : void+ cascadeOnDelete(boolean) : void+ cascadeOnUpdate(boolean) : void+ compare(ObjectAttribute) : void+ enableRepl ication(boolean) : void+ generateUUIDs(boolean) : void+ generateVersionNumbers(boolean) : void+ maximumActivationDepth(int) : void+ minimumActivationDepth(int) : void+ objectField(String) : ObjectField+ persistStaticFieldValues() : void+ readAs(Object) : void+ rename(String) : void+ storeTransientFields(boolean) : void+ translate(ObjectTranslator) : void+ updateDepth(int) : void
«interface»
ObjectField
+ cascadeOnActivate(boolean) : void+ cascadeOnDelete(boolean) : void+ cascadeOnUpdate(boolean) : void+ indexed(boolean) : void+ queryEvaluation(boolean) : void+ rename(String) : void
«interface»
Configuration
+ activationDepth(int) : void+ objectClass(Object) : ObjectClass+ setOut(PrintStream) : void+ singleThreadedClient(boolean) : void+ testConstructors(boolean) : void+ timeoutCl ientSocket(int) : void+ timeoutPingCl ients(int) : void+ timeoutServerSocket(int) : void+ unicode(boolean) : void+ updateDepth(int) : void+ weakReferenceCol lectionInterval(int) : void+ weakReferences(boolean) : void
access access
DISEÑO DEL SERVICIO 14 - CONFIGURACIÓN DE DB4O
La configuración se puede realizar a nivel global de la base de datos desde la interfase
Configuration, a nivel de clase desde la interfase ObjectClass, y a nivel de atributo por medio de la
interfase ObjectField. Los métodos cascadeOnXXX, indican que la operación XXX será propagada a las
referencias. El método updateDepth de ObjectClass permite indicar hasta que nivel las referencias serán
actualizadas cuando se actualice un objeto. Como mencioné anteriormente, por defecto se actualizan las
referencias nativas, lo que representa el nivel 1.
CONECTOR PERSISTENTE CON SOPORTE DE DB4O
Para el conector con soporte persistente, el repositorio está representado por una base de datos
creada con DB4O.
REGISTRO DEL CONECTOR
La primer parte del conector está dada por su contribución al compilador. Para ello, agrega a la
configuración del servicio, las líneas que indican cual es la clase que implementa el TargetCompiler del
conector, que en este caso es PersistentTargetCompiler, junto con el archivo de configuración de las
plantillas de código.
El servicio de persistencia necesita además, de una configuración para soportar este conector.
Por esto, este conector agrega al archivo de configuración del servicio, la línea que indica la clase que será
responsable de procesar el registro del conector, que en este caso es: PersistentConnector.
FUNCIONAMIENTO
Las sesiones que crea el conector estarán representadas por la clase PersistenSession. Una
sesión se conecta a un DataStore por medio de la interfase DataStoreConnection. Existen dos clases
registered=persistent persistent=ar.uba.fi.pmi.corba.pss.psdl.compiler.persistent.PersistentTargetCompiler persistent.templateProperties=psdl_persistent_templates.properties
PSSPMIConnector.ar.uba.fi.pmi.corba.pss.connector.PersistentConnector=Persistent
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
100
concretas que implementan dicha interfase: LocalDataStoreConnection y RemoteDataStoreConnection.
La primera y más simple, es una conexión a un repositorio local representado por un archivo. La segunda
forma de conexión permite acceder a un repositorio remoto existente en otra máquina.
class Persistent Connection
AbstractDataStoreConnection
+ AbstractDataStoreConnection(factory)+ close() : void+ createQuery() : Query+ doAttomicOperation(operation) : void+ flush(object, introspectionLevel) : void+ getContainer() : ObjectContainer
DataStore
+ connectionClosed(connection) : void+ create() : ObjectContainer+ DataStore(fi leName)+ DataStore(fi leName, user, password, port)+ openConnection() : DataStoreConnection+ shoutDown() : void
«interface»
DataStoreConnection
+ close() : void+ createQuery() : Query+ doAttomicOperation(operation) : void+ flush(object, introspectionLevel) : void+ getContainer() : ObjectContainer
LocalDataStoreConnection
+ close() : void+ LocalDataStoreConnection(server)
«interface»
ObjectContainerFactory
+ create() : ObjectContainer
CommonSessionBase
PersistentSession
+ createQuery(operation) : Query+ destroy(storageObject) : void+ doAttomicOperation(purpose, operation) : void+ exist(storageObject) : boolean
RemoteDataStoreConnection
+ RemoteDataStoreConnection(host, port, userName, password)
«interface»
AttomicDataStoreOperation
+ doOperation(container) : void
-connection
-server
«real ize»«real ize»
-factory
DISEÑO DEL SERVICIO 15 - CONEXIÓN PARA ACCESO A LA BASE DE DATOS
Cuando se crea una sesión, ésta recibe como parámetro un objeto que implementa
DataStoreConnection, que será provisto por el conector. El conector se vale de las clases
SessionParameterBuilder y SessionParametersExtractor, para recibir e identificar los parámetros
(representados por la clase Parameter), que definen el tipo de conexión que debe crear.
La interfase DataStoreConnection permite que la sesión sea completamente independiente del
tipo de conexión que se utilice, dado que todas las operaciones contra el DataStore se realizan a través
de dicha interfase.
RELACIÓN CON DB4O
La conexión que tiene asociada una sesión, permite acceder al ObjectContainer de DB4O, que
será utilizado internamente para almacenar y recuperar los objetos. El hecho de que el conector utilice
DB4O para mantener el estado está oculto a los ojos del usuario final del servicio. Esto se debe a que un
usuario final siempre interactuará con las interfases provistas por el servicio de CORBA, sin tener que
lidiar nunca con mis implementaciones de ellas, ni con sus detalles.
OBJETOS CON ESTADO COMPARTIDO
Para acceder a un almacén registrado en el conector, será necesario considerar algunos
problemas que no se presentaban en el conector de memoria. Existe una diferencia fundamental entre el
conector de memoria y el persistente, si bien todo objeto tiene estado (definido por sus atributos), en el
conector persistente, los objetos pueden ser compartidos por múltiples sesiones. Esta característica hace
que se deba tener particular cuidado en como se accede a estos objetos en forma concurrente.
El repositorio en sí mismo, tiene atributos que definen un estado que está compartido por todas
las sesiones que lo acceden. El estado está definido fundamentalmente por tres atributos: cuales
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
101
almacenes fueron creados en el repositorio, cuales objetos almacenados fueron creados en los
almacenes del repositorio y una secuencia. La secuencia permite obtener un identificador global dentro
del repositorio, que es asignado a los objetos almacenados al momento de su creación.
IDENTIFICADORES
El conector persistente se basa en el mecanismo general propuesto por el servicio para identificar
los objetos, mediante la clase: PersistentStorageObjectIdentifier, que implementa la interfase
StorageObjectIdentifier.
Como mencioné anteriormente, el modelo de objetos de Java no provee el concepto de
identificador, por esto DB4O no recomienda el uso de identificadores y no provee mecanismos para su
generación automática. Las bases de datos relacionales proveen mecanismos eficientes para la
generación de secuencias de números, como pueden ser tipos de datos autoincrementados. Estos
mecanismos soportan acceso concurrente y transaccional a las secuencias de forma tal que dos o más
operaciones concurrentes obtengan identificadores distintos.
El modelo general del servicio de persistencia provee una clase que permite generar secuencias
de bytes, ByteArraySequencer. El conector persistente debe garantizar el acceso concurrente y
transaccional a dicha clase para proveer identificadores válidos.
La forma más simple de garantizar el acceso concurrente, es serializar el acceso a la operación
next del objeto secuencia. Esto se logra comúnmente mediante el uso de un semáforo [Conc01]. Un
semáforo es una construcción que garantiza el acceso único a un recurso o segmento de código, en este
caso a la secuencia. Cuando uno o más usuarios quieren acceder en forma simultánea a un recurso, el
semáforo sólo permitirá el paso de uno de ellos, bloqueando a los siguientes, hasta que el usuario actual
del recurso lo libere. En Java, los semáforos están encapsulados dentro del lenguaje, cada objeto tiene un
atributo (no visible al usuario) que representa un semáforo. Este semáforo se utiliza mediante la palabra
clave synchronized, que indica que sólo un Thread podrá ejecutar el código dentro del bloque
sincronizado.
La clase ByteArraySequencer garantiza el acceso concurrente a ella sincronizando el acceso al
método next. Pero sólo serán sincronizados los Threads que se ejecuten dentro de la misma máquina
virtual y sobre la misma instancia de objeto.
Para crear un objeto, se requieren dos identificadores provistos por dos secuencias distintas, el
primero es la secuencia global de identificadores del repositorio y el segundo la secuencia para la familia
de objetos almacenados a la que pertenezca el objeto. Esto último implica que las secuencias serán
compartidas por distintas conexiones a un mismo repositorio y dichas conexiones pueden ser incluso
realizadas desde otras máquinas. Por esto el mecanismo original de sincronización de las secuencia es
insuficiente en este escenario.
OPERACIONES ATÓMICAS
Para garantizar la atomicidad de este tipo de operaciones (generación de identificadores y otras),
proveo una interfase llamada AtomicDataStoreOperation. Esta interfase representa una operación que
podrá ser realizada en forma atómica en un DataStore. Una operación atómica es aquella que sólo tiene
dos posibles resultados. Exitoso en cuyo caso el contexto queda modificado y fallido, en donde el
contexto queda en el mismo estado que antes de realizar la operación. El objetivo de esta interfase es
permitir operaciones que no alteren el contexto actual, sino que hagan uso de información obtenida
como resultado de la operación.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
102
class content
I:extends IdentifiedContent
AtomicDataStoreOverContentOperation
+ AtomicDataStoreOverContentOperation(initial)# commitBeforeUnlocking() : boolean+ doOperation(container) : void# getInitialContent() : I# getWaiteTimeOut() : int# operate(container, restored, blocked) : boolean
ByteArraySequencerContent
+ ByteArraySequencerContent()+ ByteArraySequencerContent(id)+ ByteArraySequencerContent(id, content)+ next() : byte[]
ByteArraySequencerOperation
+ ByteArraySequencerOperation(id)+ getValue() : byte[]# operate(container, restored, blocked) : boolean
ContentLocator
+ locate(container, template) : I
C:extends Object
IdentifiedContent
# assertInitialized() : void# assertNotInitialzed() : void+ getContent() : C+ getId() : String+ IdentifiedContent()+ IdentifiedContent(id, content)+ isInitialized() : boolean+ setContent(content) : void+ setId(id) : void+ toString() : String
«interface»
persistent::AtomicDataStoreOperation
+ doOperation(container) : void
«real ize»
#locator
DISEÑO DEL SERVICIO 16 - GENERACIÓN ATOMICA DE IDENTIFICADORES
El diagrama que se muestra arriba, esquematiza las clases involucradas en la generación de un
nuevo identificador a partir de una secuencia. La interfase DataStoreConexion provee el método
doAtomicOperation, que recibe como parámetro una operación a realizar. Toda operación se realiza en
un nuevo contenedor de objetos (no sobre el contenedor que utiliza la sesión), que es provisto por un
objeto de tipo ObjectContainerFactory.
Existe una clase concreta llamada ByteArraySequencerOperation, que hereda de la clase base
AtomicDataStoreOverContentOperation, que provee la funcionalidad necesaria para que pueda
realizarse la operación en forma atómica. La idea general, es realizar una operación sobre un contenido
inicial que puede o no estar almacenado en el repositorio. La clase ContentLocator es la encargada de
ubicar el contenido en el repositorio, en base a un objeto prototipo. Este prototipo se utiliza para realizar
un tipo de consulta por ejemplo. El contenido será siempre un objeto que implemente la interfase:
IdentifiedContent, que básicamente permite que un objeto sea identificado por una secuencia de
caracteres.
DB4O provee semáforos con soporte nativo de la base de datos que garantizan concurrencia
entre los distintos clientes. Los semáforos se indetifican por una secuencia de caracteres. Para acceder a
los semaforos se utiliza la intefase ExtObjectContainer de DB4O.
Al inicio de la operación, la clase AtomicDataStoreOverContentOperation iniciará un semáforo
mediante el método setSemaphore con el identificador del contenido a utilizar. De esta forma cualquier
otra operación que utilice el mismo contenido (secuencia), deberá esperar que la operación en curso
libere el semáforo mediante la operación releaseSemaphore. Una vez obtenido el semáforo, se tratará de
public boolean setSemaphore(String name, int waitMilliSeconds); public void releaseSemaphore(String name);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
103
ubicar el contenido con el ContentLocator, si el contenido no existe, se creará para ser almacenado en el
contendor. Sobre el contenido se realiza la operación necesaria, mediante el método operate de la
operación. Se almacena el resultado como atributo del objeto, luego se finaliza la operación liberando el
semáforo y se cierra la conexión.
Esta forma de ejecución garantiza que una única operación pueda ser realizada sobre el mismo
repositorio y sobre el mismo contenido en forma concurrente. Las operaciones son genéricas y pueden
ser aplicadas a cualquier proceso que requiera acceso exclusivo sobre un contenido del repositorio.
Esta forma de garantizar acceso concurrente, se denomina genericamente bloqueo pesimista
[RoySeif01] dado que se realiza el bloqueo del semáforo cada vez que se quiere acceder al recurso
compartido. En un esquema optimista se tiene la premisa que la mayoría de las operaciones no
interferirán entre ellas, por lo que no es necesario realizar un bloqueo inicialmente. De todas formas, se
tiene que garantizar que en el caso de que las operaciones interfieran, los resultados serán correctos. En
este caso no se deberían tener dos identificadores iguales en distintas operaciones. En el esquema del
identificador global, es altamente probable que las operaciones interfieran entre sí, dado que existe un
único identificador global por repositorio. Y por otro lado, se entiende que el servicio de estado
persistence no tendrá una cantidad masiva de conexiones remotas concurrentes, por lo que no es tan
crítico que el tiempo consumido por la operación de creación sea mínimo. Además el bloqueo optimista
es más complicado de implementar, es por esto y lo anteriormente dicho que consideré al bloqueo
pesimista como la mejor opción para este caso.
ALMACENES
Este conector también sigue el esquema de delegación propuesto inicialmente por el servicio.
Todo almacén de objetos que utilice este conector debe implementar la interfase:
PersistentStorageHome. Esta interfase permite que el conector implemente el mismo mecanismo de
delegación que el conector de memoria, extendiendo a la interfase base del servicio:
ExtendedStorageHome.
class General Schema
ExtendedStorageHome
«interface»
persistent::PersistentStorageHome
+ create(PersistentStorageHome, PersistentStorageHomeKeySet) : SO+ find_by_spec(PersistentStorageHome, StorageObjectSpec) : SO
AbstractRootStorageHome
persistent::PersistentRootStorageHome
{leaf}
AbstractChildStorageHome
persistent::PersistentStorageHomeBase
+ create(PersistentStorageHome, PersistentStorageHomeKeySet) : SO+ find_by_spec(PersistentStorageHome, StorageObjectSpec) : SO+ PersistentStorageHomeBase()
StorageHomeKeySet
persistent::PersistentStorageHomeKeySet
+ validate(String, PersistentStorageHome) : void
AbstractTargetCompiler
persistent::PersistentTargetCompiler
BaseStorageObjectIdentifier
persitent::PersistentStorageObjectIdentifier
+ createConstraint(Query) : Constraint+ getLong_pid() : Long+ getLong_short_pid() : Long+ PersistentStorageObjectIdentifier(PersistentStorageHome)+ PersistentStorageObjectIdentifier(byte[], byte[], PersistentStorageHome)+ PersistentStorageObjectIdentifier(byte[], byte[], String, String)CommonSessionBase
persistent::PersistentSession
+ createQuery(String) : Query+ destroy(StorageObject) : void+ doAtomicOperation(String, AtomicDataStoreOperation) : void+ exist(StorageObject) : boolean
ConnectorBase
connector::PersistentConnector
+ getConnection(PersistentSession, Parameter[]) : DataStoreConnection+ PersistentConnector()
persistent::SpecToConstraintTransformer
+ transform(StorageObjectSpec) : Constraint
«realize»
«realize»
DISEÑO DEL SERVICIO 17 - ESQUEMA GENERAL DEL CONECTOR PERSISTENTE
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
104
Existen dos implementaciones base de dicha interfase, la primera es
PersistentStorageHomeBase que es utilizada como clase base por el generador de código del conector.
Todo almacén generado por el compilador heredará directa o indirectamente de esta clase. La otra
implementación es PersistentRootStorageHome, que representa a la familia de almacenes para un tipo
de objeto almacenado que no tenga un supertipo.
El único estado de los almacenes que se mantiene en los repositorios es la secuencia que
permite generar identificadores para la familia. Cada conexión a un repositorio tendrá sus propias
instancias de los almacenes, pero todas ellas compartirán los mismos juegos de secuencias mediante
objetos de tipo ByteArraySequencerOperation, que utilizarán secuencias identificadas por los
identificadores de las familias de almacenes. De manera análoga, cada sesión que se conecte a un
repositorio tendrá como atributo una operación atómica que permitirá generar identificadores globales
para el repositorio. Esta operación utilizará un identificador especial reservado para la secuencia global.
OPERACIONES DE CONSULTA
Las operaciones de consultas se realizan utilizando una de las interfases que provee DB4O para tal
efecto. Dado que el usuario final no conoce esta interfase, será necesario que el compilador genere el
código necesario para construir las consultas para el servicio.
Las consultas que debe generar el compilador están definidas principalmente por las operaciones
de búsqueda que se deriven de la definición PSDL compilada. Estas consultas son dinámicas en el sentido
de que varían en función de la jerarquía de objetos que presenten los almacenes. Por esto, no pueden ser
generadas en su totalidad por el compilador, ya que éste sólo tiene una vista parcial del esquema, que
está dada por el archivo PSDL que esté procesando.
En consecuencia, es solamente en tiempo de ejecución donde se pueden conocer todos los
parámetros necesarios para construir las consultas requeridas. Esto implica que el conector deberá
proveer al compilador un mecanismo lo suficientemente amplio como para generar las consultas en
tiempo de ejecución.
Las consultas requeridas, siguiendo el esquema general propuesto en el servicio, son las basadas
en las especificaciones de los objetos (StorageObjectSpec). Con relativamente poco trabajo, se puede
traducir una especificación de objeto en una consulta de DB4O, representada por la clase Constraint. La
clase SpecToConstraintTransformer está encargada de esta tarea. Toma una especificación de objeto,
itera sobre todos los atributos de la especificación agregando una cláusula (Constraint) por cada atributo
que defina la especificación.
Este mecanismo es lo suficientemente genérico como para que el compilador se encargue de
agregar las entradas necesarias en las clases generadas, como para construir especificaciones válidas y en
base a ellas construir las consultas necesarias por el servicio.
CREACIÓN DE OBJETOS ALMACENADOS
Los mecanismos de construcción de objetos almacenados son los mismos, tanto para este
conector como para el conector de memoria, ya que ambos siguen el esquema de delegación propuesto
por el servicio. La diferencia radica en los mecanismos que se utilizan para la generación de
identificadores (antes descripta) y para la validación de las claves.
El mecanismo general para la validación se dispara cuando se construye un objeto y se basa en la
clase StorageHomeKeySet. Dicha clase es extendida por el conector persistente, mediante
PersistentStorageHomeKeySet, en donde se delega la validación en una consulta generada a partir de
todas las claves que se definan en la especificación. La consulta se genera utilizando la clase
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
105
SpecToConstraintTransformer. El conjunto de claves que incluya la especificación determinarán la forma
de consulta.
ACCESO REMOTO AL REPOSITORIO
El tipo de repositorio y como se accede al mismo no es parte de la especificación del servicio,
por lo que toda implicación derivada de permitir este tipo de acceso no está contemplada en el servicio.
Como ya mencioné anteriormente, el modo de acceso está definido por los parámetros de
entrada que recibe el conector cuando crea la sesión. Dado que mi repositorio está representado por una
base de datos DB4O, el acceso remoto se basa en el modelo cliente / servidor que esta base de objetos
provee. Todo repositorio en DB4O, se almacena en un archivo administrado por la base de datos.
La primera implicación, es que para acceder en forma remota se requiere que exista un servidor
corriendo en la máquina donde está almacenado el repositorio. O sea, para que un cliente pueda acceder
al repositorio remoto, el proceso servidor deberá estar operativo.
Para resolver esta primera implicación he tomado una política simple, cualquier servicio de
persistencia que se ejecute es un potencial servidor, cualquier repositorio que sea utilizado por algún
servicio de persistencia, podrá ser accedido en forma remota con tan sólo unos parámetros al momento
de inicio de la sesión. El único requerimiento para que se mantenga activo el servidor es que el proceso
donde se creó la sesión se mantenga activo. El conector persistente es el encargado de mantener activos
a los servidores locales existentes.
La clase ar.uba.fi.pmi.corba.pss.catalog.persistent.DataStore representa un repositorio local. El
conector lleva el registro de todos los DataStore creados desde que se inicializó el conector. Cuando se
crea una nueva conexión a un DataStore, sólo se crea un DataStore, si es la primera vez que se accedió a
ese DataStore, en caso contrario se le solicita al ya existente una nueva conexión. El DataStore hace
además las veces de servidor, cuando el mismo es construido con la información necesaria: un puerto
donde escuchar por conexiones, un usuario y una clave para garantizar el acceso.
deployment Deployment Model
máquina2
máquina3
máquina1
CPU1
CPU2
CPU3
Repositorio1 - DB4O
Repositorio3 - DB4OPMIPSS1
PMIPSS2
PMIPSS3
Cliente1
Cliente2
Cliente3
remoto
«use»
«local»
«remoto»
«local»
«use»
«use»
«use»
DISEÑO DEL SERVICIO 18 - ACCESO REMOTO A LOS REPOSITORIOS
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
106
La segunda implicación es muy importante al utilizar un modelo cliente / servidor, en el cual un
proceso hace las veces de cliente y otro de servidor. Para que ambos procesos puedan compartir objetos
del mismo tipo, ambos deben compartir el mismo espacio de clases, es decir que tanto el cliente como el
servidor deberán tener disponibles en tiempo de ejecución las mismas clases de objetos que serán
almacenadas y recuperadas del repositorio. Esta segunda implicación está resuelta por la primera, ya que
es el servicio de persistencia mismo quien hace las veces de cliente y servidor. Para que el servicio pueda
funcionar como tal requiere tener disponibles las clases que representan los objetos almacenados junto
con sus almacenes.
El gráfico 18 muestra un escenario real posible, donde existen 2 repositorios ubicados en dos
máquinas distintas. Hay tres clientes que acceden a estos repositorios, por medio de mi conector
persistente. El repositorio 2 es accedido en forma local y remota por dos conectores, uno ubicado en la
misma máquina donde está el repositorio (acceso local) y otro ubicado en otra máquina. En todos los
casos los clientes no se modifican por hacer uso del acceso remoto o local.
TRANSACCIONES
Como se describió anteriormente, el servicio de estado persistente soporta transacciones a
través del servicio de transacciones de CORBA [TS01]. El conector persistente tiene soporte para
transacciones a través de la base de datos que utiliza DB4O.
DB4O soporta todas las características que definen las transacciones, llamadas propiedades
ACID, de sus siglas en inglés: Atomicity, Consistency, Isolation, Durability [GrayReuter01]. Atomicity o
Atomicidad implica que todas las operaciones que se ejecutan dentro del contexto de una transacción,
son realizadas en su totalidad cuando la transacción es exitosa y cuando falla ninguna es realizada. Todas
las operaciones se pueden ver como una unidad atómica, todas o ninguna. Consistency o Consistencia
implica que la ejecución de la transacción deja la base de datos en un estado consistente, o sea no existen
estados intermedios dentro de una transacción para operaciones que se realicen fuera del contexto de la
transacción. Isolation o Aislamiento implica que cada transacción ve a la base de datos como si fuera la
única transacción que se ejecuta en forma simultanea, o sea la base bloqueará operaciones de
modificación concurrente, de forma tal que se ejecuten secuencialmente. Y finalmente, Durability o
Durabilidad implica que una vez ejecutada la transacción en forma exitosa, los cambios en la base son
permanentes y las próximas transacciones podrán verlos.
El tipo de transacción que se quiera ejecutar dependerá de la transacción generada por el
servicio de transacciones. Cuando se accede al servicio de estado persistente, sin hacer uso del servicio de
transacciones, todas las operaciones que se ejecuten dentro del contexto de una misma sesión serán
demarcadas automáticamente dentro del contexto de una transacción. De todas formas PSS permite el
uso de sesiones sin soporte de transacciones mediante algunas operaciones explicitas en ella, como por
ejemplo las operación flush y refresh. Estas operaciones tienen el objeto de operar sobre el sistema de
cache que implemente el conector. La primer operación debería forzar la realización de los cambios y la
segunda refrescar el estado de la sesión actual. Para este conector he optado por traducir la operación de
flush en un commit sobre la transacción actual, haciendo persistentes todos los cambios realizados hasta
ese instante. En DB4O, la operación de commit hace persistentes los cambios e inmediatamente abre
otra transacción sobre la que se puede seguir trabajando. La operación close sobre un ObjectContainer,
fuerza automáticamente un commit. En el caso de la operación refresh, la especificación misma indica
que sería extremadamente raro que un usuario final necesite llamar a este método y carece de sentido en
el uso normal de DB4O.
Este manejo de transacciones, sin hacer uso del servicio de transacciones, es una buena solución
de compromiso para los posibles usos que se le pueden dar al servicio de persistencia. No se trata de
reemplazar una base de datos orientada a objetos con el servicio de persistencia, sino de proveer una
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
107
solución acotada para mantener el estado de un servant, que generalmente tendrá pocos objetos para
persistir e interactuar.
CONSTRUCCIONES RESULTANTES DEL TRABAJO
Se construyó un compilador de lenguaje PSDL. El compilador consta dos partes principalmente
o Un parser que fue construido mediante Javacc y JJtree o Un generador de código fuente. El código fuente generado depende de la
implementación del servicio de persistencia que se defina.
El compilador usa un sistema generador de código fuente que construí para este trabajo. Utiliza
un modelo de clases y plantillas de código. Además construí un intérprete de clases que permite generar
esqueletos de clases e interfases en base a un objeto real dado. Estas dos construcciones pueden ser
utilizadas en cualquier otro trabajo que pueda o no estar relacionado con éste. Las plantillas se traducen a
código fuente utilizando la biblioteca Jexl que permite incrustar secciones dinámicas en las plantillas que
se evalúan en base a un contexto provisto por mi generador de código fuente.
Se construyeron dos implementaciones de conectores para mi servicio de estado persistente de CORBA:
o Una implementación que guarda los objetos en memoria o Otra implementación que utiliza una base de datos orientada a objetos, DB4O, para
persistir estos objetos.
Todas las construcciones realizadas, incluyendo el compilador, cuentan con pruebas unitarias,
para garantizar el funcionamiento de cada una de las partes. Estas pruebas permiten que las
construcciones puedan ser mejoradas y completadas, asegurando que lo construido siga funcionando de
la forma esperada. Las pruebas unitarias se realizaron utilizando la biblioteca JUnit.
Algunas de estas pruebas unitarias requieren compilar y validar su resultado en tiempo de
ejecución, lo que equivale a compilar código fuente Java en tiempo de ejecución. Para realizar esta tarea
he utilizado un framework llamado Janino, que permite utilizar clases compiladas en tiempo de ejecución
a partir de su código fuente (generado en este caso por el compilador).
Tanto el compilador como los conectores, utilizan internamente algunas bibliotecas de Jakarta
Commons como Loggings y Collections. Jakarta Commons es un proyecto de Apache que tiene el
propósito de construir componentes de software reutilizables en todo tipo de aplicaciones Java.
El código fuente fue desarrollado utilizando el IDE Eclipse y la versión 5 de Java. El código fuente
contiene más de 300 clases que corresponden al generador de código fuente, al compilador, al servicio de
estado persistente y a los dos conectores. Además se elaboraron más de 100 pruebas unitarias para todas
las funcionalidades construidas.
Al final de esta sección se encuentra una lista con las referencias utilizadas en el trabajo.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
108
UN EJEMPLO CONCRETO
A continuación voy a presentar un ejemplo simple, mostrando el código generado por el
compilador para el conector persistente, y demostrando cómo este código es utilizado en el contexto de
una aplicación CORBA.
CÓDIGO PSDL
El código PSDL representa una definición de un objeto almacenado abstracto, un almacén
abstracto para este objeto, junto con sus respectivas implementaciones.
En este ejemplo el objeto almacenado es Persona, su almacén es PersonaHome, y PersonaBase es
su implementación, mientras que PersonaHomeBase es la implementación del almacén. Persona tiene
tres atributos: nombre, apellido y número de documento, junto con una operación de usuario llamada
nombreCompleto. El almacén de la persona define una clave única en base al número de documento de
la persona. Y expone una operación de fábrica llamada crear, que permite construir personas con
nombre, apellido y número de documento en una sola operación.
RESULTADO DE LA COMPILACIÓN DEL CÓDIGO PSDL
Al compilar un archivo PSDL que contenga el código mostrado anteriormente, se obtiene la
siguiente lista de archivos:
Persona.java: interfase que define el objeto almacenado abstracto Persona.
PersonaBase.java: clase concreta que implementa a la interfase Persona.
PersonaHome.java: interfase que define el almacén abstracto de la Persona.
PersonaHomeBase.java: clase que implementa el almacén abstracto de la Persona.
PersonaHomeHelper.java, PersonaHomeHolder.java, PersonaHomeOperations.java,
_PersonaHomeLocalBase.java: Estos últimos archivos son generados de acuerdo con la especificación
CORBA. Toda interfase IDL tiene separadas sus operaciones en otra interfase, además debe tener sus
respectivas clases Holder, Helper. El último archivo es también requerido por la especificación de CORBA,
y representa una clase que permite obtener los identificadores IDL de la interfase CORBA, que son
utilizados para ubicar la interfase en un registro.
abstract storagetype Persona { state string nombre; state string apellido; state long nroDocumento; string nombreCompleto(); }; abstract storagehome PersonaHome of Persona { key nroDocumento; factory crear(nombre, apellido, nroDocumento); }; storagetype PersonaBase implements Persona { }; storagehome PersonaHomeBase of PersonaBase implements PersonaHome { };
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
109
Para el caso de este ejemplo me voy a focalizar sólo en los cuatro primeros archivos, que son los
más importantes a los efectos prácticos del servicio de persistencia.
PERSONA
El cuadro de texto siguiente representa una versión simplificada del código fuente de la interfase
generada por el compilador: Persona. Se han eliminado parte de los comentarios para reducir el tamaño.
En el código fuente, se muestra que la interfase hereda de la interfase StorageObject de PSS, además,
cada uno de los estados tiene sus métodos para lectura y escritura, y la operación de usuario
nombreCompleto.
Tanto los comentarios de inicio del archivo como los comentarios de la interfase y cada uno de los
métodos son generados en base a las plantillas de código configuradas en el compilador.
PERSONAHOME
Las primeras operaciones permiten ubicar a una Persona por la clave definida por el número de
documento. El último método representa la operación de fábrica, que permite crear Personas por sus
atributos.
/** * …. * <b>F.I.U.B.A.</b> * ….. * <a href='mailto:[email protected]'>Pablo Maximiliano Ilardi</a> * * Persona.java: was generated by PMI-PSDL Compiler Version 1.0 * …. */ package ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple; import org.omg.CosPersistentState.StorageObject; /** * ….. * @author PMI-PSDL Compiler Version 1.0 - persistent */ public interface Persona extends StorageObject { public abstract String nombre(); public abstract void nombre(String s); public abstract String apellido(); public abstract void apellido(String s); public abstract long nroDocumento(); public abstract void nroDocumento(long l); public String nombreCompleto(); }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
110
PERSONABASE
…. package ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple; …. public abstract class PersonaBase implements Persona, StorageObject, ExtendedStorageObject { private static final long serialVersionUID = -1737034293L; private StorageObjectIdentifier identifier; private long nroDocumento; private String apellido; private String nombre; public PersonaBase() { } public final long nroDocumento() { return this.nroDocumento; } public final void nroDocumento(long l) { this.nroDocumento = l; } public final String apellido() { return this.apellido; } public final void apellido(String s) { this.apellido = s; } public final String nombre() { return this.nombre; } public final void nombre(String s) { this.nombre = s; } public final byte[] get_pid() { return this.get_identifier().get_pid(); } public final byte[] get_short_pid() { return this.get_identifier().get_short_pid(); } public final StorageHomeBase get_storage_home() { return this.get_identifier().get_storage_home(); } public final void destroy_object() { ((ExtendedStorageHome) this.get_storage_home()).destroy(this); } … public final boolean object_exists() { return ((ExtendedStorageHome) this.get_storage_home()).exist(this); } …..
package ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple; …. /** * .... * @author PMI-PSDL Compiler Version 1.0 - persistent */ public interface PersonaHome extends PersonaHomeOperations, LocalInterface, IDLEntity, StorageHomeBase { } package ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple; … public interface PersonaHomeOperations extends StorageHomeBaseOperations { public Persona find_by_nroDocumento(long nroDocumento) throws NotFound; public byte[] find_ref_by_nroDocumento(long nroDocumento); public Persona crear(String nombre, String apellido, long nroDocumento); }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
111
Al segmento de código anterior también se le han eliminado los comentarios. Algunas
características a resaltar de esta clase son: que representa un objeto almacenado concreto que
implementa una definición abstracta (Persona), esta clase es la primera en una jerarquía y no requiere
que herede de otra clase que no sea la clase Object en Java. En Java es importante no forzar la herencia
de alguna clase, dado que sólo se soporta herencia de una única clase. Desde el punto de vista de la
persistencia transparente, si bien no es ideal requerir implementar interfases, como StorageObject o
ExtendedStorageObject, el forzar la herencia de alguna clase impondría una limitación mucho más
grande al modelo que el usuario final pueda definir.
Otra característica importante es que muchos métodos están declarados como final, esto implica
que no pueden ser redefinidos en subclases de esta clase. Esta característica se debe en algunos casos al
uso del patrón de diseño template method [Gamma01] (como en el caso del método initialize), y en otros
casos para asegurar el correcto funcionamiento de algunas operaciones como las de lectura/escritura de
las propiedades persistentes.
El método after_initialize muestra como los objetos son inicializados a partir de una
especificación, en este caso, se trata de inicializar las tres propiedades persistentes del objeto
almacenado.
Como la operación de usuario nombreCompleto no está definida por el compilador, la clase está
declarada como abstracta.
… public final boolean is_initialized() { return this.identifier != null; } public final synchronized void initialize(
StorageObjectIdentifier identifier, StorageObjectSpec specification) { if (this.is_initialized()) { throw new PERSIST_STORE(
"The storage object " + this.identifier + ", was already been initialized"); } this.identifier = identifier; this.after_initialize(specification); } public final StorageObjectIdentifier get_identifier() { if (!this.is_initialized()) { throw new IllegalStateException("The storageobject " + this + " has not been initialized"); } return this.identifier; } protected void after_initialize(StorageObjectSpec specification) { if (specification.hasValue("nroDocumento")) { this.nroDocumento = specification.get_long("nroDocumento"); } if (specification.hasValue("apellido")) { this.apellido = specification.get_string("apellido"); } if (specification.hasValue("nombre")) { this.nombre = specification.get_string("nombre"); } } }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
112
PERSONHOMEBASE
Como el almacén fue compilado para el conector persistente, esta clase hereda del almacén
persistente PersistentStorageHomeBase y sólo implementa el almacén abstracto de la persona,
PersonaHome.
PersonaHomeBase representa además, el primer almacén de la jerarquía. Esto se refleja en el
método initialize, donde se indica que el almacén debe ser inicializado como raíz (root) de la jerarquía.
Las operaciones de búsqueda find_by_nroDocumento, se traducen en la construcción de las
especificaciones con los valores de las claves que estén definidas en el almacén, en este caso es el
número de documento.
La operación de fábrica crear se limita a crear una especificación con los valores de los atributos
del objeto almacenado a crear, para luego delegar la construcción efectiva en un método de la
superclase.
PERSONABASEIMPL
Cuando los objetos almacenados o los almacenes definen operaciones de usuario, el usuario es
responsable de proveer la implementación para dichas operaciones. La clase PersonaBaseImpl solamente
se limita a implementar el método nombreCompleto.
package ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple; public class PersonaHomeBase extends PersistentStorageHomeBase implements PersonaHome { … public PersonaHomeBase() {} public Persona crear(String nombre, String apellido, long nroDocumento) { StorageObjectSpec spec = this.newSpec(); spec.set_string("nombre", nombre); spec.set_string("apellido", apellido); spec.set_long("nroDocumento", nroDocumento); return (Persona)this.create(spec); } public Persona find_by_nroDocumento(long nroDocumento) throws NotFound { StorageObjectSpec spec = this.newSpec(); spec.set_long("nroDocumento", nroDocumento); return (Persona)this.find_by_spec(spec); } public byte[] find_ref_by_nroDocumento(long nroDocumento) {…} protected void collectKeys(StorageHomeKeySet keys) { super.collectKeys(keys); StorageHomeKeyBuilder keynroDocumento = keys.keyBuilder("nroDocumento"); keynroDocumento.with("nroDocumento"); keynroDocumento.build(); } …… public StorageHomeBase initialize(Session session, String registered_id) throws NotFound { return this.initializeRootHome((PersistentSession)session, registered_id, "PSDL:ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple/PersonaBase:1.0"); } }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
113
En este caso, la operación simplemente concatena el apellido y nombre de la persona.
USO DEL MODELO EN UNA APLICACIÓN CORBA
Hasta este punto se mostró el modelo PSDL, ahora se verá como una aplicación CORBA puede
hacer uso de este modelo de trabajo propuesto por el servicio de estado persistente, PSS.
Toda aplicación CORBA está compuesta por objetos que proveen servicios llamados servants
definidos en lenguaje IDL y por objetos cliente que hacen uso de los servants. Los clientes acceden a los
servants mediante la ORB, quien provee referencias de los servants, de forma tal que los clientes siempre
tratan con objetos que implementan la interfase IDL expuesta por el servant y no con el objeto real que
provee el servicio.
En este ejemplo, existe un servant que provee servicios para acceder a personas, definidas
mediante el modelo PSDL. El servant es una instancia de la interfase RegistroPersona, definida en el
siguiente segmento de código IDL.
El servant RegistroPersona permite tres operaciones básicas sobre las personas: creación,
eliminación y consulta del nombre completo de la persona (método expuesto por el objeto almacenado
Persona definido en PSDL).
Las personas son creadas por el método crearPersona, cuyos parámetros identifican a la persona
y son los siguientes: nombre, apellido y número de documento. Para eliminar a una Persona se debe
llamar al método eliminarPersona, con un parámetro que indica el número de documento que identifica
a la Persona. Finalmente, para obtener el nombre completo de una Persona, se debe llamar al método
nombreCompleto con el número de documento de la Persona a consultar.
El siguiente gráfico muestra la interacción de las partes, cliente y servidor. La clase
ServidorPersonas representa el proceso servidor, donde se instancia al servant, que en este caso, es la
clase RegistroPersonasImpl, que implementa las operaciones de la interfase definida inicialmente,
RegistroPersona.
exception PersonaNoExiste {}; exception PersonaYaExiste {}; interface RegistroPersona { void crearPersona(in string nombre, in string apellido, in long nroDocumento)
raises (PersonaYaExiste); void eliminarPersona(in long nroDocumento)
raises (PersonaNoExiste); string nombreCompleto(in long nroDocumento) raises (PersonaNoExiste); void cerrarRegistro (); };
public class PersonaBaseImpl extends PersonaBase { /** * @see ar.uba.fi.pmi.corba.pss.psdl.test.persistent.simple.Persona#nombreCompleto() */ public String nombreCompleto() { return this.apellido() + ", " + this.nombre(); } }
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
114
El cliente está representado por la clase ConsultaPersonas. Esta clase accede al registro
mediante una referencia obtenida de la ORB. El acceso a la ORB, tanto para el cliente como para el
servidor, está centralizado en la clase ORBAccessor. Esta clase es simplemente una utilidad provista por
mí, que permite centralizar todas las operaciones que requieran interacción con la ORB, como acceso a
las referencias iniciales, al servicio de persistencia o a la creación de la ORB.
Este modelo de aplicación deja muy clara la separación entre el dominio de la aplicación CORBA
y el del servicio de persistencia. Todas las operaciones del registro de personas no están ligadas a los
objetos utilizados por el servicio de persistencia, esto se debe a que dichos objetos son privados, o sea
locales al servant y no pueden ser exportados por la ORB a otra máquina. Los objetos que define el
servicio de persistencia pasan a ser detalles de implementación del servant y desde el punto de vista de
un cliente del servant no existen.
object EjemploCORBAPSS
«servidor»Serv idorPersonas
+ main(String[]) : void+ ServidorPersonas(String[])
«cliente»ConsultaPersonas
# atenderConsultas() : void+ ConsultaPersonas(String[])+ leerEntrada() : String+ main(String[]) : void+ presentarMenu() : int
IDLEntityObject
«interface»
RegistroPersona
RegistroPersonaImpl
+ cerrarRegistro() : void+ crearPersona(String, String, int) : void+ eliminarPersona(int) : void# getPersona(int) : Persona+ nombreCompleto(int) : String+ RegistroPersonaImpl(ORBAccessor, PersonaHome)
Servant
RegistroPersonaPOA
«interface»
RegistroPersonaOperations
+ cerrarRegistro() : void+ crearPersona(String, String, int) : void+ eliminarPersona(int) : void+ nombreCompleto(int) : String
IDLEntityPersonaHomeOperations
StorageHomeBaseLocalInterface
«interface»
PersonaHome
StorageObject
«interface»
Persona
+ apel lido() : String+ apel lido(String) : void+ nombre() : String+ nombre(String) : void+ nombreCompleto() : String+ nroDocumento() : long+ nroDocumento(long) : void
utils::ORBAccessor
+ activateRootPOAManager() : void+ getDynAnyFactory() : DynAnyFactory+ getNameService() : NamingContextExt+ getOrb() : ORB+ getPersistentStateService() : ConnectorRegistry+ getRootPOA() : POA+ isIni tialized() : boolean+ object_to_string(org.omg.CORBA.Object) : String+ ORBAccessor()+ ORBAccessor(String[])+ ORBAccessor(String[], Properties)+ readObject(String) : org.omg.CORBA.Object+ shoutDown(boolean) : void+ string_to_object(String) : org.omg.CORBA.Object+ writeObject(org.omg.CORBA.Object, String) : void
-registro
-home
«creación»«accede»
DISEÑO DEL SERVICIO 19 - APLICACIÓN CORBA CON ACCESSO A PSS
A continuación voy a mostrar el segmento de código que da inicio al proceso servidor.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
115
Este segmento de código corresponde a la clase ServidorPersonas y muestra la interacción entre
el servant y el servicio de persistencia. El objeto server representa una instancia de la clase ORBAccessor,
la primera línea de código obtiene una referencia al registro de conectores como una referencia inicial a
la ORB. Las siguientes líneas inicializan al conector persistente, que es el conector identificado por
Persistent, con las clases que representan el dominio del servicio de persistencia. Estas clases son: el
almacén de personas PersonaHome y el objeto almacenado Persona. Las siguientes líneas están
dedicadas a la construcción de la sesión de trabajo con el servicio. La clase SessionParameterBuilder
permite construir los parámetros necesarios para crear la sesión de trabajo basada en el archivo
“registroPersonas.db”, que será utilizado por DB4O, como almacén de la base de datos que soporte a la
sesión. Finalmente, las últimas líneas permiten la inicialización del servant RegistroPersonaImpl con una
referencia al almacén de personas, que es utilizado para delegar todas las operaciones con personas.
Si se quisiera compartir la base de datos que da soporte al repositorio con otras instancias del
servicio de persistencia, tan sólo sería necesario agregar más parámetros al método que crea la sesión de
trabajo.
En el caso de los parámetros mostrados en el cuadro anterior, se habilita al repositorio para su
acceso remoto por el puerto 12300, mediante un usuario con nombre pablo, y clave test.
Parameter[] parameters = new SessionParameterBuilder() .withFileName("c:\\registroPersonas.db") .withUserName("pablo") .withUserPassword("test") .withPort(12300) .build(server.getOrb());
// Se accede al registro de conectores ConnectorRegistry registry = server.getPersistentStateService(); // Se obtiene el conector persistente Connector connector = registry.find_connector("Persistent"); // Se registra en el conector la clase PersonaBaseImpl connector.register_storage_object_factory( PSDLUtils.getRepositoryID(PersonaBase.class), PersonaBaseImpl.class); // Se registra el almacén por defecto final String idAlmacenPersonas = PSDLUtils.getRepositoryID(PersonaHomeBase.class); connector.register_storage_home_factory(idAlmacenPersonas, PersonaHomeBase.class); // Se crean los parámetros para utilizar el archivo registroPersonas.db como // base de datos para la sesión a crear Parameter[] parameters = new SessionParameterBuilder() .withFileName("c:\\registroPersonas.db").build(server.getOrb()); // Se crea una sesión Session mySession = connector.create_basic_session(READ_WRITE.value, parameters); // Ahora se obtiene el almacén de personas de la sesión PersonaHome almacenPersonas = (PersonaHome) mySession.find_storage_home(idAlmacenPersonas); // Ahora se crea el registro de personas CORBA RegistroPersonaImpl registro = new RegistroPersonaImpl(server, almacenPersonas); // Se activa el servant, para que se pueda acceder por otros procesos server.getRootPOA().activate_object(registro);
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
116
Otra instancia del servicio de persistencia que quiera acceder al mismo repositorio creado
anteriormente, deberá utilizar parámetros como los mostrados en el cuadro anterior. Se asume que la
dirección de Internet, donde se creó el repositorio inicialmente es 192.168.133.100.
Parameter[] parameters = new SessionParameterBuilder() .withHostName("192.168.133.100") .withUserName("pablo") .withUserPassword("test") .withPort(12300) .build(server.getOrb());
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
117
REFERENCIAS
PSS01 – Persistence State Service V2.0 OMG 2002. http://www.omg.org/cgi-bin/doc?formal/02-
09-06
TS01 - Transaction Service Specification, OMG
2003.http://www.omg.org/technology/documents/corbaservices_spec_catalog.htm
CORBA01 – Common Object Request Broker Architecture, Core Specification OMG.
http://www.omg.org/technology/documents/corba_spec_catalog.htm
JVMTI01 – Java Virtual Machine Tool interface – SUN Java 5 2004.
http://java.sun.com/j2se/1.5.0/docs/guide/jvmti
JVMI01 – Java Virtual Machine Instrumentation – SUN Java 5 2004.
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/instrument/Instrumentation.html
IDL2Java – IDL to Java Language Mapping Specification. http://www.omg.org/cgi-
bin/doc?formal/02-08-05
CorbaJava00 – CORBA Technology and the Java (TM) 2 Platform Standard Edition.
http://java.sun.com/j2se/1.5.0/docs/guide/idl/corba.html
Garshol00 – BNF and EBNF: What are they and how do they work?, Lars Marius Garshol.
http://www.garshol.priv.no/download/text/bnf.html
CooperRice00 – Engineering A Compiler, Keith Cooper - Rice University, Houston, Texas; Linda
Torczon - Rice University, Houston, Texas http://www.cs.rice.edu/~keith/
Javacc00 – Java Compiler Compiler [tm] (JavaCC [tm]) - The Java Parser Generator.
https://javacc.dev.java.net/
JavaccTu00 – The JavaCC Tutorial, Theodore S. Norvell http://www.engr.mun.ca/~theo/JavaCC-
Tutorial/
Crenshaw00 – Let's Build a Compiler, by Jack Crenshaw http://compilers.iecc.com/crenshaw/
Dasgupta00 – Algorithms, S. Dasgupta, C. H. Papadimitriou, and U. V. Vazirani. July 18, 2006.
Gamma01 – Design Patterns: Elements of Reusable Object-Oriented Software - Addison-Wesley
Professional Computing Series - by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides,
January 15, 1995.
BM0 – Meyer, Bertrand. “Object-Oriented Software Construction”. Prentice Hall, 1997. ISBN 0-
13-629155-4.
GrandM01 – Patterns in Java: A Catalog of Reusable Design Patterns Illustrated with UML, 2nd
Edition, Volume 1, by Mark Grand. Wiley; September 17, 2002. ISBN-10: 0471227293.
KingG01 – Hibernate in Action, Christian Bauer, Gavin King; Manning, 2005. ISBN: 1932394-15-X.
http://www.hibernate.org
RoodsR01 – Java Data Objects, Robin M. Roods; Addison-Wesley, 2003. ISBN 0-321-12380-8.
http://java.sun.com/javaee/technologies/jdo
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
118
ReverbelF01 – Persistence in Distributed Object Systems: ORB/ODBMS Integration, Francisco
Reverbel. Ph.D. Dissertation Presented to the Computer Science Department of the University of
New Mexico. April 1996.
NewardT01 – Avoiding the Quagmire Offering solutions to the problems of Object/Relational-
Mapping by Ted Neward. May 21th, 2007. http://www.odbms.org/experts.html#article12
CookWR01 – Native Queries for Persistent Objects. A Design White Paper. William R. Cook.
Department of Computer Sciences, The University of Texas. Febraury 15th,
2006.http://www.cs.utexas.edu/users/wcook/papers/NativeQueries/NativeQueries8-23-05.pdf
Castor01 – Castor Open Source data binding framework for Java. http://www.castor.org
Toplink01 – Oracle TopLink, Oracle Fusion Middleware family of products.
http://www.oracle.com/technology/products/ias/toplink/index.html
CKA01 - Surviving Object-Oriented Projects, By Alistair Cockburn, Dec 22, 1997, Addison Wesley
Professional. Part of The Agile Software Development Series.
DBO401 – Database for Objects. http://www.db4o.com/about/productinformation
SODA01 – Simple Object Database Access. http://sodaquery.sourceforge.net
Cache01 – InterSystems Caché® high-performance object database.
http://www.intersystems.com/cache/index.html
Annotations01 – Java 5.0 Annotations.
http://java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html
Conc01 – The origin of concurrent programming: from semaphores to remote procedure calls
book contents. ISBN:0-387-95401-5, Center for Science and Technology, Syracuse University,
Syracuse, NY. Edsger W. Dijkstra, Per Brinch Hansen, C. A. R. Hoare
RoySeif01 – Concepts, Techniques, and Models of Computer Programming, by Peter Van Roy and
Seif Haridi. The MIT Press Cambridge, Massachusetts London, England. ISBN 0-262-22069-5
GrayReuter01 - Transaction Processing: Concepts and Techniques by Jim Gray, Andreas Reuter ,
1993 Morgan Kaufmann. ISBN 1558601902
DEVQTC01 – Developing Quality Technical Information. A handbook for Writers and Editors.
Second Edition. G. Hargis, M. Carey, A. Hernandez, P. Hughes, D. Longo, S. Rouiller, E. Wide. ISBN
0-13-1477490.
PSS-PMI – Mi servicio de estado persistente Pablo M. Ilardi
119
REFERENCIAS A BIBLIOTECAS
Jakarta Commons – http://commons.apache.org/
Jakarta Commons Logging – http://commons.apache.org/logging/
Jakarta Commons Collections – http://commons.apache.org/collections/
Jakarta Commons Jexl – http://commons.apache.org/jexl/
Hibernate – http://www.hibernate.org/
JDO – http://java.sun.com/jdo/
Db4o – Database For Objects , http://www.db4o.com/
Javacc – Java Compiler Compiler, https://javacc.dev.java.net/
JJTree – https://javacc.dev.java.net/doc/JJTree.html
Junit – http://www.junit.org/
Janino – An Embedded Java Compiler, http://www.janino.net/
Eclipse – http://www.eclipse.org/
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
121
COMPARACIÓN CON OTRAS IMPLEMENTACIONES DEL SERVICIO DE ESTADO
PERSISTENTE DE CORBA
Servicios a Comparar ..............................................................................................123
OpenORB ............................................................................................................123
OpenCCM ............................................................................................................124
Puntos de Comparación ..........................................................................................124
Comparación entre OpenORB, OpenCCM y mi servicio ..........................................125
Compatibilidad con el estándar ...........................................................................125
Nivel de dependencia con la ORB ........................................................................127
Conectores disponibles y características de los mismos .......................................127
Requerimientos y facilidad de uso .......................................................................127
Extensibilidad para la creación de nuevos conectores..........................................128
Conclusiones ...........................................................................................................128
Referencias .............................................................................................................130
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
123
ado que PSS no es uno de los servicios CORBA más difundidos, existen pocas implementaciones,
y especialmente en Java. Cuando se habla de una implementación de CORBA, generalmente se
está hablando de implementaciones de la ORB. Todas las implementaciones proveen además de
la ORB, implementaciones de los servicios básicos, como puede ser el NameService y algunas en
particular ofrecen implementaciones del PSS.
Mi proyecto no contempla la creación de una ORB, ya que hay muchas implementaciones de
ella, sino se propone integrar el servicio con, idealmente, cualquier implementación de la ORB en Java.
He probado mi servicio, con dos implementaciones de ORB: la de SUN para Java que trae la máquina
virtual y la ORB de JacORB [JACORB01]. JacORB es una implementación en código abierto del
Departamento de Ciencias de la Computación de la Universidad de Freie en Berlín, Alemania.
Ninguna de las dos ORBs con las que he probado mi servicio, tienen implementaciones de PSS, y
por ende, constituyeron un buen escenario para integrar mi servicio con ellas.
SERVICIOS A COMPARAR
De las pocas implementaciones existentes, para realizar la comparación he elegido las dos más
representativas: OpenORB [OPENORB01] y OpenCCM [OPENCCM01].
OPENORB OpenORB (u ORB abierta) es una implementación de la ORB de CORBA, basada en una ORB
anterior, escrita totalmente en Java, y llamada JavaORB (que actualmente se encuentra discontinuada).
Esta ORB fue construida por el grupo de personas llamado Distributed Object Group (Grupo de Objetos
Distribuidos) [DOG01], que se dedica a proveer tecnología CORBA de código abierto. Uno de los servicios
que provee OpenORB es el servicio de estado persistente que utilizaré para la comparación. Según la
documentación de este servicio, provee una implementación totalmente compatible con la especificación
PSS de CORBA.
OpenORB provee tres conectores para el servicio: conector de memoria sin soporte
transaccional, otro conector con soporte de archivos donde se almacena el estado persistente con
soporte transaccional y un tercer conector que utiliza una base de datos relacional para almacenar el
estado persistente del repositorio. Este último conector requiere un proveedor de base de datos
relacional, que se encuentra fuera del control del servicio. Al tratarse de una dependencia no controlada
por el servicio, la aplicación CORBA requiere tener configurada una base de datos relacional para poder
utilizar esta implementación del servicio.
Según la documentación, este servicio requiere utilizar su propia implementación de ORB y su
servicio de Transacciones OTS, también provisto por este grupo.
El servicio provee un compilador PSDL, que se invoca mediante un script por línea de comandos.
El servicio no soporta persistencia transparente, sólo se puede utilizar el servicio mediante una definición
PSDL compilada por su propio compilador.
La configuración se realiza a través del mecanismo de configuración de la ORB provista por
OpenORB, lo que lo hace completamente dependiente de ella. Esta configuración también incluye los
parámetros de uso de los conectores de base de datos relacional y de archivos, dejando sin uso a los
parámetros de construcción de la sesión.
En cuando a las posibilidades de extensión, la documentación sólo trata la interacción con el
servicio por un usuario del mismo. De todas formas, el código fuente es libre y se lo puede analizar. El
compilador de PSDL que utiliza, está basado en el compilador de OpenORB para el que está definido el
D
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
124
servicio. Para agregar un nuevo conector es necesario modificar el código del servicio, ya que éste no
provee configuración alguna que permita un comportamiento dinámico del compilador. El compilador
sólo genera código para sus tres conectores. De manera análoga, el servicio tampoco soporta registro
dinámico de conectores, lo que obliga a modificar el código para agregar un nuevo conector.
OPENCCM OpenCCM también surge de JavaORB, pero no se trata de una implementación de ORB, sino de
una implementación del modelo de componentes de CORBA [CCM01]. Una parte de CCM es la ORB que
se utiliza internamente y en este caso se trata de JacORB. Al tratarse de un contenedor de componentes,
de acuerdo a la especificación de CORBA, es posible también combinarlo con el servicio de estado
persistente PSS. OpenCCM provee su propia implementación de PSS.
Este servicio provee dos conectores de funcionalidades similares. El primero es un conector que
utiliza al framework Hibernate [KingG01] para almacenar el estado en una base de datos relacional.
Hibernate es una herramienta de mapeo de objetos a bases de datos relacionales. El otro conector utiliza
a la API de SUN, para persistencia de objetos llamada JDO (Java Database Object) [RoodsR01]. Si bien el
servicio provee dos conectores, se debe definir en tiempo de compilación del servicio cual conector se
utilizará, lo que deja en la práctica un único conector posible en tiempo de ejecución. El servicio está muy
acoplado a la implementación de la ORB y al contenedor de componentes. No me fue posible configurar
el conector de Hibernate en mis pruebas, sólo logré hacerlo funcionar con JDO.
Tanto JDO como Hibernate requieren del uso de una base de datos relacional. De todas formas
Hibernate provee herramientas para utilizar una base de datos incrustada en el proceso donde se lo
utilice, como por ejemplo HypersonicSQL [HSQLDB01] que está realizada completamente en Java.
La posibilidad de extensión está dada en el momento de compilación del servicio, pero está muy
acoplada al modelo de construcción de componentes y servicios que provee OpenCCM.
PUNTOS DE COMPARACIÓN
Las características que he comparado con estas dos implementaciones del servicio son:
1. Compatibilidad con el estándar Se evalúa el grado en que se respetan los requerimientos impuestos por el estándar con respecto
a los requerimientos del mismo, tales como claves múltiples, construcciones de PSDL, generación de
código Java, funcionalidades del compilador, etc.
2. Nivel de dependencia con la ORB Se evalúa la dependencia de la implementación del servicio con respecto a la ORB para la que se
promueve su utilización.
3. Conectores disponibles y características de los mismos Este punto es quizás, el más importante desde la perspectiva de un usuario del servicio. Se
evalúan los conectores que provee el servicio, sus requerimientos y funcionalidades que proveen.
4. Requerimientos y facilidad de uso Otro punto importante para un usuario final son los requerimientos que impone el uso de un
determinado servicio o conector, y los conocimientos y recursos de los cuales debe disponer un nuevo
usuario.
5. Extensibilidad para la creación de nuevos conectores Finalmente este punto trata de evaluar las posibilidades y limitaciones para la extensión del
servicio, ya sea mediante un nuevo conector o modificando el servicio.
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
125
COMPARACIÓN ENTRE OPENORB, OPENCCM Y MI SERVICIO
COMPATIBILIDAD CON EL ESTÁNDAR Uno de los puntos destacados de la especificación de CORBA es la unicidad de interfases entre
distintas implementaciones del servicio. Esto implica que todos los servicios deberán generar el mismo
código para las mismas definiciones abstractas compiladas.
OpenORB, genera código de interfases incompatible con la especificación, dado que no respeta
el mapeo de las referencias. De acuerdo a la especificación, el mapeo de Java define que las referencias
se deben traducir a byte[] y para C++, a clases llamadas EntidadRef, donde Entidad es el tipo del objeto
almacenado. En OpenORB, las referencias se traducen en clases EntidadRef, es decir que utilizaron el
mapeo de C++ en Java. Al definir una clase particular para la representación de las referencias, da como
resultado que el código generado por su compilador sea muy distinto al esperado por la especificación.
OpenCCM, tampoco respeta las interfases. De acuerdo a la especificación, las interfases que
representen objetos almacenados deberán heredar de la interfase StorageObject de CORBA. Sin embargo
en OpenCCM, heredan de una interfase definida por ellos:
org.objectweb.openccm.pss.runtime.common.api.StorageObject, que a su vez hereda de
StorageObject. Dicha interfase agrega un método inicialize, que es utilizado para indicarle al objeto el
almacén al que pertenece.
En cuanto a la generación de almacenes abstractos, OpenORB y mi compilador generan código
muy similar, pero OpenCCM genera código totalmente incompatible con CORBA.
OpenCCM no genera interfases para las operaciones del almacén. Todas las operaciones están
declaradas dentro de la misma interfase del almacén. Tampoco genera las clases de utilidad como Holder
y Helper que requiere CORBA para una interfase. OpenCCM no genera interfases CORBA válidas.
Considero que es un problema importante que no se respete el estándar a nivel de interfase,
dado que expone la implementación de un servicio CORBA al usuario del mismo, sin permitir un
intercambio directo de implementaciones entre distintos proveedores del servicio. La independencia de
la implementación es una de las características más importantes de las especificaciones CORBA.
// En OpenCCM public interface PersonHome extends org.omg.CosPersistentState.StorageHomeBase // En mi compilador (similar en OpenORB) public interface PersonHome extends PersonHomeOperations, LocalInterface, IDLEntity, StorageHomeBase
// En OpenCCM public interface Person extends org.objectweb.openccm.pss.runtime.common.api.StorageObject // En mi compilador import org.omg.CosPersistentState.StorageObject; public interface Person extends StorageObject
// En OpenORB public void adress( org.objectweb.openccm.pss.demo1.AddressRef arg ); // En mi compilador public abstract void adress(byte[] br);
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
126
En cuanto al compilador, tanto OpenCCM como OpenORB, proveen soporte de preprocesador,
que permite darle directivas al procesador de código. Por ejemplo, se puede definir un módulo con un
nombre simple para traducirlo en Java a un paquete más complejo con varios niveles de nombre. En el
caso de mi compilador, sólo soporta la directiva al preprocesador para incluir archivos externos en el
proceso de compilación.
Una característica que sólo posee mi compilador, es la optimización de los nombres de clase
utilizados. En Java se puede referenciar a una clase por su nombre completo (incluyendo el paquete), o
por su nombre importando el nombre completo en el archivo, si es que la clase se encuentra definida en
otro paquete Java. Si la misma clase se utiliza en muchos lugares del código, es recomendable importarlo,
para poder referenciarla simplemente con su nombre en todos esos lugares. Mi compilador genera
código optimizado, importando las clases utilizadas cuando sea necesario y sólo utiliza el nombre
completo de la clase cuando exista otra clase utilizada en el mismo archivo pero definida en otro paquete.
En el caso de OpenORB y OpenCCM, se utiliza el nombre completo siempre, incluso cuando la clase
referenciada se encuentra dentro del mismo paquete Java.
Tanto en el caso de OpenORB como OpenCCM, sus respectivos compiladores generan código
para las construcciones IDL definidas en el archivo PSDL compilado, mi compilador no lo hace, tan solo las
ignora.
En cuanto a la generación de clases concretas, si bien queda fuera del alcance de la
especificación, existen algunas características importantes. Mi compilador y el de OpenORB generan
objetos almacenados que sólo implementan interfases, mientras que OpenCCM obliga a heredar de una
clase interna del conector. Además OpenCCM también obliga a implementar una interfase de JDO,
exponiendo totalmente los detalles de la implementación del conector, que en este caso logra la
persistencia por medio de JDO. En el caso de OpenORB, se implementan interfases propias del conector,
mientras que el caso de mi servicio solo se implementa una interfase definida por el servicio, ocultado los
detalles del conector que se esté utilizando.
Como resultado de esta comparación, se puede decir que tanto OpenORB como OpenCCM
generan interfases incompatibles con la especificación CORBA por los motivos expuestos. De todas
formas, OpenCCM tiene un mayor grado de incompatibilidad al exponer interfases propias de su
// En OpenCCM public abstract class ST_Person extends org.objectweb.openccm.pss.runtime.jdo.lib.StorageObjectBase implements org.objectweb.openccm.pss.demo1.Person, javax.jdo.InstanceCallbacks // En OpenORB public abstract class ST_Person implements org.objectweb.openccm.pss.demo1.Person, org.openorb.pss.connector.memory.PersistentObject // En mi compilador public abstract class ST_Person implements Person, StorageObject, ExtendedStorageObject
// En OpenORB o OpenCCM public org.objectweb.openccm.pss.demo1.Address adress(); public void adress( org.objectweb.openccm.pss.demo1.Address arg ); // En mi compilador public abstract Address adress(); public abstract void adress(Address b);
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
127
implementación e ignorar la definición de interfases para almacenes. Mi compilador está en desventaja al
no generar construcciones IDL, aunque genera código más optimizado para las construcciones PSDL y
compatible con la especificación CORBA.
NIVEL DE DEPENDENCIA CON LA ORB OpenORB requiere la configuración del servicio por los mismos mecanismos que usa para
configurar su implementación de ORB. Esto implica que sólo se puede utilizar el servicio con esta
configuración, y por ende, con una implementación particular de la ORB. A nivel de código fuente, es
también dependiente de la implementación de la ORB. Esto se debe principalmente a que se reutilizaron
muchas utilidades / funcionalidades entre los distintos servicios que esta ORB provee.
En cuanto a OpenCCM, la documentación menciona explícitamente que se requiere de
OpenCCM para que el servicio funcione correctamente.
En conclusión, mi servicio es el único que soporta distintas ORBs. La dependencia de una ORB en
el contexto de la implementación completa de una ORB, no es necesariamente una mala cualidad, dado
que la dependencia permite un grado más alto de reutilización de componentes entre servicios. Por
ejemplo, es una gran ventaja no tener que volver a implementar el compilador desde cero para cada
lenguaje que requiera cada servicio, si el compilador IDL de la ORB permite extensiones al lenguaje, tal
como PSDL.
CONECTORES DISPONIBLES Y CARACTERÍSTICAS DE LOS MISMOS El servicio de OpenORB es el que provee mayor número de conectores, tres conectores. El
conector de memoria es equivalente a mi conector de memoria con las mismas funcionalidades. Tanto el
conector de archivos, como el de base de datos, proveen integración con el servicio de transacciones.
Ninguno de los dos conectores hace referencia a acceso remoto al repositorio.
OpenCCM provee solo dos conectores con las mismas funcionalidades, la única diferencia entre
ellos, es la biblioteca que utilizan para persistir los objetos almacenados. De todas formas, el servicio
solamente funciona con uno de los conectores por vez, debido a la configuración que requiere la ORB
para habilitarlos. OpenCCM provee integración con su implementación del servicio de transacciones de
CORBA. Al igual que OpenCCM, tampoco hace referencia al acceso remoto del repositorio.
Analizando el código fuente de OpenORB, se deduce que no tiene soporte para compartir el
repositorio entre más de un servicio. No implementa ningún mecanismo para la generación de
identificadores, que contemple otro proceso además del propio. Para la generación de identificadores
utiliza la hora de la máquina en la cual se está ejecutando el proceso que genera el identificador único.
En el caso de OpenCCM, se utilizan los mecanismos que proveen tanto Hibernate como JDO para
la generación de identificadores, mediante alguna traducción a los identificadores que requiere el servicio
de estado persistente. Estos mecanismos se basan en la generación de identificadores mediante una
secuencia o algún valor auto incrementado que provea la base de datos relacional que se utilice en
conjunto con el servicio.
REQUERIMIENTOS Y FACILIDAD DE USO En el caso de OpenORB, tanto el conector de memoria como el de archivos, no tienen
requerimientos adicionales, sólo se debe configurar el servicio. En el caso del conector de base de datos
relacional, se requiere tener una base datos operativa para poder utilizar el servicio.
OpenCCM requiere de una configuración adicional para poder utilizar el servicio. Primero se
debe identificar el tipo de conector que será utilizado y luego, tanto para el caso del conector de JDO
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
128
como para el de Hibernate, es necesario crear y administrar la base datos con la cual se utilizará el
servicio, antes de poder utilizar el servicio mismo.
Tanto OpenORB como OpenCCM requieren de la intervención explícita del usuario para poder
utilizar el conector de base de datos relacional. El usuario debe agregar también el driver de conexión
para la base datos que decida utilizar.
En el caso de mi servicio, no se requiere ninguna configuración adicional para que funcione el
servicio o los conectores. Solamente requiere los parámetros necesarios para la creación de la sesión de
trabajo. A diferencia de los otros dos servicios, mis conectores no exponen los detalles de
implementacion de los mismos para su configuración. Cuando se utiliza el conector para DB4O, el usuario
no sabe realmente cómo y con qué se están persistiendo los objetos.
EXTENSIBILIDAD PARA LA CREACIÓN DE NUEVOS CONECTORES OpenORB no prevé la existencia de nuevos conectores. Su registro de conectores debe conocer
los tipos de conectores que provee el servicio. Para agregar nuevos conectores es necesario modificar el
código fuente del servicio y recompilarlo.
OpenCCM tiene muy bien desacoplado el concepto de interfase e implementación. Se puede
configurar mediante modificaciones al script que compila el servicio indicándole el tipo de conector que
se utilizará. El proceso de compilación buscará un directorio para la interfase y otro para la
implementación con el nombre del conector configurado. De todas formas, esta configuración está muy
ligada al proceso de compilación ya que no se realiza en tiempo de ejecución.
Mi servicio de persistencia es el único que provee una configuración en tiempo de ejecución de
los conectores que estarán disponibles. Para agregar un nuevo conector no se requiere recompilar el
servicio, sino solamente compilar el nuevo conector y agregar las líneas de configuración para el servicio.
El registro de conectores está completamente desacoplado de las implementaciones de conectores.
CONCLUSIONES
Un punto que está fuera de la comparación es la cantidad de personas involucradas en el
proyecto. Tanto en OpenORB como OpenCCM, los proyectos están constituidos por múltiples personas y
además, al contar con el soporte de una ORB, tienen muchas utilidades que se puede reutilizar en el
servicio. En mi caso se trató de un proyecto de una única persona.
Desde el punto de vista de la extensibilidad creo que mi servcio es el más apto de los tres, dado
que es el que permite mayor grado de configuración sin recompilar y también es el único que no expone
detalles de implementación de los conectores. Mi compilador de PSDL está en desventaja al no generar
código fuente para las construcciones IDL, esta debe ser la primer tarea a encarar en un trabajo futuro
sobre el servicio. Implementar la integración con algún servicio de transacciones de CORBA (TTS) también
es una característica que sería importante agregar. Desde el punto de vista funcional los tres servicios
proveen funcionalidades similares (salvo por la integracion con TTS). En relación a la correctitud de los
requerimientos de la especificación, creo que mi servicio es el más apto, ya que es el único que según mi
interpretación del estándar, lo respeta. La independencia de la ORB es un objetivo propio de mi trabajo y
no tiene porque estar soportado por otros servicios.
Ninguna de las implementaciones del servicio que he analizado, tanto en Java como en C++,
incluyendo la mía, proveen soporte para persistencia transparente. Esto creo que se debe principalmente
a que no está bien resuelto por la especificación, y en el modelo de trabajo de CORBA es común requerir
de un compilador de un lenguaje genérico a un lenguaje concreto.
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
129
Finalmente, el acceso remoto al repositorio es también una caracteristica que está fuera del
alcance de la especificación y que en las otras implementaciones del servicio está solo resuelta
parcialmente por el uso de una base de datos relacional.
La forma de accesso remoto propuesta tiene como desventaja que no permite independecia de
la ubicación, ya que depende directamente de la ubicación donde se creó el repositorio. De todas
formas, la ubicación del repositorio es sólo necesaria al momento de crear la sesión de trabajo y es éste el
único punto donde está expuesta. Este grado de desacoplamiento de la ubicación permite migrar el
repositorio en forma manual y relativamente sencilla, poniendo la ubicación física del repositorio en una
configuración externa a la aplicación.
A continuación expongo una tabla comparativa entre las distintas características analizadas y los
tres servicios de estado persistente comparados.
Característica \ Servicio OpenORB OpenCCM PMI-PSS
Respeta el estandar No No Si
Depende de una ORB Si Si No
Candidad de Conectores 3 2(uno x vez) 2
Complejidad de Uso Media Alta Baja
Requiere adicionales BDR + Driver BDR + Driver No
Extensibilidad Baja Media Alta
Soporte de transacciones Corba TSS Corba TSS Solo x DB4O
Compilador IDL + PSDL IDL + PSDL PSDL
Persistencia Transparente No No No
Acceso Remoto No x BD Relacional x Conector Persistente
Expone implementación Si, x BD Relacional Si, x BD Relacional y JDO No
Candidad de Personas +3 Consorcio de Software 1(Yo)
COMPARACIONES 1 - TABLA COMPARATIVA
Comparación con otras implementaciones del servicio de estado persistente Pablo M. Ilardi
130
REFERENCIAS
JACORB01 – JacORB: Software Engineering and Systems Software Group, at the CS department of
Freie Universität Berlin, Germany. www.jacorb.org
OPENORB01 – The Community OpenORB, www.openorb.org
DOG01 – Distributed Object Group. dog.team.free.fr
OPENCCM01 – OpenCCM - The Open CORBA Component Model Platform.
openccm.objectweb.org
CCM01 – CORBA Component Model (CCM) Specification, OMG.
http://www.omg.org/technology/documents/formal/components.htm
KingG01 – Hibernate in Action, Christian Bauer, Gavin King; Manning, 2005. ISBN: 1932394-15-X.
http://www.hibernate.org
RoodsR01 – Java Data Objects, Robin M. Roods; Addison-Wesley, 2003.ISBN 0-321-12380-8.
http://java.sun.com/javaee/technologies/jdo
HSQLDB01 – HSQLDB a relational database engine written in Java, with a JDBC driver, supporting a
large subset of ANSI-92 SQL. hsqldb.sourceforge.net
Trabajos Adicionales Pablo M. Ilardi
131
TRABAJOS ADICIONALES PSS tiene algunas limitaciones en su modelo que podrían ser incorporadas en un futuro. La
principal es la posibilidad de hacer consultas complejas. La única alternativa de consultas que se plantea
en PSS es mediante consultas por las claves de los objetos en los almacenes. Existen situaciones reales en
las que se quieren realizar consultas más avanzadas sobre los objetos, por atributos que no son parte de
la clave necesariamente o por condiciones complejas. Este tipo de consultas están bien resueltas en la API
de SODA en la que se basa uno de los modos de consulta de DB40.
Existe otra limitación importante de PSS, que es el manejo de colecciones. En PSS la única forma
de manejar una colección como atributo / estado de un objeto almacenado, es mediante un array de
objetos almacenados, lo que se logra mediante una definición IDL de un arreglo de objetos almacenados.
Si bien es suficiente para almacenar más de un objeto como estado, el lenguaje Java provee posibilidades
mucho más avanzadas para el manejo de colecciones, tales como colecciones con semántica de conjuntos
o colecciones indexadas. El tratar con un array plano de objetos hace que se requiera la construcción de
operaciones que ya están definidas en el lenguaje en forma nativa para poder utilizarlas con el modelo de
PSS. Esta limitación concretamente se debe a que PSS es una especificación multilenguaje,
particularmente para Java y C++. Esto requiere que las funcionalidades provistas deban ser soportadas
por todos los lenguajes de forma relativamente simple y en el caso de C++, los arrays son las únicas
construcciones nativas que se soportan para el manejo de colecciones.
Mi implementación en particular requiere agregar al compilador de PSS la posibilidad de generar
construcciones IDL para permitir interactuar a los objetos almacenados con ellas, por ejemplo, mediante
atributos / estados que sean tipos definidos en IDL directamente.
De acuerdo a la especificación PSS, existen dos características opcionales, que son persistencia
transparente y soporte transaccional. Si se provee persistencia transparente, se dice que el servicio es: "a
compliant Persistent State Service implementation with transparent persistence support", o una
implementación del servicio de estado persistente compatible con soporte de persistencia transparente.
Y si provee soporte transaccional es: “a compliant Persistent State Service implementation with
transaction support” o una implementación del servicio de estado persistente compatible con soporte de
transacciones. Mi servicio no provee ninguna de estas dos características adicionales, por lo que son
buenos candidatos de futuros trabajos.
Índice de Gráficos Pablo M. Ilardi
133
ÍNDICE DE GRÁFICOS
FUNCIONAMIENTO DEL SERVICIO DE ESTADO PERSISTENTE
Gráfico 1 - Interacción con la ORB _______________________________________________________ 33
Gráfico 2 - MODELO LÓGICO ___________________________________________________________ 34
Gráfico 3 - Sesión para acceder al DataStore _______________________________________________ 34
Gráfico 4 - Herencia Diamante de interfases en CORBA ______________________________________ 35
Gráfico 5 - Tipos y Modelo de Herencia en PSS _____________________________________________ 36
Gráfico 6 - Acceso al servicio ___________________________________________________________ 37
COMPILADOR
Compilador 1 - Proceso de Compilación ___________________________________________________ 53
Compilador 2 - Primer Paso ____________________________________________________________ 54
Compilador 3 - Javacc ________________________________________________________________ 56
Compilador 4 - Diagrama general del Compilador ___________________________________________ 58
Compilador 5 - Fases del compilador _____________________________________________________ 60
Compilador 6 - Nodos de árbol __________________________________________________________ 63
Compilador 7 - Primera Fase ___________________________________________________________ 64
Compilador 8 - Exepciones de Validación __________________________________________________ 68
Compilador 9 - Tres Partes _____________________________________________________________ 69
Compilador 10 - Qué generar ___________________________________________________________ 70
Compilador 11 - Generación de archivos __________________________________________________ 71
Compilador 12 - Cómo generarlo ________________________________________________________ 72
DISEÑO DEL SERVICIO DE ESTADO PERSISTENTE
Diseño del Servicio 1 - Configuración de la ORB _____________________________________________ 76
Diseño del Servicio 2 - Conectores _______________________________________________________ 78
Diseño del Servicio 3 - PSDLUtils_________________________________________________________ 79
Diseño del Servicio 4 - Parameters _______________________________________________________ 80
Diseño del Servicio 5 - Conector y Sesiones ________________________________________________ 82
Diseño del Servicio 6 - Modelo base de almacenes __________________________________________ 83
Diseño del Servicio 7 - DElegación _______________________________________________________ 84
Diseño del Servicio 8 - Objetos Alamacenados ______________________________________________ 85
Diseño del Servicio 9 - Restricciones de Claves ______________________________________________ 89
Diseño del Servicio 10 - Esquema General del Conector Temporal ______________________________ 90
Diseño del Servicio 11 - Generación de identificadores _______________________________________ 91
Diseño del Servicio 12 - Modelo de DB4O _________________________________________________ 96
Diseño del Servicio 13 - Modelo de consultas S.O.D.A ________________________________________ 98
Diseño del Servicio 14 - Configuración de DB4O ____________________________________________ 99
Diseño del Servicio 15 - Conexión para acceso a la base de datos ______________________________ 100
Diseño del Servicio 16 - Generación atomica de identificadores _______________________________ 102
Diseño del Servicio 17 - Esquema general del conector persistente _____________________________ 103
Diseño del Servicio 18 - Acceso remoto a los repositorios ____________________________________ 105
Diseño del Servicio 19 - Aplicación CORBA con accesso a PSS _________________________________ 114
COMPARACIÓN CON OTRAS IMPLEMENTACIONES DE PSS
Comparaciones 1 - Tabla comparativa___________________________________________________ 129
Glosario Pablo M. Ilardi
135
GLOSARIO POO – Programación Orientada a Objetos u OOP Object Oriented Programming.
API – Application programming interface o interfase de programación de aplicaciones
IDE – Integrated Development Environment o entorno de desarrollo integrado.
CORBA – Common Object Request Broker Arquitecture o Arquitectura Común para el Agente de Pedidos a
Objetos.
OMG – Object Management Group o Grupo de Administración de Objetos
OMA – Object Management Architecture o Arquitectura para la Administración de Objetos
ORB – Object Request Broker o Agente de Pedidos a Objetos
Servant – entidad programada en un lenguaje, que implementa uno o más objetos CORBA. Se dice que
los servants, encarnan los objetos, porque proveen los cuerpos o implementaciones de los mismos. Los
servants, existen dentro del contexto de una aplicación servidora. Dentro de un lenguaje de
programación orientado a objetos, se trata de una instancia de un tipo de objeto.
IDL – Interface Definition Language o Lenguaje de Definición de Interfases. Lenguaje genérico de
definiciones utilizado por CORBA para permitir interconectar objetos implementados en diferentes
interfases.
PSS – Persistent State Service o Servicio de Estado Persistente, servicio de CORBA para permitir almacenar
objetos en forma persistente para los servants.
RDBMS – Relational Dabase Management System o Sistema de Administración de Base de Datos
Relacional.
SQL – Structured Query Language o Lenguaje Estructurado de Consultas utilizado para realizar consultas
en bases de datos Relacionales.
SQL3 – Extensión al ANSI-SQL también llamado SQL 1999 que introdujo conceptos de los lenguajes
orientados a objetos.
OODBMS – Object Oriented Dabase Management System o Sistemas de Administración de Bases de
Datos Orientadas a Objetos
TS – Transaction Service o Servicio de Transacciones de CORBA para realizar operaciones en contextos
trasaccionales.
PSDL – Persistent State Definition Language o Lenguaje de Definiciones de Estado Persistente
Bytecode – Código de bytes utilizado internamente por la máquina virtual de Java.
Sistema Distribuido – conjunto de computadoras independientes que se presentan al usuario del sistema
cómo una única computadora. Desde la perspectiva de hardware, las máquinas o computadoras son
autónomas, y pero desde el punto de vista del software, el sistema se ve por el usuario como un todo.
BOA – Basic Object Adapter o Adaptador Básico de Objetos, especificación descontinuada de CORBA que
permitía invocar operaciones remotas definidas en lenguaje C ubicadas en diferentes ORBs.
Glosario Pablo M. Ilardi
136
POA – Portable Object Adapter o Adaptador Portable de Objetos, especificación de CORBA que sucedió a
BOA y permitió ejecutar operaciones remotas ubicadas en diferentes ORBs e implementadas en cualquier
lenguaje.
OR - Object Reference o Referencia a Objeto, identifica un objeto CORBA, en forma unívoca. Le permite a
CORBA, identificar, ubicar y direccionar al objeto. Para los clientes, son entidades opacas que no pueden
ser modificadas ni creadas por ellos, las utilizan para dirigir los pedidos a los objetos. Identifican un único
objeto CORBA.
GIOP – General Inter ORB Protocol o Protocolo General Inter ORB, definición abstracta de un protocolo
que permitió interconectar diferentes implementaciones de la ORB.
TCP/IP – Transmission Control Protocol / Internet Protocolo, conjunto de protocolos, el primero sobre la
capa de transporte y el segundo sobre la capa de red, utilizados para comunicarse en Internet y la
mayoría de las redes comerciales.
IIOP – Internet Inter ORB Protocol o Protocolo Inter ORB sobre Internet, implementación de GIOP
mapeada sobre TCP.
IOR – Interoperable Object Reference o Referencia Interoperable a Objeto, referencia a objeto que es
entendible por todas las implementaciones de GIOP.
CCM – CORBA Component Model o Modelo de Componentes de CORBA, nueva especificación de CORBA
que promueve la construcción de sistemas mediante un modelo de componentes que extiende a las
interfases de CORBA.
RFP – Request For Proposal o Pedido de Propuesta, consiste en el llamado a entidades a participar en la
creación de una nueva especificación.
POS – Persistent Object Service o Servicio de Objetos Persistentes, especificación descontinuada de
CORBA que fue reemplazada por PSS, utilizada para permitir persistir objetos en el entorno de una ORB.
StorageObject – (PSS) Objeto almacenado, todo objeto que esté definido en PSDL se define como "objeto
almacenado".
StorageHome – (PSS) Almacén de PSDL donde se "almacenan" objetos de una familia definida por un tipo
base.
Singleton – Patrón de diseño que define a un objeto del que sólo puede existir una única instancia en
forma simultánea.
Connector – (PSS) Interfase que representa la implementación del servicio de persistencia, mediante ella
se realizan las operaciones iníciales sobre el servicio. Se obtiene una instancia invocando a un método del
registro que devuelva la implementación según el tipo de conector solicitado.
ConnectorRegistry – (PSS) "interfase/clase" que representa el registro de conectores de PSS, permite
obtener referencias a las implementaciones del servicio de persistencia (Conectores). El registro se
obtiene llamando al método resolve_initial_references(PSS) de la ORB.
CatalogBase – (PSS) Es un repositorio donde se encuentran los almacenes y objetos persistidos,
técnicamente representa lo mismo que una sesión. Esta clase es abstracta, existen dos extensiones a la
misma, que son una sesión y un pool de sesiones. Toda instancia de home, sabe a qué catalogo
pertenece.
Glosario Pablo M. Ilardi
137
TransactionalSession – (PSS) Implementación transaccional de una sesión.
SessionPool – (PSS) Implementación alternativa de CatalogBase, cuando se requiere trabajar con Pools
de conexiones.
CosPersistentState – (PSS) es el nombre del módulo (en C++ namespace) de servicio de estado
persistente.
NotFound – (PSS) es una excepción que es lanzada cuando se realiza una búsqueda para la que no se
obtiene el resultado esperado.
StorageType – (PSS) es un string que define el tipo persistido, es la puerta de entrada al conector, para
obtener referencias a almacenes.
Pid – (PSS) el pid de un objeto almacenado identifica unívocamente a un objeto dentro de un repositorio.
ShortPid – (PSS) el short pid de cualquier objeto almacenado, identifica unívocamente a cualquier objeto
dentro de un mismo almacén.
Factory Method – Patrón de diseño que permite centralizar la creación de objetos en un método en
función de los parámetros y estado del objeto que implemente el método.
JVMTI – JVM Tool Interface o Interfase de Herramientas para la Máquina Virtual, que permite configurar
agentes que podrán interceptar puntos de control utilizados para la creación de objetos y carga de las
clases.
BNF – Backus-Naur form, notación para la definición gramatical de lenguajes.
EBNF – Extended Backus-Naur form, extensión a la notación BNF que simplifica las definiciones recursivas.
BSD – Berkely Software Distribution licencia de uso de software de tipo libre.
Backtracking – Algoritmo que permite encontrar una solución a un problema evaluando todas las posibles
soluciones.
Javacc – Java Compile Compiler.
JJTree – Pre procesador de Javacc utilizado para la generación de árboles.
Builder – Patrón de diseño que permite delegar en un objeto la construcción de otro objeto encapsulando
los detalles de la construcción en el builder.
Template Method – Patrón de diseño que permite generalizar un algoritmo o comportamiento en un
método de un objeto base, para luego ser adaptado o personalizado por subclases.
Marker Interface – Patrón de diseño que permite marcar clases para definir que la misma cumple o no
con una característica.
ORM – Object Relation Mapping o Mapeo de Objeto - Relación, definición que se adapta a toda técnica
de mapeo de objetos en un modelo relacional.
JDO – Java Database Object, API de SUN para mapear un modelo de objetos en lenguaje Java a bases de
datos relacionales.
J2EE – Java2 Enterprise Edition o Edición empresarial de Java2.
Glosario Pablo M. Ilardi
138
DDL – Data Definition Language o Lenguaje de Definición de Datos utilizado en las bases de datos
Relacionales.
DB4O – Dabase For Objects o Base de Datos para Objetos
SODA – Simple Object Data Base Access o Acceso Simple a Bases de Datos de Objetos.
JacORB – Implementación en Java de la ORB del Departamento de Ciencias de la Computación de la
Universidad de Freie en Berlín, Alemania.