HTML5. WebSockets amb Jetty

Continuant amb l’exploració de l’HTML5, avui parlo dels WebSockets. I per a il·lustrar-lo, faré un petit exercici amb websockets fent servir el servidor java Jetty.

La idea de l’exemple l’he tret del projecte http://code.google.com/p/phpwebsocket/. El client html és pràcticament idèntic. La part del servidor, en el meu exemple, l’he implementada amb Java; estant en PHP en l’original.

Els WebSockets permeten realitzar la comunicació bidireccional i full-duplex sobre un canal TCP, entre la pàgina web i el servidor de forma molt més eficient que amb l’esquema de petició-resposta de l’HTML, o que la comunicació amb AJAX o, més explí­citament, mitjançant Comet.

El protocol WebSocket es descriu a la RFC6455.

A dia d’avui, aquest protocol es troba implementat per les darreres versions dels principals navegadors:  Chrome, Internet Explorer, Firefox i Safari.

A més, el servidor ha de soportar també els WebSockets de servidor.

En si mateix el protocol recorda una crida HTTP, però no ho és:

Exemple de petició des del client:

GET /mychat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://example.com

Exemple de resposta del servidor:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Des del punt de vista del desenvolupament parlem, doncs, de clients i servidors de WebSockets.
HTML5 ofereix una API (normalitzada pel W3C) per a desenvolupar clients d’aplicacions basades en WebSocket.

Per la banda del servidor, els llenguatges més utlitzats, com Java o php aporten classes, mètodes i funcions per a crear i utilitzar websockets de servidor.

Sobre servidors Java: jWebSocket, Apache Tomcat i Jetty també tenen implementacions de sockets de servidor.
Sobre servidors PHP: phpWebSockets

Podem trobar implementacions de servidor WebSocket amb altres llenguatges. Per exemple amb Python una cerca ens proporciona: pywebsocket, o WebSocket for Python (ws4py).

Per a fer un experiment senzill utilitzaré el servidor de java servlets Jetty (versió 8) que proporciona una implementació java de WebSockets de servidor. Podeu revisar aquest article: Jetty WebSocket Server, i aquest altre: Jetty/Feature/WebSockets.
Per descomptat, el javadoc: http://download.eclipse.org/jetty/stable-8/apidocs/

Per a provar els WebSockets he creat un petit projecte amb Eclipse, amb la següent estructura de carpetes:

A la carpeta src hi creo el package java amb la següent classe: com.stsoftlliure.proves.html5.websockets.ProvaWebSocket

Fitxer ProvaWebSocket.java:

package com.stsoftlliure.proves.html5.websockets;

import java.io.IOException;
import java.util.Date;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.eclipse.jetty.util.log.Log;
public class ProvaWebSocket extends WebSocketServlet  {

   private static final long serialVersionUID = 8442112605629206192L;


    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
    throws ServletException ,IOException  {
      response.getOutputStream().println("aquesta és la resposta");
      Log.info("[doGet] No fa res");
    }

    public WebSocket doWebSocketConnect(HttpServletRequest request,
                                        String protocol) {
      Log.info("[doWebSocketConnect] entra a doWebSocketConnect");
      return new ProvaServerWebSocket();
    }

    // inner class que implementa la interface WebSocket
    class ProvaServerWebSocket implements WebSocket.OnTextMessage {
      // la connexió
      Connection wsConn;
      Date dataInici;

      public ProvaServerWebSocket() {
         dataInici = new Date();
      }

      @Override
      public void onClose(int arg0, String arg1) {
         // TODO Auto-generated method stub
         Log.info("[onClose] arg0:" + arg0 + "; arg1: " + arg1);
         wsConn.close(arg0, arg1);
      }

      @Override
      public void onOpen(Connection wsConn) {
         this.wsConn = wsConn;
         try {
            Log.info("[onOpen] wsConn:" + wsConn);
            wsConn.sendMessage("Estic connectat!");
         } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            Log.debug("Error: " + e.toString());
         }
      }

      @Override
      public void onMessage(String sMessage) {
         // TODO Auto-generated method stub
         Log.info("[onMessage] Missatge rebut: " + sMessage);
            // fa el tractament:
         // to do
         // help, hola, nom, edat, data, adeu
 
         try {
            if (sMessage.trim().equalsIgnoreCase("help")) {
               wsConn.sendMessage("ordres acceptades: help, hola, nom, edat, data, adeu");
            }

            if (sMessage.trim().equalsIgnoreCase("hola")) {
               wsConn.sendMessage("Hola! benvingut al servidor WebSocket!");
            }

            if (sMessage.trim().equalsIgnoreCase("nom")) {
               wsConn.sendMessage("Jo em dic ProvaWebSocket.class");
            }

            if (sMessage.trim().equalsIgnoreCase("edat")) {
               wsConn.sendMessage("En funcionament des de: " + dataInici);
            }

            if (sMessage.trim().equalsIgnoreCase("data")) {
               wsConn.sendMessage("ara són les: " + (new Date()));
            }

            if (sMessage.trim().equalsIgnoreCase("adeu")) {
               wsConn.sendMessage("Adéu siau!");
               wsConn.close();
            }
         } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            Log.debug("Error: " + e.toString());
         }
      }
   }
}

Algunes remarques a la classe anterior:

– La classe és un servlet del tipus nou WebSocketServlet.
– Els WebSocketServlet implementen el doGet. El puc invocar directament i obtinc una resposta HTML. Però la funcionalitat apareix amb el nou mètode doWebSocketConnect. A doWebSocketConnect és on treballa el socket de servidor.
-Quan es rep una nova connexió, el mètode doWebSocketConnect el que fa és instanciar la inner class ProvaServerWebSocket, que implementa la interface WebSocket.OnTextMessage.
– La interface WebSocket té altres subinterfaces, a més de la OnTextMessage, per a tractar altres tipus de connexions. Les subinterfaces de WebSocket són: WebSocket.OnBinaryMessage, WebSocket.OnControl, WebSocket.OnFrame, WebSocket.OnTextMessage.
– en la connexió onOpen, obtinc l’objecte Connection wsConn. Que serà propiament el canal full duplex entre servidor i client.
– El tractament d’un missatge rebut el faig en implementar el mètode public void onMessage(String sMessage). sMessage és el missatge rebut des del client. Les respostes al client s’envien mitjançant l’objecte Connection wsConn obtingut en l’event onOpen. En aquest exemple, simplement analitzo quin missatge rebo i, depenent del missatge, envio una o altre resposta al client.
– A la sortida onClose, tanco la connexió

Senzill i net, oi?

L’altre part és la pàgina del client. Una pàgina HTML5 que fa servir websockets ha d’implementar els mètodes que responen als esdeveniments de connexió, alliberament, recepció i enviament de missatges al servidor. Vet aquí la pàgina HTML que dialoga amb el servidor:

Fitxer prova.html

<html>
<head>
<title>WebSocket</title>

<style>
 html,body{font:normal 0.9em arial,helvetica;}
 #log {width:440px; height:200px; border:1px solid #7F9DB9; overflow:auto;}
 #msg {width:330px;}
</style>

<script>
var socket;

function init(){
  var host = "ws://localhost:8080/websockets2/servlet/WebSocket";
  try{
    socket = new WebSocket(host);
    log('WebSocket - status '+socket.readyState);
    socket.onopen    = function(msg){ log("Welcome - status "+this.readyState); };
    socket.onmessage = function(msg){ log("Received: "+msg.data); };
    socket.onclose   = function(msg){ log("Disconnected - status "+this.readyState); };
  }
  catch(ex){ log(ex); }
  $("msg").focus();
}

function send(){
  var txt,msg;
  txt = $("msg");
  msg = txt.value;
  if(!msg){ alert("El missatge no pot ser nul"); return; }
  txt.value="";
  txt.focus();
  try{ socket.send(msg); log('Enviat: '+msg); } catch(ex){ log(ex); }
}

function quit(){
  log("Adéu!");
  socket.close();
  socket=null;
}

// Utilities
function $(id){ return document.getElementById(id); }
function log(msg){ $("log").innerHTML+="<br>"+msg; }
function onkey(event){ if(event.keyCode==13){ send(); } }
</script>

</head>
<body onload="init()">
 <h3>WebSocket</h3>
 <div id="log"></div>
 <input id="msg" type="textbox" onkeypress="onkey(event)"/>
 <button onclick="send()">Send</button>
 <button onclick="quit()">Quit</button>
 <div>Commands: help, hola, nom, edat, data, adeu</div>
</body>
</html>

Alguns comentaris: com es veu, la pàgina és senzilla, la clau del funcionament està al mètode init

function init(){
  var host = "ws://localhost:8080/websockets2/servlet/WebSocket";
  try{
    socket = new WebSocket(host);
    log('WebSocket - status '+socket.readyState);
    socket.onopen    = function(msg){ log("Welcome - status "+this.readyState); };
    socket.onmessage = function(msg){ log("Received: "+msg.data); };
    socket.onclose   = function(msg){ log("Disconnected - status "+this.readyState); };
  }
  catch(ex){ log(ex); }
  $("msg").focus();
}

