6. Construcción de aplicaciones CORBA

Para ilustrar la construcción de aplicaciones mediante CORBA utilizaremos un ejemplo sencillo de reserva de habitaciones. El desarrollo de una aplicación incluye la especificación de la interfaz, la implementación de los objetos, la implementación del servidor, la implementación de una factory, el inicio de los servidores y de los clientes como aplicaciones y como applets.

6.1. Especificación de la aplicación

El sistema de reserva de habitaciones permite tanto la reserva de habitaciones como la cancelación de las mismas. Opera sobre una franja horaria de 9 A.M. hasta las 4 P.M. con unidades de una hora (no se consideran dias ni semanas). Las habitaciones disponibles no son fijas, el número y nombres de las habitaciones puede cambiar. Cuando se reserva una habitación se debe indicar el nombre y el propósito de la persona que la reserva. Todos los objetos creados no tienen un estado persistente. Tampoco vamos a considerar cuestiones de seguridad y cualquiera puede cancelar cualquier reserva.

Vamos a tener en cuenta las siguientes cuestiones sobre el diseño:

La figura 6.1 ilustra la configuración de nuestro sistema de reservas. Hay tres servidores de habitaciones (room servers), que tienen una implementación de objeto Room. También hay un servidor Meeting Factory que crea un objeto Meeting Factory. La Meeting Factory crea varios objetos Meeting que utilizan en mismo espacio de direcciones, y que se corresponden con una máquina virtual Java (en el caso de Java). Un servicio Naming tiene varios objetos Naming Context formando un árbol de contexto. Las implementaciones de los objetos Room y Meeting Factory se registran en el servicio de nombres (Naming service).

Figura 6.1. Sistema de reserva de habitaciones.

La especificación IDL contiene las especificaciones para Meeting, MeetingFactory, y Room. La interfaz Meeting tiene dos atributos: purpose y participants. Los atributos describen la semántica de una reserva. La interfaz tiene una operación destroy() para completar su ciclo de vida.

Los objetos reserva (meeting) son creados en tiempo de ejecución por la Meeting Factory. Proporciona una única operación, createMeeting(), cuyos parámetros se corresponden con los atributos del objeto Meeting y devuelve una referencia de objeto al nuevo objeto Meeting creado.

module roomBooking {

    interface Meeting {
    
        // Una reserva tiene dos atributos de solo lectura que describen
        // el propósito y los participantes en dicha reserva
    
        readonly attribute string purpose;
        readonly attribute string participants;

        oneway void destroy();
    };
    
    interface MeetingFactory {
    
        // Una meeting factory crea objetos reserva
    
        Meeting createMeeting( in string purpose, in string participants);
    };
    
    interface Room {
    
        // Una habitación proporciona operaciones para mostrar, realizar y 
        // cancelar reservas
        // Realizar una reserva implica asociar una reserva con un slot 
        // de tiempo (para dicha habitación particular).
    
        // Las reservas pueden realizarse en horario de oficina
        // Por simplicidad disponemos de 8 slots en los que pueden
        // realizarse las reservas
    
        enum Slot { am9, am10, am11, pm12, pm1, pm2, pm3, pm4 };

        // ya que IDL does no proporciona la forma de determinar la
        // cardinalidad de un enum, defnimos la constante MaxSlots
    
        const short MaxSlots = 8;
    
        // Las reservas asocian todas las reservas (de un dia) 
        // con slots temporales para cada habitación 
    
        typedef Meeting Meetings[ MaxSlots ];
    
        exception NoMeetingInThisSlot {};
        exception SlotAlreadyTaken {};
    
        // El atributo name da nombre a una habitación
    
        readonly attribute string name;
    
        // view devuelve las reservas de una habitación
        // Por simplicidad, la implementación maneja solamente 
        // reservas para un día
    
        Meetings view ();
    
        void book( in Slot a_slot, in Meeting  a_meeting )
            raises(SlotAlreadyTaken);
    
