3. Java IDL

IDL (Interface Definition Language) es un lenguaje declarativo utilizado para definir las interfaces CORBA. Para hacer uso de los servidores CORBA es necesario invocar métodos de sus interfaces IDL.

Tal y como ya se ha comentado en el tema 1, se trata de un lenguaje que no contiene sentencias de programación, ya que su propósito es únicamente definir las signaturas de las interfaces.

Existen mappings estándar para convertir interfaces IDL en clases C++, código C, y clases Java, entre otros. Estas clases generadas usan el marco CORBA subyacente para comunicarse con los clientes remotes y proporcionan las bases para implementar y exportar nuestros propios objetos distribuidos. Java IDL es una implementación del estándar IDL-to-Java mapping y es proporcionado por Sun con es estándar Java SDK en los paquetes org.omg.CORBA y org.omg.CosNaming y sus subpaquetes.

Al igual que RMI, Java IDL permite acceder a objetos remotos a través de la red. También proporciona las herramientas necesarias para hacer que los objetos Java sean también accesibles a otros clientes CORBA. Si exportamos una clase usando Java IDL, se puede crear una instancia de esa clase y publicarla a través del servicio de nombres. Un cliente remoto puede entontrar dicho objeto, llamar a sus métodos, y recibir datos desde ellos, igual que si se estuviesen ejecutando en la máquina local del cliente. Sin embargo, a diferencia de RMI, los objetos que son exportados usando CORBA pueden ser accedeidos por clientes implementados en cualquier lenguaje que tenga asociado un IDL binding (C, C++, Ada, etc).

Las principales diferencias entre IDL y Java son:

Todas las declaraciones hechas con IDL están disponibles mediante el repositorio de interfaz (Interfaz Repository: IR). El IR es un servicio de CORBA que proporciona información en tiempo de ejecución sobre los tipos de interfaces de objetos soportados en una instalación de ORB particular. Puede verse como un conjunto de objetos que encapsulan las definiciones IDL de todos los tipos CORBA disponibles en un dominio particular.

3.1. Módulos e Interfaces

Los módulos se declaran en IDL usando la palabra reservada module, seguida de un nombre para el módulo y una llave de apertura inicia el ámbito del módulo. Cada elemento definido dentro del ámbito del módulo (interfaces, constantes, otros módulos) pertenece a dicho módulo y es referenciado en otros módulos IDL usando la sintaxis module-name::x. Supongamos que se quiere que determinadas clases estén contenidas en un módulo llamado corba, que es parte de un módulo mayor llamado jen. En IDL esto se declara como sigue:

//IDL
module jen {
  module corba {
    interface NeatExample ...;
  };
};

Si se quiere referenciar la interfaz NeatExample en otros ficheros IDL se usa la sintaxis jen::corba::netaExample. Cabe destacar los punto y coma después de las llaves de cierre de las definiciones del módulo, que son requeridos en IDL, pero no en Java. También se requiere un punto y coma después de una definición de interfaz.

Las interfaces declaradas en IDL son convertidas en clases o interfaces en Java. Como se ha mencionado anteriormente, IDL se usa solamente para declarar módulos, interfaces y sus métodos. Los métodos de las interfaces IDL se dejan siempre abstractos, para ser definidos en el lenguaje de programación que se quiera usar para implementar la interfaz.

La declaración de una interfaz include una cabecera de interfaz y un cuerpo de interfaz. La cabecera especifica el nombre de la interfaz y las interfaces de la cual es heredera (si hay alguna). Un ejemplo de cabecera de interfaz IDL es:

interface PrintServer : Server { ...

Esta cabecera comienza con la declaración de una interfaz llamada PrintServer que hereda todos los métodos y miembros de datos de la interfaz Server. Una interfaz IDL puede heredar de múltiples interfaces; simplemente hay que separar los nombre de interfaces con comas en la parte de herencia de la cabecera.

Los módulos pueden anidarse dentro de otros módulos, y sus contenidos pueden ser referenciados de forma relativa a su ámito de nombres en el que se encuentran. Por ejemplo:

module outer {
  module inner { //módulo anidado
    interface inside{};
  };
  interface outside {//puede referirse a su clase anidada con su nombre local
    inner::inside get_inside();
  };
};

La operación get_inside() devuelve una referencia a objeto de tipo ::outer::inner::inside, pero puede usar la forma relativa del nombre debido a su posición en el mismo ámbito que el módulo interno.

Las interfaces pueden referenciarse mutuamente. Es decir, las declaraciones en una interfaz pueden usar el nombre de otra como tipo de objeto. Para evitar errores de compilación, un tipo de interfa debe ser declarado por anticipado antes de ser usado.

interface A; //declaración forward
interface B {//B puede usar A como nombre de tipo
  A get_an_A();
};
interface A {
  B get_an_B();
};

Cuando una declaración en un módulo necesita alguna referencia mutua en una declaración en otro módulo, se puede cerrar el primer módulo y "reabrirlo" después de algunas otras declaraciones.

module X {
  interface A; //declaración forward
};
module Y {
  interface B {
    X::A get_an_A();
  };
};
module X {
  interface C {
    A get_an_A();
  };
  interface A {
    Y::B get_a_B();
  };
};

3.2 Miembros de datos y métodos

El cuerpo de la interfaz declara todos los miembros de datos (o atributos) y métodos de una interfaz. Los miembros de datos se declaran usando la palabra clave attribute. Como mínimo, la declaración incluye un nombre y un tipo. Las declaraciones pueden especificar opcionalmente si el atributo es de solo lectura o no, usando la palabra reservada readonly. Por defecto, cada atributo que se declara se puede leer y escribir (para Java esto significa que el compilador IDL genera métodos públicos de lectura y escritura para dicho atributo). Un ejemplo de declaración de atributo string de sólo lectura es:

readonly attribute string myString;

Un método se declara especificando su nombre, tipo de resultado y parámetros, como mínimo. Opcionalmente se pueden declarar excepciones que el método prodría provocar, la semántica de invocación del método, y el contexto para el método de llamada. Una declaración para un metódo sencillo que devuelve un string podría ser:

string parseString(in string buffer);

esto declara un método llamado parseString() que acepta un string como argumento y devuelve un valor string.

A continuación mostramos un ejemplo completo de IDL.

module OS {
  module services {
    interface Server {
      readonly attribute string serverName;
      boolean init (in string sName);
    };

    
    interface Printable {
      boolean print(in string header);
    };