El qual crea l’objecte socket, el connecta al host, adonem-nos del protocol “ws”; i enllaça els esdeveniments del socket: onopen, onmessage, onclose amb les funcions que els tracten.

L’enviament de missatges al servidor es realitza amb la funció send, que invoca al mètode send de l’objecte socket.

function send(){
  var txt,msg;
  txt = $("msg");
  msg = txt.value;
  if(!msg){ alert("El missatge no pot ser nul"); return; }
  txt.value="";
  txt.focus();
  try{ socket.send(msg); log('Enviat: '+msg); } catch(ex){ log(ex); }
}

Amb l’anterior ja tinc tot el codi. Poso la pàgina prova.html a la carpeta webcontent del projecte Eclipse. A més, en la carpeta webcontent també hi posaré la carpeta WEB-INF, amb una subcarpeta classes buida, la carpeta lib amb les llibreries que em calen i el web.xml.

En resum, a webcontent hi trobo:

webcontent/
    prova.html
    WEB-INF/
        classes/
        lib/
            jetty-util-8.1.2.v20120308.jar
            jetty-websocket-8.1.2.v20120308.jar
        web.xml

Les llibreries a la carpeta lib corresponen a la versió 8 del Jetty i es poden trobar a la seva carpeta lib. He vist que cal incloure-les al war. Em pensava que es referenciarien automàticament al fer deploy al Jetty, però no ha estat així.

El web.xml és força estàndar:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<display-name>tailor</display-name>
	<servlet>
		<servlet-name>WebSocket</servlet-name>
		<servlet-class>com.stsoftlliure.proves.html5.websockets.ProvaWebSocket</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>WebSocket</servlet-name>
		<url-pattern>/servlet/*</url-pattern>
	</servlet-mapping>

	<welcome-file-list>
		<welcome-file>prova.html</welcome-file>
	</welcome-file-list>
</web-app>

Si de cas, remarcar el mapeig de /servlet/* a WebSocket. Aquesta és l’explicació de
var host = “ws://localhost:8080/websockets2/servlet/WebSocket”;
a la funció init.

Amb Jetty, a més, m’ha calgut establir el “context.” Al projecte, el fitxer de context l’he guardat a la carpeta homònima. És el següent fitxer.

websockets2.xml:

<?xml version="1.0"  encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">

<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <Set name="contextPath">/websockets2</Set>
  <Set name="war"><SystemProperty name="jetty.home" default="."/>/webapps/websockets2.war</Set>
</Configure>

Compte amb els salts de línia. El fitxer XML de context potser no queda “bonic” tal com està, però us podeu trobar amb problemes estranys si afegiu salts de línia.

Amb el fitxer anterior li estem dient quin és el war que ha de desplegar i quin nom tindrà el context. Amb aquest fitxer de context acabem de completar l’explicació del

var host = “ws://localhost:8080/websockets2/servlet/WebSocket”;

Ara és evident:

ws:// – Protocol websocket. Amb websockets segurs, serà wss://
localhost:8080 – Host i port del servidor websocket
websockets2 – El context, o l’aplicació web.
servlet/WebSocket – el Servlet servidor de WebSocket

Finalment, per automatitzar el muntatge i ddesplegament al servidor jetty local de l’apliació, faig servir el següent build.xml d’ant:

<project name="websocket" default="deploy" basedir=".">
  <description>
  proves amb websockets 2
  </description>

  <!-- estableix propietats globals -->
  <property name="src" location="src"/>
  <property name="bin" location="bin"/>
  <property name="war" location="war"/>
  <property name="webcontent" location="webcontent" />
  <property name="context" location="context" />
  <property name="build" location="build"/>
  <property name="lib" location="/home/albert/jetty/lib" />
  <property name="deploy" location="/home/albert/jetty/webapps" />
  <property name="deploy-context" location="/home/albert/jetty/contexts" />


  <target name="init">
    <!-- time stap -->
    <tstamp/>
    <!-- Crea el directory bin utilitzat en la compilació -->
    <mkdir dir="${bin}"/>
  </target>


  <target name="compile" depends="init" description="compila els fitxers font" >
    <!-- Compila les fonts java de ${src} en ${build} -->
    <javac srcdir="${src}" destdir="${bin}">
      <classpath>
       <pathelement location="${lib}/jetty-websocket-8.1.2.v20120308.jar"/>
       <pathelement location="${lib}/servlet-api-3.0.jar"/>
       <pathelement location="${lib}/jetty-util-8.1.2.v20120308.jar"/>
      </classpath>
   </javac>
  </target>

  <target name="build" depends="compile" description="genera el build">
    <!-- Crea el directori de distribució -->
    <delete dir="${build}" />
    <mkdir dir="${build}"/>

    <!--  copia webcontent a build -->
    <copy todir="${build}" >
        <fileset dir="${webcontent}" />
    </copy>
    
    <!--  copia bin a classes -->
    <copy todir="${build}/WEB-INF/classes">
        <fileset dir="${bin}" />
    </copy>
  </target>
     
  <target name="war" depends="build" description="genera el war">   
    <delete file="${war}/websockets2.war" />
    <jar jarfile="${war}/websockets2.war" basedir="${build}"> 
      <include name="**/*"/>
    </jar>
  </target>
  	
  <target name="deploy" depends="war" description="deploy del war i del context">
    <delete file="${deploy}/websockets2.war" />
    <delete file="${deploy-context}/websockets2.xml" />
  	<copy file="${war}/websockets2.war" todir="${deploy}" />
  	<copy file="${context}/websockets2.xml" todir="${deploy-context}" /> 
  </target>

</project>

Aleshores, ses d’Eclipse puc executar el build.xml que compila la classe, construeix el war i el copia a la carpeta webapps del meu Jetty local.

Per provar l’aplicació només en cal engegar el Jetty i obrir el navegador amb suport de websockets (en el meu cas, el Chrome) i apuntar-lo a l’adreça http://localhost:8080/websockets2

I el resultat és el següent.

I, efectivament, puc mantindre el diàleg amb el servidor:

BeanShell, scripting per a Java

BeanShell (bsh) és un interpret de Java estès amb característiques de llenguatge d’scripting. BeanShell està desenvolupat en Java i s’entrega i es pot executar com aplicació standalone des del jar, però també es pot afegir com a llibreria en una aplicació standalone Java que incorporaria d’aquesta forma un llenguatge d’scripting per a fer macros, per exemple.

A tall d’exemple, BeanShell és un dels llenguatges de macros que incorpora OpenOffice.org/LibreOffice (OOo/LO). També és el llenguatge de macros de l’editor JEdit, també es pot trobar com a llenguatge de macros a l’IDE de Java NetBeans.

Encastat a una aplicació Java, i pel fet d’executar-se en la mateixa JVM de l’aplicació hoste, és capaç d’interactuar amb els objectes de l’aplicació. Com que és interpretat i no cal compilar els scripts de bsh obre una porta a l’extensió de l’aplicació hoste. I amb tota la potència de Java, doncs BeanShell és capaç d’importar packages java.

BeanShell és un jar que ocupa menys de 300KB. És considerablement més petit, doncs, que alternatives més potents com Groovy (De fet, Groovy va molt més enllà del que seria un llenguatge d’scripting. Per exemple, el framework  Grails  està basat en Groovy).

Els punts forts de BeanShell són aquests: és java, ofereix facilitats per a l’scripting, és petit, és encastable i és fa servir a OOo/LO, entre d’altres.

Punts dèbils? malgrat que la llista de correu de desenvolupadors i d’usuaris de BeanShell a SourceForge mostra certa activitat enguany, la llista d’anuncis de noves versions resta aturada des de 2005 (justament quan el JCP aprovava la creació d’una JSR per al BeanShell). El punt dèbil és, doncs, que no ha evolucionat des de fa massa temps.

En tot cas, el fet de ser un interpret de Java fa que sigui immediatament utilitzable per programadors d’aquest llenguatge.

L’escenari d’us de BeanShell seria, doncs, aquell en el que cal incorporar un llenguatge de macros a una aplicació java que sigui lleuger i, a la vegada, d’aplicació immediata.

La sintaxi del llenguatge és la de Java. Res a descobrir, doncs. Tanmateix, hi han afegides característiques orientades a l’scripting, com el “tipatge relaxat”, o comandes específiques  per a entrada sortida per consola

Les referències són, per una banda el manual d’usuari; i per l’altre, el javadoc de beanshell. També és recomanable veure la presentació amb diapositives (en format .pdf) Un punt d’entrada és l’anàlisi dels exemples que també es poden trobar a la web.

Si hem instal·lat bsh des del Centre de Programari de l’Ubuntu, o si tenim instal·lat l’OpenOffice.org/LibreOffice, és possible que trobem el bsh.jar a les ubicacions següents:

/usr/share/java/bsh-2.0b4.jar
/usr/share/java/bsh.jar
/usr/lib/openoffice/basis3.2/program/classes/bsh.jar

Per engegar l’interpret n’hi ha prou amb escriure bsh a la línia de comandes. Però també el podem engegar com la aplicació Java que és:

java -jar /usr/share/java/bsh.jar bsh.

en aquest punt ja podem introduir ordres. tanmateix és més pràctic fer un script:

Fem la prova canònica. Creo l’script hello.bsh

a = “hola “;
b = ” món!”;
print (a + b);

i l’executo

bsh ./hello.bsh

hola  món!

Com era d’esperar… L’script anterior en bsh és indistingible d’altres llenguatges d’script que disposen de la comanda print.

Haviem dit que bsh és java amb característiques d’script. Vet aquí la primera: no ha calgut definir el tipus de les variables.

Hauria pogut ser més rigurós. Amplio l’script amb línies que són Java inequívocament:

a = “hola “;
b = ” món!”;
print (a + b);

String s_Nom = “Serveix TIC i Programari Lliure”;
String s_llocweb = ” (http://apuntstecnologia.blogspot.com/)”;

System.out.println(s_Nom + “, ” + s_llocweb);

System.out.println(“Variable no inicialitzada: ” + sNom + “, ” + s_LlocWeb);

i el resultat és:

albert@atenea:~$ bsh ./hello.bsh
hola  món!
Serveix TIC i Programari Lliure,  (http://apuntstecnologia.blogspot.com/)
Variable no inicialitzada: void, void

void, en comptes de null o “”. A bsh es pot verificar si una variable està inicialitzada comparant-la amb “void”.

Més coses: els “scripted objects”, una forma de definir objectes a l’estil de com ho fan altres llenguatges d’scripts (recorda Javascript) que aprofiten les estructures de blocs.

ClasseBsh() {
   propietat1 = 1;
   propietat2 = “un text”;  
   
   Metode1() {
      print(“Aquest és el mètode 1”);
   }

   Metode2() {
      print(“Aquest és el mètode 2”);
   }

   Metode3() {
       cbsh2 = ClasseBsh2();
       print(“cbsh2.propietat1: ” + cbsh2.propietat1);
       print(“cbsh2.propietat2: ” + cbsh2.propietat2);
       cbsh2.Metode1();
       cbsh2.Metode2();
   }

   
   ClasseBsh2() {
       propietat1 = 1;
       propietat2 = “un altre text”;   
       
       Metode1() {
           print(“Aquest és el mètode 1 intern”);
       }

       Metode2() {
           print(“Aquest és el mètode 2 intern”);
       }

      return this;
    }

    return this;
}

cbsh1 = ClasseBsh();

print(“cbsh1.propietat1: ” + cbsh1.propietat1);
print(“cbsh1.propietat2: ” + cbsh1.propietat2);
cbsh1.Metode1();
cbsh1.Metode2();
cbsh1.Metode3();

El resultat és:

albert@atenea:~$ bsh ./classes.bsh
cbsh1.propietat1: 1
cbsh1.propietat2: un text
Aquest és el mètode 1
Aquest és el mètode 2
cbsh2.propietat1: 1
cbsh2.propietat2: un altre text
Aquest és el mètode 1 intern
Aquest és el mètode 2 intern

Remarcar la sintaxi simplificada, i també com es poden fer classes dins de classes sense ĺímit de nivell.

Com encastar BeanShell a una aplicació Java?

Res més senzill. Podem fer una petita classe java que carrega un interpret de BeanShell que executa un script:

package com.sticipl.proves;

public class ProvaBshRun {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new ProvaBshRun();
    }

    public ProvaBshRun() {
        try {
            // carrega script i l’executa
            System.out.println(“carrega script i l’executa”);
            Interpreter beanshellInterpret = new Interpreter();
            beanshellInterpret.source(“/home/albert/hello.bsh”); 
      
        } catch(Exception e) {
            System.out.println(“Error: ” + e.toString());
        }
    }
}

 

Per a fer l’execució més senzilla, empaqueto la classe anterior en un jar. Faig servir el següent build.xml per a l’ant

<project name=”provabsh” default=”build” basedir=”.”>
  <description>
  proves amb beanshell
  </description>

  <!– estableix propietats globals –>
  <property name=”src” location=”src”/>
  <property name=”bin” location=”bin”/>
  <property name=”build” location=”build”/>
  <property name=”lib”  location=”lib”/>


  <target name=”init”>
    <!– time stap –>
    <tstamp/>
    <!– Crea el directory bin utilitzat en la compilació –>
    <mkdir dir=”${bin}”/>
  </target>


  <target name=”compile” depends=”init” description=”compila els fitxers font” >
    <!– Compila les fonts java de ${src} en ${build} –>
    <javac srcdir=”${src}” destdir=”${bin}”>
      <classpath>
       <pathelement location=”${lib}/bsh-2.0b4.jar”/>
      </classpath>
   </javac>
  </target>

  <target name=”build” depends=”compile” description=”genera el jar”>
    <!– Crea el directori de distribució –>
    <mkdir dir=”${build}”/>

    <jar jarfile=”${build}/provabsh.jar” basedir=”${bin}”>
      <include name=”**/*.class”/>
      <manifest>
        <attribute name=”Main-Class” value=”com.sticipl.proves.ProvaBshRun”/>
          <attribute name=”Class-Path” value=”bsh-2.0b4.jar”/>
      </manifest>
    </jar>
     
      <copy file=”${lib}/bsh-2.0b4.jar” todir=”${build}” />
  </target>

</project>

Amb el build.xml anterior obtinc una carpeta build on he deixat el jar provabsj.jar que conté la classe ProvaBshRun. El manifest del jar indica que cal fer servir la llibreria bsh-2.0b4.jar.

Puc executar la classe amb:

albert@atenea:~/wk-java/ProvaBsh/build$ java -jar ./provabsh.jar

o bé amb:

albert@atenea:~/wk-java/ProvaBsh/build$ java -jar -cp ./bsh-2.0b4.jar ./provabsh.jar

i el resultat de l’execució és:

carrega script i l’executa
hola  món!
Serveis TIC i Programari Lliure,  (http://apuntstecnologia.blogspot.com/)
Variable no inicialitzada: void, void
data: Tue Jul 19 18:38:08 CEST 2011

Un cop carregat l’script també es poden manipular-ne els mètodes i propietats. Per exemple, carrego l’script classes.bsh i n’executo mètodes i en canvio propietats:

package com.sticipl.proves;

import bsh.Interpreter;
import java.util.Date;

public class ProvaBshRun {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new ProvaBshRun();
    }

    public ProvaBshRun() {
        Interpreter beanshellInterpret = new Interpreter();

        try {
            // Avaluació d’instruccions i expressions
            // Carrega script i en modifica propietats
            System.out.println(“Carrega script”);
            beanshellInterpret.source(“/home/albert/classes.bsh”);    
          
            int  iPropietat1 = ((Integer) beanshellInterpret.get(“cbsh1.propietat1”)).intValue();
            String sPropietat2 = (String) beanshellInterpret.get(“cbsh1.propietat2”);
            System.out.println(“iPropietat1: ” + iPropietat1);
            System.out.println(“sPropietat2: ” + sPropietat2);
           
           
            iPropietat1 = 25;
            beanshellInterpret.set(“cbsh1.propietat1”, iPropietat1);
            sPropietat2 = “aquest és el text canviat des de l’aplicació”;
            beanshellInterpret.set(“cbsh1.propietat2”, sPropietat2);
            System.out.println(“des de l’aplicació”);
            System.out.println(“iPropietat1: ” + iPropietat1);
            System.out.println(“sPropietat2: ” + sPropietat2);   
            System.out.println(“des de beanshell”);
            beanshellInterpret.eval(“print(\”cbsh1.propietat1: \” + cbsh1.propietat1);”);
            beanshellInterpret.eval(“print(\”cbsh1.propietat2: \” + cbsh1.propietat2);”);
           
            System.out.println(“Executa mètode 1:” );
            beanshellInterpret.eval(“cbsh1.Metode1();”);
            System.out.println(“Executa mètode 2:” );
            beanshellInterpret.eval(“cbsh1.Metode2();”);
            System.out.println(“Executa mètode 3:” );
            beanshellInterpret.eval(“cbsh1.Metode3();”);

        } catch(Exception e) {
            System.out.println(“Error: ” + e.toString());
        }
    }
}

El resultat de l’anterior és


Carrega script
cbsh1.propietat1: 1
cbsh1.propietat2: un text
Aquest és el mètode 1
Aquest és el mètode 2
cbsh2.propietat1: 1
cbsh2.propietat2: un altre text
Aquest és el mètode 1 intern
Aquest és el mètode 2 intern
iPropietat1: 1
sPropietat2: un altre text
des de l’aplicació
iPropietat1: 25
sPropietat2: aquest és el text canviat des de l’aplicació
des de beanshell
cbsh1.propietat1: 25
cbsh1.propietat2: aquest és el text canviat des de l’aplicació
Executa mètode 1:
Aquest és el mètode 1
Executa mètode 2:
Aquest és el mètode 2
Executa mètode 3:
cbsh2.propietat1: 1
cbsh2.propietat2: un altre text
Aquest és el mètode 1 intern
Aquest és el mètode 2 intern

En el jar de BeanShell es poden trobar classes per a l’execució remota d’scripts, i també per l’execució en mode servlet. Deixo per a futurs posts la revisió d’aquestes opcions.

En resum s’ha presentat l’aplicació BeanShell i les seves característiques que més immediatament es poden utilitzar en aplicacions java.

Com fer un component UNO amb java

El capítol clau del Developer’s Guide (DG) és http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/WritingUNO/Writing_UNO_Components
En particular: http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/WritingUNO/Simple_Component_in_Java

El procés general queda descrit en aquesta esquema extret de JavaEclipse tutorial i que ja hem presentat en un post anterior

El que faré serà seguir aquest esquema aplicant-lo al codi d’exemple inclòs a l’SDK de l’OpenOffice.org.

Desenvoluparé aquest experiment sobre un Ubuntu 10.04 Lucid Lynx amb un OpenOffice.org 3.2.

En altres plataformes i versions les ubicacions poden ser diferents.

El codi d’exemple és el MinimalComponent que es troba a /opt/openoffice.org/basis3.2/sdk/examples/java/MinimalComponent/

El contingut de la carpeta és el següent:

BuildMinimalComponent.xml: un fitxer build d’ant per a generar el component. Lamentablement les ubicacions corresponen a les d’OpenOffice.org sobre Windows i cal corregir les adreces.

Makefile: un makefile per a generar el component fent servir el tradicional make.

MinimalComponent.idl: El primer pas per a crear un component és definir-ne la seva interfase. Tots els components han d’implementar certes interfases. El que es fa en l’exemple és crear un component que implementa una d’aquestes interfases obligatòries: la com/sun/star/lang/XServiceInfo.idl la qual proporciona mètodes que informen sobre les serveis que implementa el component.

Un component útil tindria una interfase específica que es definiria al fitxer .idl i, a més, també implementarà les interfases obligatòries (XServiceInfo, XInitialize i XTypeProvider).

El llenguatge de deficició de interfases idl de UNO, el UNOIDL, es descriu en el capítol del DG: http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/WritingUNO/Using_UNOIDL_to_Specify_New_Components.
També és interessant consultar http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/WritingUNO/Generating_Source_Code_from_UNOIDL_Definitions

MinimalComponent.java: La implementació del component. He dit abans que tots els components han d’implementar obligatòriament XServiceInfo, XInitialize i XTypeProvider. El que es fa en aquesta classe és heredar de WeakBase que ja implementa XTypeProvider, de forma que només resta per implementar XServiceinfo i XInitialize. WeakBase és una de les classes que es poden fer servir de base per a implementar components (Al DG les en diuen Helper Classes).

TestMinimalComponent.java: Finalment, una classe conductora per a provar  el component que hem creat. Hi trobem codi per a instanciar el nou component i executar-ne els mètodes de la seva interfase.

El que faré serà seguir pas a pas el diagrama. Així doncs, el primer de tot és compilar la descripció de la interfase. Això es fa amb la utilitat idlc (IDL compiler) de l’SDK de l’OpenOffice.org.

En el meu cas, he creat un projecte de java nou amb l’Eclipse i hi he copiat la carpeta MinimalComponent sencera.

Observant amb atenció l’esquema, s’observa que té dues entrades possibles.
– a partir del .idl
– a partir del .java

compilar .idl (especificació)

La primera cadena, a partir de l’idl,

Em situo en aquesta carpeta i invoco l’idlc tenint en compte que em caldrà incloure la definició (amb opció -I) de la XServiceInfo.

Això últim es fa evident sense més que examinar MinimalComponent.idl

#ifndef _org_openoffice_MinimalComponent_idl_ 
#define _org_openoffice_MinimalComponent_idl_ 


#include <com/sun/star/lang/XServiceInfo.idl> 


// org
module org {
    // openoffice
    module openoffice {
        // example service, XServiceInfo is implemented here for demonstration
        // issues. XServiceInfo must be implemented by all components. But
        // here it is used to show the new code generation feature for services.
        // See the TestMinimalComponent.java how it can be used!
        service MinimalComponent: ::com::sun::star::lang::XServiceInfo;
    };
};


#endif 

Per compilar l’idl, doncs, faig servir la següent instrucció

/opt/openoffice.org/basis3.2/sdk/bin/idlc -I /opt/openoffice.org/basis3.2/sdk/idl MinimalComponent.idl

El resultat és el fitxer MinimalComponent.urd  que hauria de contenir la definició del nou tipus descrit per la interfase. Cal incloure aquesta nova definició a un Registre de tipus per a que es pugui invocar i sigui utilitzable. (per a més detalls sobre el registre, consulteu  la DG, http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/WritingUNO/Running_and_Debugging_Java_Components)

Això ho faig amb el programa regmerge.

/opt/openoffice.org/ure/bin/regmerge MinimalComponent.rdb /UCR MinimalComponent.ure

Amb el que obtinc una base de dades de registre: el fitxer MinimalComponent.rdb.

Ara bé, si observem el contingut d’aquesta base de dades de registre amb l’eina regview /opt/openoffice.org/ure/bin/regview ./MinimalComponent.rdb (suposant que estic a la mateixa carpeta que el .rdb), obtinc:

/
 / UCR
   / org
     / openoffice
       / MinimalComponent
         Value: Type = RG_VALUETYPE_BINARY
                Size = 142
                Data = version: 1
                       documentation: “”
                       file name: “”
                       type class: service
                       type name: “org/openoffice/MinimalComponent”
                       super type count: 1
                       super type name 0: “com/sun/star/lang/XServiceInfo”
                       field count: 0
                       method count: 1
                       method 0:
                           documentation: “”
                           flags: synchronous
                           name: “”
                           return type name: “void”
                           parameter count: 0
                           exception count: 0
                       reference count: 0


       Value: Type = RG_VALUETYPE_BINARY
              Size = 57
              Data = version: 0
                     documentation: “”
                     file name: “”
                     type class: module
                     type name: “org/openoffice”
                     super type count: 0
                     field count: 0
                     method count: 0
                     reference count: 0


     Value: Type = RG_VALUETYPE_BINARY
            Size = 46
            Data = version: 0
                   documentation: “”
                   file name: “”
                   type class: module
                   type name: “org”
                   super type count: 0
                   field count: 0
                   method count: 0
                   reference count: 0


El pas següent és  invocar el javamaker per a obtindre els .class corresponents al nou servei (org.openoffice.MinimalComponent)

La sintaxi d’us de javamaker és:

/opt/openoffice.org/basis3.2/sdk/bin/javamaker -BUCR -Torg.openoffice.MinimalComponent -nD /opt/openoffice.org/ure/share/misc/types.rdb ./MinimalComponent.rdb

I obtinc la classe

org.openoffice.MinimalComponent

He d’afegir la classe al classpath per a compilar la MinimalComponent de la implementació (és dir, la classe que està en Default Package) que és el segon camí d’entrada descrit en l’esquema.

compilar .java (implementació)

En el cas del MinimalComponent.java  n’hi ha prou amb afegir org.openoffice.MinimalComponent al build, compilar i empaquetar amb jar, indicant en el manifest del META-INF que la classe a registrar és MinimalComponent, això ho aconseguiré amb el següent build.xml per a l’ant. És interessant remarcar quines són les llibreries utilitzades en el classpath (en negreta): les mateixes que per a compilar una macro java  i les d’addcionals,  officebean.jar i unoloader.jar.

<project name=”ComponentUNO” default=”jar” basedir=”.”>
  <description>
  genera el zip del component UNO per a l’OpenOffice
  </description>

  <!– estableix propietats globals –>
  <property name=”src” location=”src”/>
  <property name=”build” location=”build”/>
  <property name=”dist”  location=”jar”/>
  <property name=”zip.dir”  location=”ComponentUNO”/>
  <property name=”cp-java-OOo-1″ location=”/opt/openoffice.org/ure/share/java/” />
  <property name=”cp-java-OOo-2″ location=”/opt/openoffice.org/basis3.2/program/classes/” />
  <property name=”OOo-user-macros” location=”/home/albert/.openoffice.org/3/user/Scripts/java/” />
  <property name=”OOo-shared-macros” location=”/opt/openoffice.org/basis3.2/share/Scripts/java/” />



  <target name=”init”>
    <!– time stap –>
    <tstamp/>
    <!– Crea el directory build utilitzat en la compilació –>
    <mkdir dir=”${build}”/>
  </target>


  <target name=”compile” depends=”init” description=”compila els fitxers font” >
    <!– Compila les fonts java de ${src} en ${build} –>
    <javac srcdir=”${src}” destdir=”${build}”>
      <classpath>
       <pathelement location=”${cp-java-OOo-1}/juh.jar”/>
        <pathelement location=”${cp-java-OOo-1}/jurt.jar”/>
       <pathelement location=”${cp-java-OOo-1}/ridl.jar”/>
       <pathelement location=”${cp-java-OOo-1}/unoloader.jar”/>
       <pathelement location=”${cp-java-OOo-2}/unoil.jar”/>
       <pathelement location=”${cp-java-OOo-2}/officebean.jar”/>
      </classpath>
   </javac>
  </target>




  <target name=”jar” depends=”compile” description=”genera el jar”>
    <!– Crea el directori de distribució –>
    <mkdir dir=”${dist}”/>


    <jar jarfile=”${dist}/MinimalComponent.1.uno.jar” basedir=”${build}”> 
      <include name=”**/*.class”/>
      <manifest>
        <attribute name=”RegistrationClassName” value=”MinimalComponent”/>
      </manifest>
    </jar>
  </target>


</project>

En compilar amb ant, obtinc el jar MinimalComponent.1.one.jar
En aquest punt, ja tinc el component. Com provar-lo? Primer cal fer-ne el desplegament. Un cop desplegat, escriuré una macro amb OOoBasic que n’invoqui un mètode.

En aquest punt  deixaré de seguir l’esquema.

L’eina per a registrar components des de les primeres versions de l’OpenOffice.org ha estat regcomp. Tanmateix, aquesta eina ha estat superada per eines posteriors i des de fa algunes versions el que es pot fer servir és l’eina unopkg.

L’eina unopkg permet desplegar  components en java (.jar), en c++ (.so o .dll), add-ons (.oxt)…  A més, és una eina que es pot fer servir des de línia d’ordres, però també es pot usar com una GUI. De fet, la GUI que activa és la mateixa que la del gestor d’extensions  de l’OpenOffice.org. El capítol de la DG que parla d’això és el de les Extensions.

Per tant,  executo ‘/opt/openoffice.org3/program/unopkg gui’ i puc registrar el meu component fàcilment. Recordar que el path fins unopkg ‘/opt/openoffice.org3/program/’ pot ser un de diferent a altres versions  d’Ubuntu i d’OpenOffice.org.

Se m’obre el gestor d’extensions on puc veure les extensions que tinc instal·lades:

Faig click a “Afegeix…”

Trio el tipus de fitxer  “Component Java UNO”

Obro i el meu component queda instal·lat

Si el vull desinstal·lar, puc fer-ho des del mateix gestor d’extensions.
Finalment, per acabar, provaré que el nou component es utilitzable. Creo una petita macro que l’invoca. 
Primer de tot engego l’OpenOffice.org, vaig a l’editor de macros. La macro que escric utilitza la popular  XrayTool per analitzar l’objecte UNO que he creat. A continuació invoco el mètode getImplementationName().
Sub Main
    provaComponentUNO
End Sub

sub provaComponentUNO
    Dim oMinimal as Object

    BasicLibraries.loadLibrary(“XrayTool”)

oMinimal = CreateUnoService(“org.openoffice.MinimalComponent”)
xray oMinimal
msgbox oMinimal.getImplementationName()
end sub

A  MinimalComponent.java trobem l’explicació de l’argument de CreateUnoService:
public class MinimalComponent {
    /** This class implements the component. At least the interfaces XServiceInfo,
     * XTypeProvider, and XInitialization should be provided by the service.
     */
    public static class _MinimalComponent extends WeakBase
        implements XInitialization, XServiceInfo {
        /** The service name, that must be used to get an instance of this service.
         */
        static private final String __serviceName =
        “org.openoffice.MinimalComponent”;
El resultat de l’execució és:
i, finalment:
Albert Baranguer
Barcelona, 08/11/2010

Com fer una aplicació java standalone que utilitzi l’OpenOffice.org per a modificar o crear un document

Una tasca comú a l’ofimàtica és automatizar la generació de documents. En el cas de l’MS Office això s’aconsegueix amb l’automatització OLE i llenguatges com Visual Basic.


Per exemple, suposem que cal documentar l’esquema d’una base de dades. Una forma de fer-ho seria escriure un programa que analitzes les meta-dades de la base de dades i generés un document de text. 

O, per exemple, crear un document amb el resultat d’analitzar escenaris amb un full de càlcul. Es podria escriure un programa que provés els diferents escenaris i generés un informe  amb els resultats 

En el món Linux es pot aconseguir tot això fent servir Java i OpenOffice.

Aquest post és un exemple de programa java que agafa un document de text de OpenOffice.org Writer que està buit (prova.odt) i l’omple amb 100 línies de text.

M’he basat en el codi disponible a http://weblogs.java.net/blog/2005/12/30/open-office-java-api i en l’OpenOffice.org Developers Guide (en PDF), capítol First Contact, per al build.xml de l’ant.




Comencem: el codi de la classe java 



package com.serveisticiprogramarilliure.standalone;


import com.sun.star.uno.XComponentContext;
import com.sun.star.frame.XComponentLoader;
import com.sun.star.frame.XStorable;
import com.sun.star.lang.XComponent;
import com.sun.star.comp.helper.Bootstrap;
import com.sun.star.beans.PropertyValue;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.text.XTextDocument;
import com.sun.star.ucb.XFileIdentifierConverter;
import com.sun.star.util.XCloseable;
import com.sun.star.frame.XDesktop;


public class ProvaStandalone {
 
    public static void main(String[] args) {
try {
   System.out.println(“Inici”);
   // engega l’OpenOffice.org
   // Bootstrap
   System.out.println(“Bootstrap”);
   XComponentContext xContext = Bootstrap.bootstrap();
   // MultiComponentFactory
   System.out.println(“MultiComponentFactory”);
     XMultiComponentFactory xMCF = xContext.getServiceManager();
     // Crea el Desktop
     System.out.println(“Raw Desktop”);
   Object oRawDesktop = xMCF.createInstanceWithContext(“com.sun.star.frame.Desktop”, xContext);
   System.out.println(“Desktop”);
   XDesktop xDesktop = (XDesktop) UnoRuntime.queryInterface(XDesktop.class, oRawDesktop);
   // Obté el carregador de components
   System.out.println(“Component loader”);
   XComponentLoader xCompLoader = (XComponentLoader) 
                                   UnoRuntime.queryInterface(XComponentLoader.class, xDesktop);


   // URL del document a generar.
   System.out.println(“convert to URL”);
   String sUrl =  convertToURL(xContext, xMCF, null, “/home/albert/Documents/prova.odt”);
   System.out.println(“Fitxer: ” + sUrl);
   
   // carrega un component document nou / 
   System.out.println(“Document existent”);
   PropertyValue[] voidProps = new PropertyValue[0];
   
   XComponent xComp = xCompLoader.loadComponentFromURL(sUrl, “_blank”, 0, voidProps);
   // “private:factory/swriter”;  document nou
   // “_blank”:  frame on es crea
   // 0: FrameSearchFlag, 
            // http://api.openoffice.org/docs/common/ref/com/sun/star/frame/FrameSearchFlag.html
   // new PropertyValue[0], 
            // http://api.openoffice.org/docs/common/ref/com/sun/star/document/MediaDescriptor.html
   
   // si en comptes d’utilitzar un document ja existent, volgués crear un document nou
            // posaria “private:factory/swriter” en comptes de sUrl
   
   // obté la interfase XTextDocument el component document nou
   System.out.println(“Escriu línies”);
   XTextDocument xTextDocument = (XTextDocument)
                                          UnoRuntime.queryInterface(XTextDocument.class, xComp);
   for(int i=0; i<100; i++) {
xTextDocument.getText().getEnd().setString( “Escriu 100 línies. Línia ” + i + “.\n” );
   }
   
   // el guarda en format PDF
   // http://www.oooforum.org/forum/viewtopic.phtml?t=71294
   // “Name”, “Type”, “UIName” 
   // “Rich Text Format”, “writer_Rich_Text_Format”, “Rich Text Format” 
   // “writer_pdf_Export”, “pdf_Portable_Document_Format”, “PDF – Portable Document Format” 
   // …
   System.out.println(“XStorable”);
   XStorable xStorable = (XStorable)UnoRuntime.queryInterface(XStorable.class, xTextDocument);
   // indica el filtre de PDF


   // i guarda el fitxer
   System.out.println(“StoreAsURL”);
   xStorable.storeAsURL(sUrl, voidProps);


   // tancar l’openoffice que ha obert
   System.out.println(“Tanca”);
   XCloseable xcloseable = (XCloseable) UnoRuntime.queryInterface(XCloseable.class, xTextDocument);
   xcloseable.close(false);
System.out.println(“Fi”);
} catch (java.lang.Exception e) {
   e.printStackTrace();
} finally {
   System.exit(0);
}
    }
    
    /**
     * Converts a system path into an URL using OOo API
     * @param sBase
     * @param sSystemPath
     * @return
     */
    private static String convertToURL(XComponentContext xContext, 
     XMultiComponentFactory xMCF, 
     String sBase, 
     String sSystemPath) {
        String sURL = null;
        try {
         System.out.println(“FileConverter”);
            XFileIdentifierConverter xFileConverter =
           (XFileIdentifierConverter) UnoRuntime.queryInterface(
               XFileIdentifierConverter.class,
               xMCF.createInstanceWithContext(“com.sun.star.ucb.FileContentProvider”, xContext));
            System.out.println(“getFileURLFromSystemPath”);
            sURL = xFileConverter.getFileURLFromSystemPath(sBase, sSystemPath );
        } catch (com.sun.star.uno.Exception e) {
            e.printStackTrace();
        } finally {
            return sURL;
        }
    }
    
    /**
     * Converts an URL into a system path using OOo API
     * @param sURLPath
     * @return
     */
    private static String convertFromURL( XComponentContext xContext,
     XMultiComponentFactory xMCF,
     String sURLPath) {
        String sSystemPath = null;
        try {
            // m_xMCF = getMultiComponentFactory();
            XFileIdentifierConverter xFileConverter =
           (XFileIdentifierConverter) UnoRuntime.queryInterface(
               XFileIdentifierConverter.class,
               xMCF.createInstanceWithContext(
                            “com.sun.star.ucb.FileContentProvider”, xContext));
            sSystemPath = xFileConverter.getSystemPathFromFileURL(sURLPath);

        } catch (com.sun.star.uno.Exception e) {
            e.printStackTrace(System.err);
        } finally {
            return sSystemPath;
        }
    }
}


A continuació el build.xml d’ant que he fet servir. Difereix del que es troba en la Developers Guide en els paths que aquí estan adaptats a un OpenOffice.org 3.2  sobre Linux.



<?xml version=”1.0″ encoding=”UTF-8″?>
<project basedir=”.” default=”all” name=”standalone01″>
  <property name=”OFFICE_ROOT” value=”/opt/openoffice.org” />
  <property name=”OFFICE_HOME” value=”${OFFICE_ROOT}/basis3.2″ />
  <property name=”OO_SDK_HOME” value=”${OFFICE_HOME}/sdk” />
  <property name=”OO_URE_HOME” value=”${OFFICE_ROOT}/ure” />

  <target name=”init”>
  <property name=”OUT_DIR” value=”${basedir}/build/” />
<property name=”BIN_DIR” value=”${basedir}/bin/” />
  </target>

  <path id=”office.class.path”>
<filelist dir=”${OFFICE_HOME}/program/classes” files=”unoil.jar” />
<filelist dir=”${OO_URE_HOME}/share/java” files=”jurt.jar,ridl.jar,juh.jar” />
  </path>

  <fileset id=”bootstrap.glue.code” dir=”${OO_SDK_HOME}/classes”>
<patternset>
<include name=”com/sun/star/lib/loader/*.class” />
</patternset>
  </fileset>

  <target name=”compile” depends=”init” unless=”eclipse.running”>
<mkdir dir=”${BIN_DIR}” />
<javac debug=”true” deprecation=”true” destdir=”${BIN_DIR}” srcdir=”.”>
<classpath refid=”office.class.path” />
</javac>
  </target>

  <target name=”jar” depends=”init,compile”>
<mkdir dir=”${OUT_DIR}” />
<jar basedir=”${BIN_DIR}” compress=”true” jarfile=”${OUT_DIR}/standalone01.jar”>
<exclude name=”**/*.java” />
<exclude name=”*.jar” />
<fileset refid=”bootstrap.glue.code” />
<manifest>
<attribute name=”Main-Class” value=”com.sun.star.lib.loader.Loader” />
<section name=”com/sun/star/lib/loader/Loader.class”>
<attribute name=”Application-Class” 
value=”com.serveisticiprogramarilliure.standalone.ProvaStandalone” />
</section>
</manifest>
</jar>
  </target>

  <target name=”all” description=”compila i linka” depends=”init,compile,jar”>
<echo message=”OK. standalone01.jar” />
  </target>

    <target name=”cleanbin” description=”neteja binaris” unless=”eclipse.running”>
<delete>
<fileset dir=”${BIN_DIR}”>
<include name=”**/*.class” />
</fileset>
</delete>
  </target>

  <target name=”cleanall” description=”neteja tot” depends=”init,cleanbin”>
<delete file=”${OUT_DIR}/standalone01.jar” />
  </target>
</project>


Finalment, el fitxer executa.sh amb el que llenço l’execució. Recordo que l’entorn amb el que treballo és un OpneOffice.org 3.2.1 (i SDK), sobre Linux Ubuntu Lucid Lynx 10.04. compilat amb JDK 1.6. Versions diferents de qualsevol d’aquests elements pot provocar que algun path no sigui el correcte i que l’aplicació no funcioni, compte amb això!



OFFICE_ROOT=/opt/openoffice.org
OFFICE_HOME=$OFFICE_ROOT/basis3.2
OO_SDK_HOME=$OFFICE_HOME/sdk
OO_URE_HOME=$OFFICE_ROOT/ure


echo java -cp “$OFFICE_HOME/program/classes/unoil.jar”,”$OFFICE_HOME/program/classes/sandbox.jar”,”$OO_URE_HOME/share/java/jurt.jar”,”$OO_URE_HOME/share/java/ridl.jar”,”$OO_URE_HOME/share/java/juh.jar” -Djava.library.path=”/opt/openoffice.org/basis3.2/sdk/classes” -Dcom.sun.star.lib.loader.unopath=”/opt/openoffice.org3/program”  -jar standalone01.jar


java -cp “$OFFICE_HOME/program/classes/unoil.jar”,”$OFFICE_HOME/program/classes/sandbox.jar”,”$OO_URE_HOME/share/java/jurt.jar”,”$OO_URE_HOME/share/java/ridl.jar”,”$OO_URE_HOME/share/java/juh.jar” -Djava.library.path=”/opt/openoffice.org/basis3.2/sdk/classes” -Dcom.sun.star.lib.loader.unopath=”/opt/openoffice.org3/program”  -jar standalone01.jar


El resultat de l’execució és un document de text prova.odt amb 100 línies de text i els següents missatges a la consola:



albert@atenea:/media/Nuevo vol/wk-java/OOo-standalone-01/build$ ./executa.sh 
java -cp /opt/openoffice.org/basis3.2/program/classes/unoil.jar,/opt/openoffice.org/basis3.2/program/classes/sandbox.jar,/opt/openoffice.org/ure/share/java/jurt.jar,/opt/openoffice.org/ure/share/java/ridl.jar,/opt/openoffice.org/ure/share/java/juh.jar -Djava.library.path=/opt/openoffice.org/basis3.2/sdk/classes -Dcom.sun.star.lib.loader.unopath=/opt/openoffice.org3/program -jar standalone01.jar
Inici
Bootstrap
MultiComponentFactory
Raw Desktop
Desktop
Component loader
convert to URL
FileConverter
getFileURLFromSystemPath
Fitxer: file:///home/albert/Documents/prova.odt
Document existent
Escriu línies
XStorable
StoreAsURL
Tanca
Fi
albert@atenea:/media/Nuevo vol/wk-java/OOo-standalone-01/build$ 
  

Albert Baranguer
Barcelona 2010

Com fer una llibreria de macros java de l’OpenOffice.org que accedeixen a la textbox d’un form de Writer.

Sense cap mena de dubte, la millor forma de fer macros per a l’OpenOffice.org és amb el OOoBasic.

Dit això, en ocasions pot ser interessant plantejar aplicacions java que modifiquin documents d’OpenOffice.org, les aproximacions java a OpenOffice poden ser de tres formes principals:

1. Aplicació StandAlone que és capaç d’obrir un OpenOffice.org i manipular-ne els documents
2. Una llibreria de macros java que és invocada des d’un OpenOffice.org
3. Component UNO java que és invocat des del menú o des d’una macro en OOoBasic (o en java, o python…) i que és una extensió del core de l’OpenOffice.org

En aquest post d’avui, faré una llibreria de macros java que invocaré des de botons al document de text i que escriuran  frases al final del document i modificaran el text d’una textbox que anomenaré textbox1.


parcel-descriptor.xml
La llibreria de macros java la posaré a /home/elmeuusuari/.openoffice.org/3/user/Scripts/java/ de forma que formarà part del conjunt de “les meves macros”. La llibreria també podria formar part de “les macros de l’OpenOffice.org”, de forma que estaria compartida a tots els usuaris d’aquesta instal·lació d’OpenOffice.org i aleshores caldria posar-la a /opt/openoffice.org/basis3.2/share/Scripts/java. No és possible associar  macros java a un document.

Per compilar una macro java de l’OpenOffice.org és necesari enllaçar, almenys, les llibreries:
/opt/openoffice.org/ure/share/java/jurt.jar
/opt/openoffice.org/ure/share/java/ridl.jar
/opt/openoffice.org/basis3.2/program/classes/unoil.jar

A més, per a que l’OpenOffice.org sàpiga que té una nova llibreria de macros, cal preparar un fitxer descriptor, el parcel-descriptor.xml, on s’indica quina és la nova llibreria, en quin jar es troba i quin mètode corresponen a cada macro.

En el cas que m’ocupa, el parcel-descriptor.xml és el següent:


<?xml version=”1.0″ encoding=”UTF-8″?>
<parcel language=”Java” xmlns:parcel=”scripting.dtd”>
  <!– mètode 1 –>
  <script language=”Java”>
    <locale lang=”Es-ca”>
      <displayname value=”MacroProves.DispName”/>
      <description>Mostra un missatge</description>
    </locale>

    <functionname value=”com.serveisticiprogramarilliure.MacroProves.print”/>
    <logicalname value=”MacroProves.print”/>

    <languagedepprops>
      <prop name=”classpath” value=”OOo-java-macro-01.jar”/>
    </languagedepprops>
  </script>

  <!– mètode 2 –>
  <script language=”Java”>
    <locale lang=”Es-ca”>
      <displayname value=”MacroProves.DispName”/>
      <description>Mostra un missatge</description>
    </locale>
    <functionname value=”com.serveisticiprogramarilliure.MacroProves.print2″/>
    <logicalname value=”MacroProves.print2″/>

    <languagedepprops>
      <prop name=”classpath” value=”OOo-java-macro-01.jar”/>
    </languagedepprops>
  </script>
</parcel>

parcel. Document que agrupa les macros, conceptualment: la llibreria
script. Bloc que defineix cada macro
functioname. És el nom del mètode java que correspon a la macro
logicalname. El nom lògic de la macro
languagedepprops. Indica la llibreria jar que cal carregar
displayname. Nom de la llibreria
description. Descripció de la llibreria

Vaig per la llibreria pròpiament dita. Faré una classe amb el parell de mètodes print i print2 que seran els que invocaré des dels botons.

MacroProves.java

Vet aquí la classe: MacroProves.java

package com.serveisticiprogramarilliure;


  import com.sun.star.script.provider.XScriptContext;
  import com.sun.star.uno.UnoRuntime;
  import com.sun.star.text.XTextDocument;
  import com.sun.star.text.XTextRange;
  import com.sun.star.text.XText;
  import com.sun.star.awt.MouseEvent;
  import com.sun.star.container.XIndexAccess;
  import com.sun.star.container.XNameAccess;
  import com.sun.star.container.XNameContainer;
  import com.sun.star.drawing.XDrawPage;
  import com.sun.star.drawing.XDrawPageSupplier;
  import com.sun.star.form.XForm;
  import com.sun.star.form.XFormComponent;
  import com.sun.star.form.XFormsSupplier;
  import com.sun.star.frame.XModel;
  import com.sun.star.frame.XController;


  /**
   *  MacroProves
   *
   */


  public class MacroProves {
    public static void main(String[] args) {
     // TODO Auto-generated method stub
    }


    public static void print(XScriptContext xSc) {
      // getting the text document object
      XTextDocument xtextdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());
      XText xText = xtextdocument.getText();
      XTextRange xTextRange = xText.getEnd();
      xTextRange.setString( “MacroProves.print.\n” );
      snippet(xSc, “a”);
    }// printHW




    public static void print(XScriptContext xSc, MouseEvent meEvent) {
        // getting the text document object
        XTextDocument xtextdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());
        XText xText = xtextdocument.getText();
        XTextRange xTextRange = xText.getEnd();
        xTextRange.setString( “MacroProves.print. Invocació des de botó.\n” );
        snippet(xSc, “b”);
    }   


    public static void print2(XScriptContext xSc) {
        // getting the text document object
        XTextDocument xtextdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());
        XText xText = xtextdocument.getText();
        XTextRange xTextRange = xText.getEnd();
        xTextRange.setString( “MacroProves.print2.\n” );
        snippet(xSc, “c”);
      }// printHW




      public static void print2(XScriptContext xSc, MouseEvent meEvent) {
          // getting the text document object
          XTextDocument xtextdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());
          XText xText = xtextdocument.getText();
          XTextRange xTextRange = xText.getEnd();
          xTextRange.setString( “MacroProves.print2. Invocació des de botó.\n” );
          snippet(xSc, “d”);
      }    
    
    
    private static void snippet(XScriptContext xSc, String sXY) {
        try {
              XTextDocument xdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());
              XController xcontroller = (XController) UnoRuntime.queryInterface(XController.class, xdocument.getCurrentController());
              XModel xmodel = (XModel) UnoRuntime.queryInterface(XModel.class, xcontroller.getModel());
              XDrawPageSupplier xDrawPageSupplier = (XDrawPageSupplier) UnoRuntime.queryInterface(XDrawPageSupplier.class, xmodel);
              XDrawPage xDrawPage = xDrawPageSupplier.getDrawPage();
              XFormsSupplier xFormsSupplier = (XFormsSupplier) UnoRuntime.queryInterface(XFormsSupplier.class, xDrawPage);
              XNameContainer xNameContainer = xFormsSupplier.getForms();
              XIndexAccess xIndexAccess = (XIndexAccess) UnoRuntime.queryInterface(XIndexAccess.class, xNameContainer);
              XForm xForm = (XForm) UnoRuntime.queryInterface(XForm.class, xIndexAccess.getByIndex(0));
              XNameAccess xNameAccess = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xForm);
              XFormComponent xFormComponent = (XFormComponent) UnoRuntime.queryInterface(XFormComponent.class, xNameAccess.getByName(“textbox1”));
              XTextRange xTextRange = (XTextRange) UnoRuntime.queryInterface(XTextRange.class, xFormComponent);
              String sActual = xTextRange.getString();
              xTextRange.setString(sActual + ” ” + sXY);
        } catch (Exception e4) {
         // getByName 
         e4.printStackTrace();
        }
    }    
  }  

Vàries a coses a comentar:

1. Els mètodes print i print2 estan sobrecarregats. amb signatures (XScriptContext xSc) i  (XScriptContext xSc, MouseEvent meEvent). El motiu és que depenent de l’esdeveniment que invoca la macro es passa un segon argument que pot ser de diferents tipus.

  • Els mètodes amb un únic argument són invocats des del Dialeg d’Executar Macro, o des d’un menú, o des d’una barra d’eines. Als fòrums d’OpenOffice.org es diu que quan s’invoca el mètode de macro des d’una barra d’eines es rep un Short com segon argument. Però en les meves proves amb un OOo3.2.1 sobre Ubuntu 10.04 quan invoco des d’una barra d’eines no es rep aquest segon argument.
  • Els mètodes de la macro que s’executen en resposta a un esdeveniment de mouse, com “en prémer el botó del ratolí”, de la llista d’esdeveniments del botó, reben un MouseEvent.
  • Els mètodes de la macro que s’executen en resposta a un esdeveniment d’acció, com “Executa l’acció” de la llista d’esdeveniments del botó, reben un ActionEvent.
  • Els mètodes de la macro que s’executen en resposta a un esdeveniment de focus, com “En rebre el focus” de la llista d’esdeveniments del botó, reben un FocusEvent.

2. Els mètodes de macro han de ser public static void

3. El primer argument que reben els mètodes de macro sempre és un XScriptContext.
XScripContext és una interfase UNO que permet accedir a la resta d’interfases i de serveis (la funcionalitat) del document.

En particular, la forma d’accedir al Document de Writer a partir del XScripContext és:

XTextDocument xtextdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());


3.1 La invocació al UnoRuntime .queryInterface és un mantra que es repeteix constantment a l’hora d’accedir als diferents elements del document. Quan invoco UnoRuntime.queryInterface el resultat és que l’objecte fa un “casting” a la interfase demanada. Adonem-nos que les interfases no són interfases java, sinó interfases UNO. Un objecte UNO “implementa” un conjunt de interfases UNO. La forma d’accedir a les diferents interfases és amb el mecanisme UnoRuntime.queryInterface. 


3.2 L’altra clau de l’èxit en el treball amb macros java és disposar de documentació sobre les interfases i els mètodes que les defineixen. No hi ha més remei que fer servir la documentació: Hi han centenars (milers?) d’interfases. Consulteu http://api.openoffice.org/.

Per exemple,  XScriptContext té els següents mètodes:







getDocument Obtain the document reference on which the script can operate  
getInvocationContext provides access to the context where the script was invoked  
getDesktop Obtain the desktop reference on which the script can operate  
getComponentContext Obtain the component context which the script can use to create other uno components




I XTextDocument, aquests altres:







getText
reformat reformats the contents of the document.  




També he fet servir XText i XTextRange. En realitat XText és una interfase que deriva de la interfase XSimpleText, que a la seva vegada deriva de XTextRange. O sigui: XText té tots els mètodes de XSimpleText i de XTextRange. 


XText






insertTextContent inserts a content, such as a text table, text frame or text field.  
removeTextContent removes the specified content from the text object. 




XSimpleText






createTextCursor
createTextCursorByRange
insertString inserts a string of characters into the text.  
insertControlCharacter inserts a control character (like a paragraph break or a hard space) into the text.

XTextRange






getText
getStart
getEnd
getString
setString the whole string of characters of this piece of text is replaced.




Per tant, les línies 






XText xText = xtextdocument.getText();
XTextRange xTextRange = xText.getEnd();
xTextRange.setString( “MacroProves.print.\n” )




Es poden simplificar així:






xtextdocument.getText().getEnd().setString( “MacroProves.print.\n” )






3.3 la tercera clau de l’èxit, juntament amb el mantra UnoRuntime.queryInterface i la documentació és la comprensió de l’estructura interna dels documents d’OpenOffice.org.


El mètode snippet és molt interessant i desvetlla part d’aquesta estructura interna. Vet aquí el detall del que fa:


Primer de tot, obtinc la interfase del XTextDocument a partir de l’ScriptContext 






XTextDocument xdocument = (XTextDocument) UnoRuntime.queryInterface(XTextDocument.class, xSc.getDocument());

Ara obtinc el Controlador, interfase XController,  del XTextDocument. Els document d’OpenOffice segueixen internament el patró FMC (Frame Model Controller) que és una variant del prou conegut patró MVC (Model View Controller).  
XController xcontroller = (XController) UnoRuntime.queryInterface(XController.class, xdocument.getCurrentController());


A través del controlador obtinc el model, interfase XModel, que representa les dades i els objectes del document.
XModel xmodel = (XModel) UnoRuntime.queryInterface(XModel.class, xcontroller.getModel());


Del model obtinc el proveïdor de la DrawPage, és dir la interfase XDrawPageSupplier. Al tractar-se d’un TextDocument només té una DrawPage. Si es tractes d’un document de Draw, Impress o Calc, aleshores podria tenir vàries DrawPages (en plural), la interfase que caldria obtenir seria la XDrawPagesSupplier (adoneu-vos del plural). 
La DrawPage és un contenidor dels objectes dibuixats al document. En particular conté formularis i els controls del formulari.  
XDrawPageSupplier xDrawPageSupplier = (XDrawPageSupplier) UnoRuntime.queryInterface(XDrawPageSupplier.class, xmodel);