        void cancelBooking( in Slot  a_slot )
            raises(NoMeetingInThisSlot);
};

Para la interfaz Room, definimos algunos tipos y constantes, junto con dos excepciones: NoMeetingInThisSlot y SlotAlreadyTaken. Hay tres operaciones definidas: view() devuelve Meetings (vector de objetos Meeting). Una referencia a objeto nil significa que el slot correspondiente está libre. La operación book() realiza una reserva en el slot a_slot del objeto Room sobre el que se invoca la operación. La operación cancelBooking() elimina la reserva del slot a_slot.

Para compilar la especificación IDL, asumiendo que la especificación se llama RoomBooking.idl utilizamos el compilador idlj:

idlj -fall RoomBooking.idl

Se generan los siguientes ficheros con interfaces java:

También se generan clases holder. Entre otros métodos, las clases helper contienen los métodos narrow() para cada interfaz:

Se generan las clases stub y skeleton para cada interfaz:

Adicionalmente, hay clases para los tipos de datos y las excepciones definidas en la interface IDL.

6.2. Implementación de los objetos

Las clases que tenemos que implementar utilizan las interfaces Meeting y Room. Para ello extienden las clases POA skeleton generadas.

Implementación del objeto Meeting

import org.omg.CORBA.*;


class MeetingImpl 
    extends MeetingPOA {

    private String purpose;
    private String participants;

    /** constructor */

    MeetingImpl( String purpose, String participants) 
    {
	 // initialise private variables
	 this.purpose = purpose;
	 this.participants = participants;
    }
  
    // attributes
    public String purpose() {
	  return purpose;
    }

    public String participants() {
	return participants;
    }


    /** deactivates the object */
    public void destroy() 
    {
	  try
	  {
	    _poa().deactivate_object( _poa().servant_to_id(this));
	  }
	  catch( Exception e )
  	  {
	    // ignore
	  }
   }
}

El método destroy() desactiva el objeto para que puede ser "recogido" por el recolector de basuras.

El método _poa() se hereda de la clase base skeleton org.omg.PortableServer.Servant. Recupera la instancia POA del contexto de ejecución actual que es responsable de este objeto CORBA.

Implementación del objeto Room

import org.omg.CORBA.*;


public class RoomImpl extends RoomPOA 
{

    private String name;
    private Meeting[] meetings;

    // constructor
    public RoomImpl( String name ) 
    {
        this.name = name;
        meetings = new Meeting[ Room.MaxSlots ];
    }
    
    // attributes
    public String name() 
    {
        return name; 
    }
        
    // operations
    public Meeting[] view() 
    {
        return meetings;
    }

    public void book( Slot slot,
                      Meeting meeting )
        throws SlotAlreadyTaken 
    {
 
        if( meetings[ slot.value() ] == null ) 
        {
            meetings[ slot.value() ] = meeting;
        }
        else 
        {
            throw new SlotAlreadyTaken();
        }        
        return;
    }