    interface PrintServer: Server {
      boolean printThis(in Printable p);
    };
  };
};

La primera interfaz, Server, tiene un único atributo string de solo lectura y un método init() que acepta un string y devuelve un boolean. La interfaz Printable tiene un único método print() que acepta una cabecera string. Finalmente, la interfaz PrintServer "extiende" la interfaz Server (y por lo tanto hereda todos sus métodos y atributos) y añade un método printThis que acepta un objeto Printable y devuelve un boolean. En todos los casos hemos declarado los argumentos de los métodos solamente de entrada (es decir, se pasan por valor), usando la palabra reservada in.

3.3. Constantes y tipos

El nombre de cualquier interfaz declarada en IDL constityue un nombre de tipo de objeto que puede usarse como el tipo de cualquier parámetro de operación o valor de retorno, o como un miembro en una declaración de tipo estructurado. IDL proporciona un conjunto de tipos básicos para representar números, strings, caracteres, y booleanos. Las definiciones de los mismos son bastante precisas para permitir codificaciones ambiguas. Los tipos estructurados en IDL son las estructuras, uniones con discriminante, arrays, y secuencias. Las excepciones pueden considerarse un caso especial de estructuras que pueden usarse solamente en cláusulas raise de las operaciones.

Los tipos básicos proporcionados por IDL son:

Se puede utilizar la palabra reservada typedef para crear tipos de datos. En el caso de tipos "plantilla" (tipos que requieren un parámetro para determinar su longitud o contenidos) se requiere un typedef antes del tipo que puede usarse en una declaración de atributo o de operación.

Los strings pueden ser limitados o ilimitados. Los tipos string limitados son tipos "plantilla", indicándose su longitud máxima como parámetro.

interface StringProcessor {
   typedef octstring string <8>;
   typedef centastring string <100>;
   //...
   octstring MiddleEight(in string str);
   centastring PadOctString(in octstring ostr, char pad_char);
};

Los tipos de punto fijo también son tipos "plantilla" y deben ser introducidos con un typedef, especificando el número de dígidos decimales y la escala, expresada como potencia de 10. Así por ejemplo para representar el número 123.45, se indicaría como un número con 5 dígitos, y una escala de 2.

typedef fixed <5,2> three_point_two;

Los tipos enumerados se declaran con un nombre, y una coma separa la lista de identificadores.

enum glass_color {gc_clear, gc_red, gc_blue, gc_green} 

Las estructuras se declaran con la palabra reservada struct, seguida de un nombre.

interface HardwareStore {
   struct window_spec {
     glass_color color;
     height float;
     width float;

};

Las uniones con discriminante se declaran con la palabra reservada union, seguida de un nombre y de la palabra reservada switch, y parametrizada por un tipo escalar (integer, char, Boolean o enum) que actúa como discriminante.

enum fitting_kind {door_k, window_k, shelf_k, cupboard_k};
union fitting switch (fitting_kind) {
  case door_k,
    cupboard_k: door_spec door;
  case window_k: window_spec win;
  default: float width;
};

Las secuencias son tipos plantilla. Una secuencia es una colección ordenada de items que puede crecer en tiempo de ejecución. Sus elementos son accedidos mediante un índice. Las secuencias pueden ser limitadas o no. Todas las secuencias tienen dos características en tiempo de ejecución: su longitud máxima y la actual. La longitud máxima de las secuencias limitadas se determina en tiempo de compilación. La ventaja de las secuencias es que solamente se pasa a un objeto remoto el número de elementos actuales de la secuencia cuando se utiliza una secuencia como argumento.

//union type fitting declarada anteriormente
typedef sequence <fitting> HardwareOrderSeq;
typedef sequence <fitting,10> LimitedHWOrderSeq;
typedef sequence <sequence <<fitting>,3> ThreeStoreHWOrderSeq;
typedef sequence <sequence <fitting> ManyStoreHWOrderSeq;
  default: float width;
};

Los arrays también se declaran normalmente con un typedef. Los arrays tendrán una longitud fija en tiempo de ejecución. Independientemente de su longitud y contenido, el array completo será codificado y enviado por la red.

typedef window WindowVec10[10];
typedef fitting FittingGrid[3][10];
struct bathroom {
  float width;
  float length;
  float heigth;
  boolean has_toilet;
  fitting fittings[6];

};

Las excepciones se declaran de la misma forma que las estructuras, usando la palabra reservada exception, en lugar de struct.

exception OrderTooLarge {
  long max_items;
  long num_items_submitted;
};
exception ColorMismatch {
  sequence <color> other_window_colors;
  color  color_submitted;
};

Las constantes pueden declararse con un ámbito global o dentro de los módulos e interfaces. Se utiliza la palabra reservada const.

const short max_storage_bays = 200; 
const windows_per_bay=45;
const long max_windows = max_storage_bays * windows_per_bay;
const string initial_quote= "fox in socks on knox on bloks";
const HarcwareStore::CashAmount balance = (max_storage_bays -3)/1.45;

3.4. Operaciones y atributos

Las declaraciones de las operaciones contienen un nombre de operación, un tipo de retorno (o void para indicar que no se espera ningún valor), y una lista de parámetros, que puede ser vacía. Una operación puede tener cláusulas raise, para indicar las excepciones.

typedef float CashAmount;  
typedef sequence <window_spec> WindowSeq;
CashAmount OrderFittings(in HardwareOrderSeq order)
  raises (OrderTooLarge);
void OrderWindows(
  in WindowSeq order,
  in CashAmount willing_to_pay,
  out CashAmount total_price,
  out short  order_number)
  raises (OrderTooLarge, ColorMismatch);

El modo de paso de parámetros es por valor para los tipos básicos y para estructuras, uniones y enumeraciones. Los argumentos de tipo objeto se pasan siempre por referencia.

Un atributo es equivalente lógicamente a un par de funciones de acceso, una para acceder al valor, y otra para modificarlo. Los atributos de solo lectura solamente requieren una función de acceso. Los atributos consisten en la palabra reservada attribute seguida del tipo del atributo y de una lista de nombres de atributos. Pueden ir precedidos de la palabra reservada readonly.

readonly attribute CashAmount min_order, max_order;
readonly attribute FittingSeq new_fittings; 
attribute string quote_of_the_day;  

Los atributos previos pueden reemplazarse por el siguiente IDL:

CashAmount min_order();
CashAmount max_order();
FittingSeq new_fittings(); 
string get_quote_of_the_day(); 
void set_quote_of_the_day(in string quote);  

Tal y como están declaradas, las operaciones y los atributos son equivalentes.

3.5. Valuetypes

Los valuetypes son un tipo especial de datos en IDL introducidos para permitir que las entidades fueran pasadas por valor en lugar de por referencia en una invocación de operación, para que de esta forma el acceso al valor fuese siempre una operación local.

Otra razón de ser de los valuetypes es el proporcionar semánticas compartidas y "null" para valores IDL tales como structs y secuencias. Usando valuetypes, por ejemplo, es posible pasar un grafo entero en una invocación de una operación de forma que se preserven los valores compartidos en el grafo. O por ejemplo, una estructura IDL puede estar vacía (es decir, sus valores pueden estar no inicializados), pero nunca puede ser "null"; los valuetypes sí permiten esto.

Los valuetypes son una mezcla entre interfaces y estructuras, y se declaran con la palabra reservada valuetype. Existen varias clases de valuetypes. Los regular valuetypes, como las interfaces, pueden contener definiciones de atributos, operaciones y declaraciones de tipos locales. A diferencia de las interfaces, pueden tener miembros que reflejan el estado, e inicializadores.

interface Observer {
  void notify();
}
valuetype ShoppingCart supports Observer {
  readonly attribute long current_value;
  private HardwareOrderSeq selected;
  void addToCart(in fitting item);
  factory init(in HardwareOrderSeq initial);

};

Los miembros que reflejan el estado (state members) constituyen el estado que es codificado y enviado al receptor. Los miembros estado pueden declararse public o private. Si se declara private debería ser colamente accesible por la implementación de las operaciones valuetype.

Para crear nuevas instancias, puede definirse un inicializador usando la palabra reservada factory.

Los abstract valuetypes son un tipo de valuetypes que no pueden ser instanciados. No tienen inicializadores, ni valores mismbros, solamente se permiten operaciones o definiciones de tipos de datos.

abstract valuetype Cart supports Observer {
  void add(in Any item);
}

3.6. Convirtiendo IDL en Java

Una vez que se han especificado las interfaces en IDL, es necesario generar las clases Java que sirven como punto de partida para implementar las interfaces remotas en Java usando un compilador Idl-to-Java. Cada compilador estándar Idl-to-Java genera las siguientes clases a partir de una interfaz IDL:

Con respecto a la clase helper, la implementación del método narrow() convierte una referencia a un Object de CORBA en una referencia a un tipo específico. Cada stub CORBA para un objeto remoto contiene un objeto Delegate interno (obtenido del paquete org.omg.CORBA.portable) que es usado por el stub para invocar peticiones remotas. Si el objeto no contiene un objeto Delegate interno, el método narrow() producirá la excepción BAD_PARAM.

Una clase holder contiene una única instancia de un objeto CORBA. Cuando un objeto holder es pasado en una llamada a un método remoto como un argumento inout, se invoca a su método _write(). Este método toma el objeto contenido en la clase holder, lo serializa, y lo envía como un flujo de datos a través del ORB al servidor del objeto remoto. Cuando la llamada al método remoto termina, se invoca al método _read() de la clase holder para leer el (posiblemente actualizado) objeto desde el servidor del objeto remoto, y el objeto holder reemplaza su valor interno con el objeto actualizado.

La herramienta idlj proporcionada por Sun puede generar dos clases adicionales:

La clase base para la implementación del servidor acepta peticiones destinadas a la implementación del servidor desde el ORB. La clase base converte una petición en una llamada a un método en el objeto servidor y entonces toma el resultado de la llamada y la devuelve al ORB para enviarla al stub del cliente. Todo este trabajo se realiza por el método del skeleton del servidor invoque(). El método invoque() "averigua" el método que está siendo llamado, decodifica los argumentos del método (si los hay) desde la petición, y llama al método directamente.

Una vez que se ha escrito la interfaz IDL y generado la interfaz Java y las clases de soporte, incluyendo las clases stub y skeleton, es necesario concretar las implementaciones del servidor para todos los métodos de la interfaz. Esto lo podemos hacer creando subclases a partir de la clase _xxxImplBase generada por el compilador IDL.