Un cop tinc el proveïdor, obtinc la DrawPage. En aquest cas, el proveïdor em proporciona l’objecte i per això és una crida de mètode directa. No cal invocar la interfase amb UnoRuntime.queryInterface.
XDrawPage xDrawPage = xDrawPageSupplier.getDrawPage();


De la DrawPage obtinc la interfase del proveïdor de Forms (XFormsSupplier):
XFormsSupplier xFormsSupplier = (XFormsSupplier) UnoRuntime.queryInterface(XFormsSupplier.class, xDrawPage);


El proveïdor de forms em dona, directament, “l’enumeració” (XNameContainer) dels forms.    
XNameContainer xNameContainer = xFormsSupplier.getForms();


De l’enumeració dels forms obtinc una interfase per accedir als forms per índex (XIndexAccess)
XIndexAccess xIndexAccess = (XIndexAccess) UnoRuntime.queryInterface(XIndexAccess.class, xNameContainer);


En el cas que ens ocupa només hi ha un form, per tant és el d’índex 0. Obtinc la interfase XForm  per índex.
XForm xForm = (XForm) UnoRuntime.queryInterface(XForm.class, xIndexAccess.getByIndex(0));


Ara buscaré dins del form al meu objecte textbox1 per nom, per tant obtinc una interfase d’acceś per nom. (XNameAccess)
XNameAccess xNameAccess = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xForm);


Finalment obtinc el component textbox1 (XFormComponent) 
XFormComponent xFormComponent = (XFormComponent) UnoRuntime.queryInterface(XFormComponent.class, xNameAccess.getByName(“textbox1”));


I del component, n’obtinc les dades de text (XtextRange)
XTextRange xTextRange = (XTextRange) UnoRuntime.queryInterface(XTextRange.class, xFormComponent);


N’obtinc el text actual i li afegeixo una lletra
String sActual = xTextRange.getString();
xTextRange.setString(sActual + ” ” + sXY)



build.xml


I tot això com és desplega?
Com he dit al principi, posaré la llibreria de macros a /home/elmeuusuari/.openoffice.org/3/user/Scripts/java/

Si observem aquesta carpeta trobarem l’exemple de la macro java HelloWorldJava que és el que ha servit de base per al post d’avui. A la carpeta HelloWorldJava hi trobem el jar, el fitxer font java i el parcel-descriptor.xml.  Es tracta de fer el mateix.

Una forma de fer ho és amb un fitxer d’ant. Jo he fet servir aquest:
  

<project name=”MacroProves” default=”dist” basedir=”.”>
  <description>
  compila i genera el jar de la macro per a l’OpenOffice
  </description>
  
  <!– estableix propietats globals –>
  <property name=”src” location=”src”/>
  <property name=”build” location=”build”/>
  <property name=”config” location=”config”/>
  <property name=”dist”  location=”MacroProves”/>
  <property name=”cp-java-OOo-1″ 
            location=”/opt/openoffice.org/ure/share/java/” />
  <property name=”cp-java-OOo-2″ 
            location=”/opt/openoffice.org/basis3.2/program/classes/” />
  <property name=”OOo-user-macros”
            location=”/home/albert/.openoffice.org/3/user/Scripts/java/” />
  <property name=”OOo-shared-macros” 
            location=”/opt/openoffice.org/basis3.2/share/Scripts/java/” />



  <target name=”init” depends=”clean”>
    <!– time stap –>
    <tstamp/>
    <!– Crea el directory build utilitzat en la compilació –>
    <mkdir dir=”${build}”/>
  </target>


  <target name=”compile” depends=”init”
    description=”compila els fitxers font” >


    <!– Compila les fonts java de ${src} en ${build} –>
    <javac srcdir=”${src}” destdir=”${build}”>
      <classpath>
        <pathelement location=”${cp-java-OOo-1}/jurt.jar”/>
       <pathelement location=”${cp-java-OOo-1}/ridl.jar”/>
       <pathelement location=”${cp-java-OOo-2}/unoil.jar”/>
      </classpath>
   </javac>
  </target>


  <target name=”dist” depends=”compile” description=”genera la  distribució” >
    <!– Crea el directori de distribució –>
    <mkdir dir=”${dist}”/>


    <!– Posa tot el que hi ha a ${build} dins de OOo-java-macro-01.jar –>
    <jar jarfile=”${dist}/OOo-java-macro-01.jar” basedir=”${build}” />
  
   <!– també posa el fitxer font –>
   <copy todir=”${dist}”> 
   <fileset dir=”${src}” />
   </copy>
  
   <!– i el parcel-descriptor.xml –>
   <copy file=”${config}/parcel-descriptor.xml” todir=”${dist}” />
  
   <!– i ho copia tot a la carpeta de macros java de l’usuari–>
   <copy todir=”${OOo-user-macros}/MacroProves”>
   <fileset dir=”${dist}” />
   </copy>
  
   <!– també ho podria copiar a la carpeta de macros java compartides –>
   <!–
   <copy todir=”${OOo-shared-macros}/HelloWorldJava”>
   <fileset src=”${dist}” />
   </copy>
   –>
  </target>


  <target name=”clean” description=”fa neteja” >
    <!– esborra els directoris ${build} and ${dist}–>
    <delete dir=”${build}”/>
    <delete dir=”${dist}”/>
  </target>
</project>




El document de Writer amb el formulari
Finalment, creo un document de text amb Writer. En aquest document hi posaré una textbox que anomenaré textbox1; un botó que anomeno boto1, i enllaço l’esdeveniment “en prémer el botó del mouse” amb MacroProves.print; i un botó que anomeno boto2 i enllaço l’esdeveniment “en prémer el botó del mouse” amb MacroProves.print2;
Cada botó  activa respectivament una macro java.



Albert Baranguer
Barcelona 12/10/2010

Afegir número de versió automàticament amb ant / 30 octubre 2008

Afegir número de versió automàticament amb ant

Hi ha una optional task, la propertyfile,  de l’ant que permet generar un fitxer amb, per exemple, el número i la data de compilació.

El següent ho ficariem al build.xml

  <target name=”version”>
      <propertyfile
          file=”${resources}/version.txt”
          comment=”número de versió i data de compilació”>
        <entry  key=”BuildDate”
                type=”date”
                value=”now”
                pattern=”dd/MM/yyyy  HH-mm”/>   

        <entry  key=”BuildNumber”
                type=”int”
                value=”1″
                operation=”+”/>

      </propertyfile>
  </target>   

Si, per exemple,  aquest target “version” el fiquem com a dependència d’un target “compile” o “deploy”, el resultat serà que tindrem un fitxer version.txt a la carpeta {resources} en el que s’actualitzaran automàticament  la BuildDate i el BuildNumber.

per consultes i referèncial, anem a la pàgina d’ant, al manual d’ant en linea.