    public void cancelBooking( Slot slot )
        throws NoMeetingInThisSlot 
    {

        System.err.println("cancel " + slot );
        if( meetings[slot.value()] != null  ) {
            meetings[slot.value()].destroy();
            meetings[slot.value()] = null;
        }
        else 
        {
            throw new NoMeetingInThisSlot();
        }
    }
}

La variable name almacena el nombre del objeto Room, y meetings contiene el vector de habitaciones reservadas.

Implementamos los métodos name(), view(), book(), y cancelBooking(). El método book() comprueba si el slot está vacío, en cuyo caso asignamos la reserva a dicho slot; en otro caso, lanzamos la excepción SlotAlreadyTaken. La clase de dicha excepción se define en el paquete RoomPackage debido a que la correspondiente excepción IDL se define en la interfaz Room.

6.3. Construcción de los servidores

Para instanciar las implementaciones de los objetos y hacer que estén disponibles para los clientes debemos implementar un servidor. Dicho servidor se ejecuta como una tarea (o proceso) del sistema operativo. En el caso de Java se trata de una JVM en la que las instancias de los objetos se ejecutan. Puede habar un servidor por objeto, o un servidor puede manejar múltiples objetos. Un servidor tiene cuatro tareas fundamentales:

Servidores de tareas adicionales pueden incluir el registro de los objetos con el servicio Naming o el servicio Trading.

El servidor RoomServer lleva a cabo estas cuatro tareas fundamentales y registra cada nueva habitación creada en el servicio Naming. Para ello se define una clase RoomServer y se implementa su método main(). Definimos dos strings que se usan cuando registramos el objeto Room en el servicio Naming. A continuación chequeamos que el número de argumentos sea correcto y abandonamos el programa en caso de que no sea así. Se espera un argumento que determine el nombre del objeto Room.

Para usar el servicio Naming de forma exitosa, los objetos que queremos que compartan información via dicho servicio tienen que utilizar una misma convención de nombrado. Para este ejemplo utilizaremos la siguiente convención, ilustrada en la Figura 6.2: debajo del contexto raiz tenemos un contexto "Building Applications", que contiene un contexto denominado "Rooms" y el objeto Meeting Factory en el contexto "Building Applications". Siguiendo esta convención aseguramos que los clientes pueden localizar a los objetos apropiados.

Figura 6.2. Convención de nombrado.

De acuerdo con esta convención de nombres inicicializamos la variable context_name con su versión en forma de cadena de caraceres el nombre de contexto Room.

import java.io.*;
import org.omg.CORBA.*;
import org.omg.PortableServer.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
 
public class RoomServer 
{
    public static void main(String[] args) 
    {
        String context_name, str_name;

        if( args.length != 1 ) {
            System.out.println("Usage: vbj RoomServer room_name");
            System.exit( 1 );
	}

        context_name = new String("BuildingApplications/Rooms");

        try {
	    //init
	    ORB orb = ORB.init( args, null );
	    POA poa = POAHelper.narrow( orb.resolve_initial_references( "RootPOA"));
	    poa.the_POAManager().activate();

	    // create the Room object and
	    // export the object reference
	    org.omg.CORBA.Object room_obj = 
		poa.servant_to_reference( new RoomImpl( args[0] ) );

            // register with naming service
            str_name = context_name + "/" + args[0];

	    NamingContextExt root = 
		NamingContextExtHelper.narrow( 
			 orb.resolve_initial_references("NameService")
			 );

	    try
	    {
		// make sure the "rooms" context is  bound
		root.bind_new_context( root.to_name( context_name ));
	    }
	    catch( AlreadyBound ab )
	    { 
		// does not matter .
	    }
	    catch( NotFound nf )
	    {
                System.err.println("Context " + context_name + " not found, 
                     start MeetingFactoryServer first.");
                System.err.println("exiting ...");
            }

            root.bind( root.to_name( str_name), room_obj );

	    // wait for requests
	    orb.run();
        }
	catch( AlreadyBound already_bound ) {
	    System.err.println("Room " + context_name + args[0] +
                " already bound.");
	    System.err.println("exiting ...");
        }
	catch(UserException ue) {
            ue.printStackTrace();
        }
	catch(SystemException se) {
	    System.err.println(se);
        }
    }
}

Para inicializar el ORB y obtener una referencia a POA llamamos al método de clase init() de la clase CORBA.ORB, y a resolve_initial_references("RootPOA") sobre el pseudo-objeto orb.

La segunda tarea es crear el objeto Room. La tercera tarea consiste en activar el servant como una réplica del objeto Room en el RootPOA. La ctivación del objeto se realiza de forma implícita (como efecto lateral resultante de crear al referencia al objeto para el objeto Room).

El siguiente paso es registrar el objeto en el servicio Naming. Primero obtenemos una referencia a un contexto inicial de un servicio Naming mediante los mecanismos proporcionados por ORB. El servicio Naming maneja nombres incluyendo contextos de forma similar a la notación de nombres de ficheros en varios sistemas operativos:

/<context1>/<context2>/.../<contextn>/name

La cuarta tarea del servidor es crear un bucle para "despachar" invocaciones mediante la llamada al método run() del ORB.

6.4. Construcción de las Factories

Una factory es una implementación de objeto con un particular patrón de diseño. La diferencia con los objetos ordinarios es que las factories proporcionan métodos para crear de forma dinámica nuevos objetos. Realizan la misma inicialización de objetos nuevos que el método main() de un servidor. El proceso de la construcción de factories está formado por los mismos pasos que la construcción de cualquier otro servidor: implementación del objeto e implementación del servidor.

Implementación de Meeting Factory

La implementación de Meeting Factory, la clase MeetingFactoryImpl, es una extensión de la correspondiente clase skeleton MeetingFactoryPOA.

import org.omg.CORBA.*;

class MeetingFactoryImpl 
    extends MeetingFactoryPOA 
{
//      // constructor
//      MeetingFactoryImpl(ORB orb) 
//      {
//  	this.orb = orb;	
//      }
  
    // method
    public Meeting createMeeting(String purpose, String participants ) 
    {
        MeetingImpl meetingImpl = new MeetingImpl(purpose, participants);
	try
	{
	    org.omg.CORBA.Object obj = _poa().servant_to_reference(meetingImpl);
	    Meeting meeting = MeetingHelper.narrow(obj);
	    return meeting; 
	}
	catch( SystemException se )
	{
	}
	catch( UserException ue )
	{}
	return null;
    }
}

El único método de la Meeting Factory, createMeeting(), tiene como parámetros los correspondientes al constructor del objeto Meeting, MeetingImpl().

Implementación de Meeting Factory Server

El servidor de la Factory Meeting sigue el mismo patrón que el servidor Room. Inicializamos el ORB y POA, creamos el objeto Meeting Factory, y lo activamos.

import java.io.*;
import org.omg.CORBA.*;
import org.omg.PortableServer.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
 
public class MeetingFactoryServer 
{

    public static void main(String[] args) 
    {
        String str_name, context_name;

        if( args.length != 0 ) {
	    System.out.println("Usage: java MeetingFactoryServer");
	    System.exit( 1 );
	}

	// the stringified names we want to use
	context_name = "BuildingApplications";
        str_name = context_name + "/MeetingFactory";

        try {
	    //initialise ORB
	    ORB orb = ORB.init( args, null );

            // initialise and activate POA
	    POA poa = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
	    poa.the_POAManager().activate();

	    // create the MeetingFactory object
	    MeetingFactoryImpl meeting_factory = new MeetingFactoryImpl();

	    // export the object reference
	    org.omg.CORBA.Object meeting_factory_obj = 
		poa.servant_to_reference(meeting_factory);

            // register with the CORBA Naming Service
	    NamingContextExt root = 
		NamingContextExtHelper.narrow( 
			 orb.resolve_initial_references("NameService")
			 );

	    try
	    {
		// make sure our context exists, if the 
		// name server has been running for a while
		// someone may have already set up the context earlier
		root.bind_new_context( root.to_name( context_name ));
	    }
	    catch( AlreadyBound ab )
	    { 
		// ok, we ignore that
	    }

	    // bind our new meeting factory
	    try
	    {
		root.bind( root.to_name(str_name), meeting_factory_obj );
	    }
	    catch( AlreadyBound ab )
	    { 
		System.err.println("A meeting factory is already bound, exiting...");
		System.exit(1);
	    }
	    // enter event loop
	    orb.run();
        }
	catch(Exception e) 
	{
	    e.printStackTrace();
        }
    }
}

Finalmente llamamos a orb.run() para esperar la llegada de invocaciones y capturamos las excepciones.

6.5. Inicio de los servidores

Para iniciar los servidores se requieren los siguientes pasos:

6.6. Construcción de los clientes

Los clientes pueden implementarse como aplicaciones Java o applets. Las diferencias entre estos dos tipos de clientes son:

Un cliente como un Applet

Lo primero que debemos hacer es escribir la página HTML que lo contiene.

<html><header>
<title>
Room Booking Applet
</title>
<body>
<center>
<h1>
Room Booking Applet
<h1>
<applet
   code = ClientApplet.class width=600 height=300>
</applet>
</center>
</body></html>

A continuación mostramos el código del applet:

import java.awt.*;


public class ClientApplet
    extends java.applet.Applet 
{
    private RoomBookingClient client;

    /** override init method of Class Applet */
    public void init() 
    {
        // create a RoomBookingClient client -
        // using the applet constructor
        client = new RoomBookingClient( this );

        // initialiase the GUI
        client.init_GUI( this );

        // initialise the Naming Service
        client.init_from_ns();

        // view existing bookings
        client.view();
    }
}

Inicializamos la interfaz gráfica con el método init_GUI() sobre el objeto client. Comenzamos llamando al método init_from_ns() sobre el objeto client, que contiene las referencias a la Meeting Factory y Room del servicio Naming. Entonces invocamos al método view(), que obtiene las habitaciones disponibles del servicio Naming e invoca la operación view() sobre cada uno de dichos objetos Room.

Un cliente como una aplicación

Implementamos la clase clientApplication que extiende la clase java.awt.Frame. También implementamos el método main() de la clase, que es similar al método init() de la clase applet.

import java.awt.*;
import java.awt.event.*;
import org.omg.CORBA.*;
 
public class ClientApplication
    extends Frame
    implements WindowListener 
{

    private static RoomBookingClient client;

    /** constructor */
    ClientApplication() {
        super( "Room Booking System" );
        addWindowListener( this );
        setSize( 350, 250 );
    }

    public void windowActivated(WindowEvent e) {}
    public void windowClosed(WindowEvent e) 
    {
	System.exit(0);
    }
    public void windowClosing(WindowEvent e) {}
    public void windowDeactivated(WindowEvent e) {}
    public void windowDeiconified(WindowEvent e) {}
    public void windowIconified(WindowEvent e) {}
    public void windowOpened(WindowEvent e) {}

    /** override to reposition frame (for appearances sake only) */
    public synchronized void setVisible(boolean vis) 
    {
        if(vis == true) {
            setLocation(50, 50);
        }
            super.setVisible(vis);
    }

    public static void main(String args[]) 
    {
        // create an object of its own class
        ClientApplication gui =
            new ClientApplication();

        gui.setVisible(true);

        // create a RoomBookingClient object -
        // using the application constructor
        client = new RoomBookingClient();

        // initialise the GUI
        client.init_GUI( gui );

        // initialise the Naming Service
        client.init_from_ns();

        // view existing bookings
        client.view();
    }
}

Código independiente del tipo de cliente

A continuación explicamos el código cliente que es independiente de los detalles del applet o aplicación, es decir, el código cliente que realiza la llamada a las implementaciones de los objetos.

La clase RoomBookingClient implementa los siguientes métodos: