Sunday, December 23, 2007
Rhino: JavaScript for Java
Rhino is an open-source implementation of JavaScript written entirely in Java. It is typically embedded into Java applications to provide scripting to end users.
Downloads
How to get source and binaries.
Documentation
Information on Rhino for script writers and embedders.
Help
Some resources if you get stuck.
Wednesday, April 04, 2007
Generate an XML Document from an Object Model with JAXB 2
- Use simple file I/O or javax.xml.stream.XmlStreamWriter.
- Use XML serialization with java.beans.XMLEncoder, which generates an XML representation of a Java Bean in the same way ObjectOutputStream can be used to create binary representation of Serializable objects.
- Use a dedicated library like XStream Play directly with SAX (Simple API for XML) or DOM (Document Object Model) through the JAXP API.
Yet despite XML and Java technology being natural partners in data exchange, mapping a Java object model into XML and vice versa can be a mystery. Consider JAXB as your solution. JAXB (Java Architecture for XML Binding) enables you to perform XML-to-Java data binding and generate Java classes from XML schemas and vice-versa. It's portable, easy to use, and it offers functionalities such as XML validation and customization using annotation and adapters. (Figure 1 illustrates the uses of JAXB.)
Figure 1. JAXB Architecture |
The JAXB API, defined in the javax.xml.bind package, is a set of interfaces and classes through which applications communicate with code generated from a schema. The center of the JAXB API is the javax.xml.bind.JAXBContext class. JAXBContext is an abstract class that manages the XML/Java binding. It can be seen as a factory because it provides:
- An Unmarshaller class that deserializes XML into Java and optionally validates the XML (using the setSchema method).
- A Marshaller class that serializes an object graph into XML data and optionally validates it.
First of all, JAXB can define a set of classes into an XML schema by using a schema generator. It also enables the reverse action, allowing you to generate a collection of Java classes from a given XML schema through the schema compiler. The schema compiler takes XML schemas as input and generates a package of Java classes and interfaces that reflect the rules defined in the source schema. These classes are annotated to provide the runtime framework with a customized Java-XML mapping.
JAXB also can generate a Java object hierarchy from an XML schema using a schema generator, or give an object Java hierarchy to describe the corresponding XML schema. The runtime framework provides the corresponding unmarshalling, marshalling, and validation functionalities. That is, it lets you transform an XML document into an object graph (unmarshalling) or transform an object graph into XML format (marshalling).
These capabilities are why JAXB is often associated with web services. Web services use the API to transform objects into messages that they then send through SOAP. This isn't a web services article, however. Using an example address book application for a fictional music company called Watermelon, it focuses on JAXB's XML binding features for round-tripping XML to Java.
What You Need |
---|
JAXB (this article uses version 2.1.2 ) |
JUnit 4.1 |
Ant 1.7 |
MySQL 5 and TopLink Essentials as the implementation of JPA |
Watermelon sells and delivers music products such as instruments, amplifiers, scores, books, and so on. In its address book, the company stores two types of customers: individuals and companies. Each has a home address and a set of delivery addresses. Each delivery address can be used on weekends and/or evenings and/or weekdays. I will include this information by adding tags to an address. Figure 2 shows the business model for Watermelon's address book.
Figure 2. Business Model for Watermelon's Address Book |
Watermelon wants to send the details of some of its customers to a partner in XML format, so it needs an XML document of its object model for a given customer. With JAXB, this is really easy to do. The following code creates an instance of an Individual and then sets its attributes (first name, last name…), a home address, and two delivery addresses that are tagged. Once the objects are all set, it uses the javax.xml.bind.Marshaller to generate an XML representation of the Individual object, which is then displayed:
Listing 1: Creates an XML Representation of an Individual
// Instantiates Tag objects
Tag tag1 = new Tag("working hours");
Tag tag2 = new Tag("week-ends");
Tag tag3 = new Tag("mind the dog");
// Instantiates an individual object with home address
calendar.set(1940, 7, 7, 0, 0, 0);
Individual individual = new Individual(1L, "Ringo", "Starr", "+187445", "ringo@star.co.uk", calendar.getTime());
individual.setHomeAddress(new Address(2L, "Abbey Road", "London", "SW14", "UK"));
// Instantiates a first delivery address
Address deliveryAddress1 = new Address(3L, "Findsbury Avenue", "London", "CE451", "UK");
deliveryAddress1.addTag(tag1);
deliveryAddress1.addTag(tag3);
individual.addDeliveryAddress(deliveryAddress1);
// Instantiates a second delivery address
Address deliveryAddress2 = new Address(4L, "Camden Street", "Brighton", "NW487", "UK");
deliveryAddress2.addTag(tag1);
deliveryAddress2.addTag(tag2);
individual.addDeliveryAddress(deliveryAddress2);
// Generates XML representation of an individual
StringWriter writer = new StringWriter();
JAXBContext context = JAXBContext.newInstance(Customer.class);
Marshaller m = context.createMarshaller();
m.marshal(individual, writer);
System.out.println(writer);
Author's note: The Marshaller.marshal() method takes an object and marshals it into several supports. The example uses a StringWriter to have a string representation of the XML document, but you could use a FileOutputStream to store the XML into a file, as follows:
File file = new File("customer.xml");
FileOutputStream outputFile = new FileOutputStream(file);
m.marshal(individual, outputFile);
The code creates a new instance of JAXBContext by using the static method newInstance, to which it passes the root class that the application needs to manipulate (Customer). From the created Marshaller object, it then calls the marshal method that generates the XML representation of an individual into a StringWriter.
The only thing left to do is add an @XmlRootElement annotation to the Customer class. The @XmlRootElement annotation notifies JAXB that the annotated class is the root element of the XML document. If this annotation is missing, JAXB will throw an exception. If you add it and run the code, you will get the following XML document:
Listing 2: XML Representation of an Individual
London
UK
3
Findsbury Avenue
working hours
mind the dog
CE451
Brighton
UK
4
Camden Street
working hours
week-ends
NW487
ringo@star.co.uk
London
UK
2
Abbey Road
SW14
1
+187445
With just one annotation (@XmlRootElement), a Marshaller object, and the coding-by-exception approach that guides JAXB (you need to add custom code only when the default is inadequate), you can easily get an XML representation of your object graph. The root element represents the Customer object, and it includes all the attributes (one home address, two delivery addresses, an ID, a phone number, and so on).
Watermelon and its business partner are not completely happy with the structure of the previous XML document (Listing 2). They would like to get rid of some information (address identifiers, tags), format the date of birth, order some attributes, and rename others. Thanks to the annotations of the javax.xml.bind.annotation package, JAXB provides a way to customize and control the XML structure.
First of all, you don't really want to map customers. Rather, you want to map individuals and companies (that means getting rid of the element and instead using or as root elements). To notify JAXB not to take the abstract Customer class into account, you have to get rid of the @XmlRootElement annotation and make the class transient with @XmlTransient. You can use this annotation on a class or an attribute. It informs JAXB not to take it into account when creating the XML representation.
An XML document is made of elements (value) and attributes (). JAXB uses two annotations to differentiate them: @XmlAttribute and @XmlElement. Each annotation has a set of parameters that allows you to rename an attribute, allow null value or not, give it a default value, etc. The following code uses these annotations to turn id into an XML attribute (instead of an element) and to rename the delivery address element (address instead of deliveryAddresses):
@XmlTransient
public abstract class Customer {
@XmlAttribute
protected Long id;
protected String telephone;
protected String email;
protected Address homeAddress;
@XmlElementWrapper(name = "delivery")
@XmlElement(name = "address")
protected List deliveryAddresses = new ArrayList();
// Constructors, getters, setters
}
This code uses another interesting annotation, @XmlElementWrapper. It generates a wrapper element around the collections of delivery addresses. If you look back at Listing 2, you'll see two elements. With the code above, you get one element that wraps two
elements.
Talking about addresses, you want to get rid of the identifier and the tags from the XML document. For that, use the @XmlTransient annotation. To rename an element, just use the name property of the @XmlElement annotation. The following code renames the attribute zipcode into a element:
@XmlType(propOrder = {"street", "zipcode", "city", "country"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Address {
@XmlTransient
private Long id;
private String street;
private String city;
@XmlElement(name = "zip")
private String zipcode;
private String country;
@XmlTransient
private List tags = new ArrayList();
// Constructors, getters, setters
}
Notice the @XmlType annotation on the top of the class. It allows JAXB to map a class or an enum to a XML schema type. You can use it to specify a namespace or to order attributes using the propOrder property, which takes a list of names of attributes and generates the XML document following this order.
Author's note: The Address class is annotated with @XmlAccessorType. It controls whether fields or Java bean properties are serialized. XmlAccessType represents the possible values (FIELD, NONE, PROPERTY, PUBLIC_MEMBER). This example uses the FIELD value, which notifies JAXB to use the attributes for the XML mapping (and not getters). |
Table 1 below shows three different extracts of XML documents:
- The default customer XML document that you would get with coding-by-exceptions (with no annotation except @XmlRootElement)
- The customizations done to the customer class (the identifier becomes an attribute and the delivery addresses are wrapped into a element)
- Taken into account the annotations of the Address class (no identifier and no tags are displayed; elements are in a certain order)
Default XML Representation | Annotated Customer Class | Annotated Address Class |
---|---|---|
London UK 3 Findsbury working hours mind the dog CE451 Brighton UK 4 Camden working hours week-ends NW487 (...) | London UK 3 Findsbury working hours mind the dog CE451 Brighton UK 4 Camden working hours week-ends NW487 (...) | Findsbury CE451 London UK Camden NW487 Brighton UK (...) |
Table 1. Customizations of the XML Representation of a Customer |
You also can annotate the concrete classes Company and Individual to customize the mapping (see table below). First of all, being the root of the XML document now, both have to use the @XmlRootElement annotation where they can specify the XML namespace http://www.watermelon.example/customer. Like the Address, the example uses the @XmlType.propOrder to order the attributes. Note that in the list of attributes, you can use the ones inherited by the super class Customer, such as id, email, telephone, homeAddress, and so on.
Annotated Company Class | Annotated Individual Class |
---|---|
@XmlRootElement(name = "company", namespace = "http://www.watermelon.example/customer") @XmlType(propOrder = {"id", "name", "contactName", "telephone", "email", "numberOfEmployees", "homeAddress", "deliveryAddresses"}) @XmlAccessorType(XmlAccessType.FIELD) public class Company extends Customer { @XmlAttribute private String name; private String contactName; private Integer numberOfEmployees; // Constructors, getters, setters } | @XmlRootElement(name = "individual", namespace = "http://www.watermelon.example/customer") @XmlType(propOrder = {"id", "lastname", "firstname", "dateOfBirth", "telephone", "email", "homeAddress", "deliveryAddresses"}) @XmlAccessorType(XmlAccessType.FIELD) public class Individual extends Customer { private String firstname; @XmlAttribute private String lastname; @XmlJavaTypeAdapter(DateAdapter.class) private Date dateOfBirth; // Constructors, getters, setters } |
There is still one thing that Watermelon is not happy about: the date format. JAXB maps java.util.Date attributes into the default value. For example, the date of birth of an individual will be displayed as follow:
1940-08-07T00:00:00.781+02:00
To format this date (e.g., 07/08/1940), you have two choices:
- Use the datatype javax.xml.datatype.XMLGregorianCalendar instead of java.util.Date. The XMLGregorianCalendar gives a W3C XML representation for date/time datatypes and allows certain manipulations.
- Use an adapter. As you can see in the code above, the Individual class uses a @XmlJavaTypeAdapter annotation. @XmlJavaTypeAdapter(DateAdapter.class) notifies JAXB to use the custom adapter called DateAdapter when marshalling/unmarshalling the dateOfBirth attribute. Adapters are used when Java types do not map naturally to a XML representation. You can then adapt a bound type to a value type or vice versa.
To format the date, you have to write a class (DateAdpater) that extends XmlAdapter. It has to override the marshal and unmarshal methods, which the JAXB binding framework invokes during marshalling and unmarshalling. This way, you can marshal a date into a formatted string and vice versa. The following code just uses a java.text.SimpleDateFormat object to format the date into a string and vice-versa:
public class DateAdapter extends XmlAdapter {
DateFormat df = new SimpleDateFormat("dd/MM/yyyy");
public Date unmarshal(String date) throws Exception {
return df.parse(date);
}
public String marshal(Date date) throws Exception {
return df.format(date);
}
}
Now if you go back to Listing 1, when you call the Marshaller.marshal() method, the DateAdapter.marshal() is also called (@XmlJavaTypeAdapter(DateAdapter.class)) and the date of birth is then formatted. Here is the XML document you obtain:
Individual XML Document |
---|
Ringo 07/08/1940 +187445 ringo@star.co.uk Abbey Road SW14 London UK Findsbury Avenue CE451 London UK Camden Street NW487 Brighton UK |
Company XML Document |
Mr Father +14519454 contact@sony.com 25000 General Alley 75011 Paris FR St James St SW14 London UK Central Side Park 7845 New York US |
As Figure 1 showed, JAXB can also be used to unmarshal, generate, and compile a schema. So let's quickly talk about that. From what you've learned so far, you should understand unmarshalling. The idea is to take the previously obtained XML document and generate an object graph. For that, you get a JAXBContext, from which you create an Unmarshaller object. You then call the unmarshal method, passing an XML document (either from a String or a file), and it returns an instance of an Individual with its attributes:
// xmlString contains the XML document of an individual
StringReader reader = new StringReader(xmlString);
JAXBContext context = JAXBContext.newInstance(Individual.class);
Unmarshaller u = context.createUnmarshaller();
Individual individual = (Individual) u.unmarshal(reader);
System.out.println(individual.getFirstname());
An XML schema describes the structure of an XML document and it is written in XML syntax. Even if you don't know much about XML schema, you can generate one using the schemaGen tool provided by Sun's JAXB implementation (command line or Ant task). For example, schemaGen can take all the classes developed for this article and generate the XML schema in Listing 3. In that schema, you can see the Address, Company, Individual, and Tag classes described as complex types (download the code and you will see how schemaGen is invoked from Ant):
address">
company">
individual">
tag">
dateAdapter">
When you download JAXB, another tool comes along with it: the schema compiler (xjc). schemaGen allows to generate an XML schema from Java classes, and xjc does the opposite: from an XML schema, it creates annotated Java files. You can use it as a command line or as an Ant task. Take Listing 3, run xjc, and that will generate the class hierarchy (not the objects, the classes) that describes the Watermelon business model.
Some of you might already know of the fictional music company called Watermelon. I've used it in two previous articles about JPA ("Master the New Persistence Paradigm with JPA" and "Persistence Pays Offs: Advanced Mapping with JPA"). If you think of JAXB as a way to persist data into XML, JPA is its counterpart in terms of relational databases. In fact, both technologies rely heavily on annotations. That means that the same class can be annotated by JPA and JAXB annotations and, in this way, can give an XML representation as well as be persisted in a database.
For those of you interested in this double use of annotations, you will find an example in the code download. But just to give you a taste, here is what the Address class would look like:
@Entity
@Table(name = "t_address")
@XmlType(propOrder = {"street", "zipcode", "city", "country"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Address {
@XmlTransient
@Id @GeneratedValue
private Long id;
private String street;
@Column(length = 100)
private String city;
@Column(name = "zip_code", length = 10)
@XmlElement(name = "zip")
private String zipcode;
@Column(length = 50)
private String country;
@XmlTransient
@ManyToMany(cascade = CascadeType.PERSIST)
@JoinTable(name = "t_address_tag",
joinColumns = {@JoinColumn(name = "address_fk")},
inverseJoinColumns = {@JoinColumn(name = "tag_fk")})
private List tags = new ArrayList();
// Constructors, getters, setters
}
Friday, February 02, 2007
Joda Time
Si algo negativo se podía decir de ella es que al no ser estándar era un jar más a incluir en el proyecto. Ahora sus creadores la han llevado a Java Community Process y han creado el JSR 310 para intentar convertirla en parte estándar de la plataforma. Ahora mismo el JSR se encuentra en su primera fase; aún no ha sido aceptado. El 12 de febrero habrá una votación para decidir si se acepta o no. Esperemos que así sea y que esta útil librería se convierta en una parte estándar de la plataforma.
Para los que no la conocéis, aquí va un ejemplo de su uso:
DateTime dt = new DateTime();
int year = dt.getYear();
String monthText = dt.monthOfYear().getAsText(Locale.ENGLISH);
String monthInFrench = dt.monthOfYear().getAsText(Locale.FRENCH);
String dateAsISO8601Format = dt.toString();
Thursday, February 01, 2007
Jasypt
- Sigue los estándares y normas marcados por RSA tanto para message digesting como para cifrado basado en clave.
- Es completamente thread-safe.
- Puede usarse "a lo fácil", con dificultad casi cero, o "a lo power-user", de manera que se puede configurar mucho.
- Tiene un módulo de integración transparente con Hibernate: Se le dice en un mapping que tal o cual campos de una entidad son cifrados y cómo se cifran y listo, para nuestra aplicación es transparente y en la base de datos se guardan los datos cifrados (muy útil para datos de caracter personal, o para bases de datos con muchos usuarios de sólo lectura...).
- Se puede usar perfectamente en Spring. Todas las herramietnas de cifrado están pensadas de manera que sea sencillo usarlas desde un contenedor IoC. Además, el hecho de que sea thread-safe evita problemas en aplicaciones web debido al uso intensivo de singletons.
- Es muy configurable: se pueden hacer incluso cosas como que un "encryptor" al inicializarse le pida su clave por HTTPS (por ejemplo) a algún servidor remoto, para evitar tenerla en el código fuente.
Monday, January 29, 2007
JMaki framework para ajax
JMaki es un framework para construir aplicaciones web 2.0 usando las tecnlogías estándar para ellos: css, javascript y html pero además permite vincular de forma transparente estas tecnologías con tus componentes del lado del servidor.
A grandes rasgos, JMaki es solo una librería para usar los widgets principalmente de Dojo (y algunos de scriptaculous, google y yahoo) como tags de jsp o componentes JSF. De hecho su nombre viene de J por javascript y Maki que es "Wrapper" en japonés.
La lista de widgets que se pueden usar la pueden ver en esta galería. Sin embargo, JMaki tiene más características, como un sistema de publicación/subscripción para eventos generados por el widget, un sistema para comunicar los widgets con el código del lado del servidor usando AJAX e incluso una galería de layouts genéricos para sitios web que puedes usar como punto de partida para crear tus aplicaciones.
En el artículo enlazado, Carla Mott que es desarrolladora del proyecto y colaboradora del toolkit de Dojo, enseña a empezar a usar este framework usando netbeans y un plugin para dicho IDE; lo que vuelve sumamente fácil la integración de jMaki con aplicaciones java. Por ahora jMaki es aún Beta pero se espera pronto la versión 1.0
Por mi parte me parece un poco alarmante que ya con años con el hype de la web 2.0 sea hasta ahora que se haya creado una forma sencilla de usar css y componetes javascript para aplicaciones java, basta decir que en JSF se sigue haciendo todo con las viejas tablas html, mientras que en la vida real los diseñadores web hace mucho que migraron a los div y a los estilos css para realizar páginas web.
¿Qué piensan de esta iniciativa? ¿Siguen usando el viejo html o han migrado a formas más sofisticadas propuestas por el paradigma web 2.0, acostumbra usar widgets javascript en sus aplicaciones? ¿Porqué en el mundo java parece que se promueve mucho un código robusto y eficiente del lado del servidor pero se deja a un lado una Vista que sea atractiva y usable para el cliente?
Building web 2.0 apps with jMaki
Posted by carlavmott on January 03, 2007 at 10:03 PM | Permalink | Comments (2)
jMaki is a light weight framework for developing JavaScript centric Ajax-enabled web applications using Java, Phobos and PHP. In this blog I will show how to use one of the layout templates, various widgets, the dynamic container and the glue feature that are part of jMaki to quickly build a web 2.0 application.
I will use the NetBeans plugin to build the app as that makes life even easier but the plugin is not required. I have included the tags I used in the application below, so regardless of the environment you are familiar with, you can build this app too. Also see the JSP tutorial for more information on building an application without an IDE.
The web application I will build uses the standard layout template, the Dojo fisheye widget for navigation, the Yahoo Geocoder and Google map widgets. The fisheye widget comes with default images of some of the contributors to jMaki which I will use even if other images would better fit the application.
Start by downloading the NetBeans plugin 1.6.3 which supports jMaki 1.0 Beta 2. If you have not used the jMaki plugin for NetBeans see this page for pointers to some screencasts to see how to add the plugin and create a simple web app.
In NetBeans, create a new project that is a web application, click next, just use the default web application name for this project, click next, select the jMaki framework and click finish. First thing to do in this project is to create a new file that is a stylized JSP page and call it "index" and select the standard layout. This layout has a a menu bar on top, a left side bar I'll use for navigation and a main content area in the center of the page. See below for details.
Look at the "index.jsp" page that is generated for you by NetBeans. You will see that it contains all the CSS rules needed to display such a page. Now we need to add content to the div tags. In this application I will use the Dojo fisheye widget in the left side bar. Using the jMaki palette on the right drag and drop the fisheye widget to the page in the div tag for the left side bar. Highlight the widget and click on the jMaki item on the menu bar. This will bring up the component editor. For this application I want the fisheye widget to have a verticle orientation and the labels to be displayed on the right of the image. Make those changes using the component editor and save the file.
Next create a new JSP page called "mapit" and in that page add two widgets, the Yahoo Geo coder widget and the Google Map widget and save that file. Note that you will not need the JavaScript code that is generated when the widget is dropped in the page. I have therefore removed it from the application here.
So far I have the pieces of the application and now I need to put them together. There are two parts to this. First, in the "mapit.jsp" page I want the user to type a location in the text box and a map of that location to appear below it. This will be done automatically (no extra code needed) because of the sample JavaScript code that is in the glue.js file provided with the jMaki release. You will find the glue.js file in the resources directory of your web app. Glue code is specific to an application so you can modify what is there or provide action handlers as needed by your application. Because each of the widgets in jMaki pubishes a topic when a user performs some action, developers only need to write action handlers for the events they are interested in. In the case of the Yahoo Geocoder widget, when the user clicks on the button an event is published to the "/yahoo/geocoder" topic along with the latitude and longitude of the specified location. The glue code contains the action handler for that topic. That action handler gets the location data from the Yahoo Geocoder and passes it to any Yahoo or Google maps declared in the page. In this example, I used the Google map widget. See the glue code page for more details on what is happening.
Next we want to load "mapit.jsp" in the main content area of our web app when a user selects the first element in the fisheye widget. This is done behind the scenes using the injector feature. Go back to the index.html file and add the jMaki dcontainer widget to the main content div. By simply adding the dcontainer widget to that section it it tells jMaki to load any JavaScript or CSS content from a page refered to by a url to the current page (in this case in the main content div). Since the "mapit.jsp" page uses Google maps we must use the iframe property on the dcontainer widget. This is because google maps uses document.write when rendering the page which could overwrite the content of the current page so we need to put the widget in an iframe like container for the rendering to happen correctly.
<a:ajax name="jmaki.dcontainer" args="{iframe: true}"/>
Finally, to the fisheye widget add the url attribute referring to the "mapit.jsp" page to the first item in the widget. This tells the injector code which page to load in the main content area. Other widgets such as the Dojo tree and jMaki menu have a url attribute for the same purpose.
<a:ajax name="dojo.fisheye" args="{labelEdge:'right', items:[{'iconSrc':'https://ajax.dev.java.net/images/blog_murray.jpg','url':'mapit.jsp', 'caption':'You are here!'},{'iconSrc':'https://ajax.dev.java.net/images/chinnici.jpg', 'url': 'table.jsp', 'caption':'test3'}, {'iconSrc':'https://ajax.dev.java.net/images/JayashriVisvanathan.jpg','caption':'test4'}], orientation:'vertical'}"/>
The fisheye widget has the url to the page and now needs to broadcast that url to the dcontainer. To do that add the following code to the glue.js file. Note that this is similar to what was done in the glue.js file for the Yahoo Geocoder described above.
/* Programatically Register the glue */ jmaki.addGlueListener({topic : "/dojo/fisheye",action: "call", target: { object: "jmaki.carla",functionName: "fishEyeEvent"}}); jmaki.carla = { fishEyeEvent : function(args) { alert("sending out dcontainer event to load the url: " + args.target.url); jmaki.publish("/jmaki/dcontainer", args.target.url); } }
When the user clicks on the first item it will broadcast an event including the url to the page should be loaded. The dcontainer in the main content area is listening for such events and will load the page there. Save all changes and select run to build and deploy the application. The web application should come up in a browser.
Click on the first item in the fisheye and you will see the "mapit.jsp" page loaded in the main content area. Next type in a location to the Yahoo Geo coder and click the Go button. A map of that location will be displayed.
The only thing left is to resize the containers. It turns out that the fisheye widget doesn't automatically resize to fit the container. To fix this edit the jmaki-standard.css file which contains the application page CSS code for the standard layout we selected. I modified the file as follows.
.content { margin: 0 0 0 100px; border: 1px solid #000000; } .leftSidebar { float: left; width: 50px; height: 150px; border: 1px solid #000000; }
Make the above changes and save. Reload the page in your browser and you should see the left side bar now fits the fisheye widget. Notice that the app behaves correctly when you resize your page. Now it turns out that if you look at the fisheye widget you will see that I have added a url attribute to the second item in the widget. In that case, I want to bring up a table when that item is clicked. The glue code that I added for the fisheye widgets handles both cases because it simply broadcasts the url it has. This way you can load either page in the center area depending on what the user clicks.
To complete the application create another JSP page called "table" and add the code below. Here I added an editable Dojo table which allows you to sort the contents by clicking on the different columns.
<a:ajax name="dojo.table" value="{columns: { 'title' : 'Title', 'author':'Author', 'isbn': 'ISBN #', 'description':'Description'}, rows:[['JavaScript 101', 'Lu Sckrepter','4412', 'Some long description'], ['Ajax with Java', 'Jean Bean','4413', 'Some long description']]}" />
Save the "table.jsp" file and reload the application in your browser. This time click on the second item in the fisheye widget and see the table displayed.
The jMaki framework allows you to quickly create complete web 2.0 applications that are based on standards. We think that this is a very useful tool and are always happy to hear your feedback.
jMaki beta 2 outPosted by carlavmott on December 28, 2006 at 12:40 PM | Permalink | Comments (3)
jMaki beta 2 release is out and contains several key improvements and bug fixes over beta 1.
Specifically, the dynamic container which loads pages on demand has been improved overall and with additional fixes when running in IE. There is a new widget model where widgets have a constructor and are now placed in their own namespace to avoid collisions in the global JavaScript namespace. The JSF components are working the same as the JSP components. There are updates to the jMaki ibrowser and Yahoo maps and of course bug fixes.
Let us know what you think about this release. We are planning on pushing a final release early Feb so want to get your feedback soon.
jMaki is a framework that provides a lightweight model for building Ajax enabled web applications using standards-based technologies. jMaki allows you to use widgets from popular toolkits
jMaki beta candidate available
Posted by carlavmott on November 29, 2006 at 10:24 PM | Permalink | Comments (0)
The latest jMaki release is the beta release candidate for the next major jMaki release. It has several new features including
- The addition of containers (Yahoo tabbed view, Dojo tabbed view and jMaki dynamic)
- Templates to define styles
- jMaki glue used to tie widgets together using publish/subscribe mechanism
- Support to load all resources from the classpath for JSF view of jMaki
At the same time there is also a jMaki release available which supports PHP 5. See Greg's blog for examples on how to use jMaki in your PHP application.
This week we're in the process of updating the online documentation and fixing critical bugs. Download a release, try it out and let us know what you think. Beta release is schedule for next week.
Wednesday, January 17, 2007
Uso correcto de las excepciones
Cuando las excepciones se corresponden a bugs o fallos de infraestructura (disco duro lleno, no hay red, la base de datos lanza una excepción...) las excepciones deberían ser uncheked porque el código cliente no va a poder hacer nada para gestionar la excepción. Si no hay red, por ejemplo, no hay nada que hacer. Estas excepciones deberían capturarse muy cerca de la interfaz de usuario, probablemente en algún tipo de bucle que gestione todos los eventos. El resto del código no debería ocuparse de ellas, ni capturarlas ni lanzarlas. Capturar o lazar este tipo de excepciones lo único que consigue es escribir un montón de código inútil.
Según el autor del artículo, las excepciones en Java sí están bien pensadas (cosa en la que no está de acuerdo todo el mundo) pero algunas partes de la librería estándar hacen un uso incorrectas de ellas lo cual lleva a pensar que hay algún problema con las excepciones, cuando el problema está en la librería.
Abstract
One of the most important architectural decisions a Java developer can make is how to use the Java exception model. Java exceptions have been the subject of considerable debate in the community. Some have argued that checked exceptions in the Java language are an experiment that failed. This article argues that the fault does not lie with the Java model, but with Java library designers who failed to acknowledge the two basic causes of method failure. It advocates a way of thinking about the nature of exceptional conditions and describes design patterns that will help your design. Finally, it discusses exception handling as a crosscutting concern in the Aspect Oriented Programming model. Java exceptions are a great benefit when they are used correctly. This article will help you do that.
Why Exceptions Matter
Exception handling in a Java application tells you a lot about the strength of the architecture used to build it. Architecture is about decisions made and followed consistently at all levels of an application. One of the most important decisions to make is the way that the classes, subsystems, or tiers within your application will communicate with each other. Java exceptions are the means by which methods communicate alternative outcomes for an operation and therefore deserve special attention in your application architecture.
A good way to measure the skill of a Java architect and the development team's discipline is to look at exception handling code inside their application. The first thing to observe is how much code is devoted to catching exceptions, logging them, trying to determine what happened, and translating one exception to another. Clean, compact, and coherent exception handling is a sign that the team has a consistent approach to using Java exceptions. When the amount of exception handling code threatens to outweigh everything else, you can tell that communication between team members has broken down (or was never there in the first place), and everyone is treating exceptions "their own way."
The results of ad hoc exception handling are very predictable. If you ask team members why they threw, caught, or ignored an exception at a particular point in their code, the response is usually, "I didn't know what else to do." If you ask them what would happen if an exception they are coding for actually occurred, a frown follows, and you get a statement similar to, "I don't know. We never tested that."
You can tell if a Java component has made effective use of Java exceptions by looking at the code of its clients. If they contain reams of logic to figure out when an operation failed, why it failed, and if there's anything to do about it, the reason is almost always because of the component's error reporting design. Flawed reporting produces lots of "log and forget" code in clients and rarely anything useful. Worst of all are the twisted logic paths, nested try/catch/finally blocks, and other confusion that results in a fragile and unmanageable application.
Addressing exceptions as an afterthought (or not addressing them at all) is a major cause of confusion and delay in software projects. Exception handling is a concern that cuts across all parts of a design. Establishing architectural conventions for exceptions should be among the first decisions made in your project. Using the Java exception model properly will go a long way toward keeping your application simple, maintainable, and correct.
Challenging the Exception Canon
What constitutes "proper use" of Java's exception model has been the subject of considerable debate. Java was not the first language to support exception-like semantics; however, it was the first language in which the compiler enforced rules governing how certain exceptions were declared and treated. Compile-time exception checking was seen by many as an aid to precise software design that harmonized nicely with other language features. Figure 1 shows the Java exception hierarchy.
In general, the Java compiler forces a method that throws an exception based on java.lang.Throwable including that exception in the "throws" clause in its declaration. Also, the compiler verifies that clients of the method either catch the declared exception type or specify that they throw that exception type themselves. These simple rules have had far-reaching consequences for Java developers world-wide.
The compiler relaxes its exception checking behavior for two branches of the Throwable inheritance tree. Subclasses of java.lang.Error and java.lang.RuntimeException are exempt from compile-time checking. Of the two, runtime exceptions are usually of greater interest to software designers. The term "unchecked" exception is applied to this group to distinguish it from all other "checked" exceptions.
Figure 1. Java exception hierarchy
I imagine that checked exceptions were embraced by those who also valued strong typing in Java. After all, compiler-imposed constraints on data types encouraged rigorous coding and precise thinking. Compile-time type checking helped prevent nasty surprises at run-time. Compile-time exception checking would work similarly, reminding developers that a method had potential alternate outcomes that needed to be addressed.
Early on, the recommendation was to use checked exceptions wherever possible to take maximum advantage of the help provided by the compiler to produce error-free software. The designers of the Java library API evidently subscribed to the checked exception canon, using these exceptions extensively to model almost any contingency that could occur in a library method. Checked exception types still outnumber unchecked types by more than two to one in the J2SE 5.1 API Specification.
To programmers, it seemed like most of the common methods in Java library classes declared checked exceptions for every possible failure. For example, the java.io package relies heavily on the checked exception IOException. At least 63 Java library packages issue this exception, either directly or through one of its dozens of subclasses.
An I/O failure is a serious but extremely rare event. On top of that, there is usually nothing your code can do to recover from one. Java programmers found themselves forced to provide for IOException and similar unrecoverable events that could possibly occur in a simple Java library method call. Catching these exceptions added clutter to what should be simple code because there was very little that could be done in a catch block to help the situation. Not catching them was probably worse since the compiler required that you add them to the list of exceptions your method throws. This exposes implementation details that good object-oriented design would naturally want to hide.
This no-win situation resulted in most of the notorious exception handling anti-patterns we are warned about today. It also spawned lots of advice on the right ways and the wrong ways to build workarounds.
Some Java luminaries started to question whether Java's checked exception model was a failed experiment. Something failed for sure, but it had nothing to do with including exception checking in the Java language. The failure was in the thinking by the Java API designers that most failure conditions were the same and could be communicated by the same kind of exception.
Faults and Contingencies
Consider a CheckingAccount class within an imaginary banking application. A CheckingAccount belongs to a customer, maintains a current balance, and is able to accept deposits, accept stop payment orders on checks, and process incoming checks. A CheckingAccount object must coordinate accesses by concurrent threads, any of which may alter its state. CheckingAccount's processCheck() method accepts a Check object as an argument and normally deducts the check amount from the account balance. But a check-clearing client that calls processCheck() must be ready for two contingencies. First, the CheckingAccount may have a stop payment order registered for the check. Second, the account may not have sufficient funds to cover the check amount.
So, the processCheck() method can respond to its caller in three possible ways. The nominal response is that the check gets processed and the result declared in the method signature is returned to the invoking service. The two contingency responses represent very real situations in the banking domain that need to be communicated to the check-clearing client. All three processCheck() responses were designed intentionally to model the behavior of a typical checking account.
The natural way to represent the contingency responses in Java is to define two exceptions, say StopPaymentException and InsufficientFundsException. It wouldn't be right for a client to ignore these, since they are sure to be thrown in the normal operation of the application. They help express the full behavior of the method just as importantly as the method signature.
Clients can easily handle both kinds of exception. If payment on a check is stopped, the client can route the check for special handling. If there are insufficient funds, the client can transfer funds from the customer's savings account to cover the check and try again.
The contingencies are expected and natural consequences of using the CheckingAccount API. They do not represent a failure of the software or of the execution environment. Contrast these with actual failures that could arise due to problems related to the internal implementation details of the CheckingAccount class.
Imagine that CheckingAccount maintains its persistent state in a database and uses the JDBC API to access it. Almost every database access method in that API has the potential to fail for reasons unrelated to the implementation of CheckingAccount. For example, someone may have forgotten to turn on the database server, unplugged a network cable, or changed the password needed to access the database.
JDBC relies on a single checked exception, SQLException, to report everything that could possibly go wrong. Most of what could go wrong has to do with configuring the database, the connectivity to it, and the hardware it resides on. There's nothing that the processCheck() method could do to deal with these situations in a meaningful way. That's a shame, because processCheck() at least knows about its own implementation. Upstream methods in the call stack have an even smaller chance of being able to address problems.
The CheckingAccount example illustrates the two basic reasons that a method execution can fail to return its intended result. They are worthy of some descriptive terms:
- Contingency
- An expected condition demanding an alternative response from a method that can be expressed in terms of the method's intended purpose. The caller of the method expects these kinds of conditions and has a strategy for coping with them.
- Fault
- An unplanned condition that prevents a method from achieving its intended purpose that cannot be described without reference to the method's internal implementation.
Using this terminology, a stop payment order and an overdraft are the two possible contingencies for the processCheck() method. The SQL problem represents a possible fault condition. The caller of processCheck() ought to have a way of providing for the contingencies, but could not be reasonably expected to handle the fault, should it occur.
Mapping Java Exceptions
Thinking about "what could go wrong" in terms of contingencies and faults will go a long way toward establishing conventions for Java exceptions in your application architecture.
Condition | Contingency | Fault |
Is considered to be | A part of the design | A nasty surprise |
Is expected to happen | Regularly but rarely | Never |
Who cares about it | The upstream code that invokes the method | The people who need to fix the problem |
Examples | Alternative return modes | Programming bugs, hardware malfunctions, configuration mistakes, missing files, unavailable servers |
Best Mapping | A checked exception | An unchecked exception |
Contingency conditions map admirably well to Java checked exceptions. Since they are an integral part of a method's semantic contract, it makes sense to enlist the compiler's help to ensure that they are addressed. If you find that the compiler is "getting in the way" by insisting that contingency exceptions be handled or declared when it is inconvenient, it's a sure bet that your design could use some refactoring. That's actually a good thing.
Fault conditions are interesting to people but not to software logic. Those acting in the role of "software proctologist" need information about faults to diagnose and fix whatever caused them to happen. Therefore, unchecked Java exceptions are the perfect way to represent faults. They allow fault notifications to percolate untouched through all methods on the call stack to a level specifically designed to catch them, capture the diagnostic information they contain, and provide a controlled and graceful conclusion to the activity. The fault-generating method is not required to declare them, upstream methods are not required to catch them, and the method's implementation stays properly hidden—all with a minimum of code clutter.
Newer Java APIs such as the Spring Framework and the Java Data Objects library have little or no reliance on checked exceptions. The Hibernate ORM framework redefined key facilities as of release 3.0 to eliminate the use of checked exceptions. This reflects the realization that the great majority of the exception conditions that these frameworks report are unrecoverable, stemming from incorrect coding of a method call, or a failure of some underlying component such as a database server. Practically speaking, there is almost no benefit to be gained by forcing a caller to catch or declare such exceptions.
Fault handling in your architecture
The first step toward handling faults effectively in your architecture is to admit that you need to do it. Coming to this acceptance is difficult for engineers who take pride in their ability to create impeccable software. Here is some reasoning that will help. First, your application will be spending a great deal of time in development where mistakes are commonplace. Providing for programmer-generated faults will make it easier for your team to diagnose and fix them. Second, the (over)use of checked exceptions in the Java library for fault conditions will force your code to deal with them, even if your calling sequences are completely correct. If there's no fault handling framework in place, the resulting makeshift exception handling will inject entropy into your application.
A successful fault handling framework has to accomplish four goals:
- Minimize code clutter
- Capture and preserve diagnostics
- Alert the right person
- Exit the activity gracefully
Faults are a distraction from your application's real purpose. Therefore, the amount of code devoted to processing them should be minimal and, ideally, isolated from the semantic parts of the application. Fault processing must serve the needs of the people responsible for correcting them. They need to know that a fault happened and get the information that will help them figure out why. Even though a fault, by definition, is not recoverable, good fault handling will attempt to terminate the activity that encountered the fault in a graceful way.
Use unchecked exceptions for fault conditions
There are lots of reasons to make the architectural decision to represent fault conditions with unchecked exceptions. The Java runtime rewards programming mistakes by throwing RuntimeException subclasses such as ArithmeticException and ClassCastException, setting a precedent for your architecture. Unchecked exceptions minimize clutter by freeing upstream methods from the requirement to include code for conditions that are irrelevant to their purpose.
Your fault handling strategy should recognize that methods in the Java library and other APIs may use checked exceptions to represent what could only be fault conditions in the context of your application. In this case, adopt the architectural convention to catch the API exception where it happens, treat it as a fault, and throw an unchecked exception to signal the fault condition and capture diagnostic information.
The specific exception type to throw in this situation should be defined by your architecture. Don't forget that the primary purpose of a fault exception is to convey diagnostic information that will be recorded to help people figure out what went wrong. Using multiple fault exception types is probably overkill, since your architecture will treat them all identically. A good, descriptive message embedded inside a single fault exception type will do the job in most cases. It's easy to defend using Java's generic RuntimeException to represent your fault conditions. As of Java 1.4, RuntimeException, like all throwables, supports exception chaining, allowing you to capture and report a fault-inducing checked exception.
You may choose to define your own unchecked exception for the purpose of fault reporting. This would be necessary if you need to use Java 1.3 or earlier versions that do not support exception chaining. It is simple to implement a similar chaining capability to capture and translate checked exceptions that constitute faults in your application. Your application may have a need for special behavior in a fault reporting exception. That would be another reason to create a subclass of RuntimeException for your architecture.
Establish a fault barrier
Deciding which exception to throw and when to throw it are important decisions for your fault-handling framework. Just as important are the questions of when to catch a fault exception and what to do afterward. The goal here is to free the functional portions of your application from the responsibility of processing faults. Separation of concerns is generally a good thing, and a central facility responsible for dealing with faults will pay benefits down the road.
In the fault barrier pattern, any application component can throw a fault exception, but only the component acting as the "fault barrier" catches them. Adopting this pattern eliminates much of the intricate code that developers insert locally to deal with faults. The fault barrier resides logically toward the top of the call stack where it stops the upward propagation of an exception before default action is triggered. Default action means different things depending on the application type. For a stand-alone Java application, it means that the active thread is terminated. For a Web application hosted by an application server, it means that the application server sends an unfriendly (and embarrassing) response to the browser.
The first responsibility of a fault barrier component is to record the information contained in the fault exception for future action. An application log is by far the best place to do this. The exception's chained messages, stack traces, and so on, are all valuable pieces of information for diagnosticians. The worst place to send fault information is across the user interface. Involving the client of your application in your debugging process is hardly ever good for you or your client. If you are really tempted to paint the user interface with diagnostic information, it probably means that your logging strategy needs improvement.
The next responsibility of a fault barrier is to close out the operation in a controlled manner. What that means is up to your application design but usually involves generating a generic response to a client that may be waiting for one. If your application is a Web service, it means building a SOAP <fault> element into the response with a <faultcode> of soap:Server and a generic <faultstring> failure message. If your application communicates with a Web browser, the barrier would arrange to send a generic HTML response indicating that the request could not be processed.
In a Struts application, your fault barrier can take the form of a global exception handler configured to process any subclass of RuntimeException. Your fault barrier class will extendorg.apache.struts.action.ExceptionHandler, overriding methods as needed to implement the custom processing you need. This will take care of inadvertently generated fault conditions and fault conditions explicitly discovered during the processing of a Struts action. Figure 2 shows contingency and fault exceptions.
Figure 2. Contingency and fault exceptions
If you are using the Spring MVC framework, your fault barrier can easily be built by extending SimpleMappingExceptionResolver and configuring it to handle RuntimeException and its subclasses. By overriding the resolveException() method, you can add any custom handling you need before using the superclass method to route the request to a view component that sends a generic error display.
When your architecture includes a fault barrier and developers are made aware of it, the temptation to write one-off fault exception handling code decreases dramatically. The result is cleaner and more maintainable code throughout your application.
Contingency Handling in Your Architecture
With fault processing relegated to the barrier, contingency communication between primary components becomes much simpler. A contingency represents an alternative method result that is just as important as the principal return result. Therefore, checked exception type is a good vehicle to convey the existence of a contingency condition and supply the information needed to contend with it. This practice enlists the help of the Java compiler to remind developers of all aspects of the API they are using and the need to provide for the full range of method outcomes.
It is possible to convey simple contingencies by using a method's return type alone. For example, returning a null reference instead of an actual object can signify that the object could not be created for a defined reason. Java I/O methods typically return an integer value of -1 instead of a byte value or byte count to indicate an end-of-file condition. If your method's semantics are simple enough to allow it, alternative return values may be the way to go, since they eliminate the overhead that comes with exceptions. The downside is that the method caller is responsible for testing the return value to see if it is a primary result or a contingency result. The compiler will not insist that the method caller makes that test, however.
If a method has a void return type, an exception is the only way to indicate that a contingency occurred. If a method is returns an object reference, the vocabulary that the return value can express is limited to two values (null and non-null). If a method returns an integral value, it may be possible to express several contingency conditions by choosing values that are guaranteed not to conflict with the primary return values. But now we have entered the world of error code checking, something the Java exception model was developed to avoid.
Supply something useful
It made little sense to define different fault reporting exception types, since the fault barrier treats them all the same. Contingency exceptions are quite different, because they are meant to convey diverse conditions to method callers. Your architecture would probably specify that these exceptions should all extend java.lang.Exception or a designated base class that does.
Do not forget your exceptions are complete Java types that can accommodate specialized fields, methods, and even constructors that can be shaped for your unique purposes. For example, the InsufficientFundsException type thrown by the imaginary CheckingAccount processCheck() method could include an OverdraftProtection object that is able to transfer funds needed to cover the shortfall from another account whose identity depends on how the checking account is set up.
To log or not to log
Logging fault exceptions makes sense because their purpose is to draw the attention of people to situations that need to be corrected. The same cannot be said for contingency exceptions. They may represent relatively rare events, but every one of them is expected to happen during the life of your application. If anything, they signify that the application is working the way it was designed to work. Routinely adding logging code to contingency catch blocks adds clutter to your code with no actual benefit. If a contingency represents a significant event, it is probably better for a method to generate a log entry recording the event before throwing a contingency exception to alert its caller.
Exception Aspects
In Aspect Oriented Programming (AOP) terms, fault and contingency handling are crosscutting concerns. To implement the fault barrier pattern, for example, all the participating classes must follow common conventions:
- The fault barrier method must reside at the head of a graph of method calls that traverses the participating classes.
- They must all use unchecked exceptions to signify fault conditions.
- They must all use the specific unchecked exception types that the fault barrier is expecting to receive.
- They all must catch and translate checked exceptions from lower methods that are deemed to be faults in their execution context.
- They must not interfere with the propagation of fault exceptions on their way to the barrier.
These concerns cut across the boundaries of otherwise unrelated classes. The result is minor bits of scattered fault handling code and implicit coupling between the barrier class and the participants (although still a great improvement over not using a pattern at all!). AOP allows the fault handling concern to be encapsulated in a common Aspect applied to the participating classes. Java AOP frameworks such as AspectJ and Spring AOP recognize exception handling as a join point to which fault handling behavior (or advice) can be attached. In this way, the conventions that bind participants in the fault barrier pattern can be relaxed. Fault processing can now reside within an independent, out-of-line aspect, eliminating the need for a "barrier" method to be placed at the head of a method invocation sequence.
If you are exploiting AOP in your architecture, fault and contingency handling are ideal candidates for aspects that apply throughout an application. A full exploration of how fault and contingency handling could work in the AOP world would make an interesting topic for a future article.
Conclusion
Although the Java exception model has generated spirited discussion during its lifetime, it provides excellent value when it is applied correctly. As an architect, it is up to you to establish conventions that get the most from the model. Thinking of exceptions in terms of faults and contingencies can help you make the right choices. Using the Java exception model properly will keep your application simple, maintainable, and correct. Aspect Oriented Programming techniques may offer some definite advantages for your architecture by recognizing fault and contingency handling as crosscutting concerns.
Tuesday, January 09, 2007
Deploying eclipse based application with Java Web Start
Applications built on eclipse 3.1 can now be deployed using Java Web Start.
Java Web Start "is an application-deployment technology that gives you the power to launch full-featured applications with a single click from your web browser".
The prerequisites to start eclipse from Java Web Start are:
- The deployed application must be eclipse 3.1 based;
- All deployed plug-ins must be jar'ed;
- All plug-ins must be signed since the application need full permission from the client.
The following steps describe how to setup a Java Web Start site serving up a feature based RCP application.
Step 1, creating a wrappering feature
- Create a feature including all the features that are listed in your product definition;
- Copy in a folder of your feature the startup.jar;
- Add the following line to the build.properties of the feature.
root=<folderContainingStartup.jar>/
Step 2, exporting the wrappering feature and the startup.jar
Note. Before proceeding with this step make sure to have a keystore available. Eclipse does not provide any facility to create keystores. You need to use keytool. In addition, ensure that the eclipse you are developing with is running on a Java SDK instead of a JRE. If this constraint is not satisfied, the jar signing will fail.
- Select the wrappering feature and do File > Export > Feature. In the wizard, select the wrappering feature, choose the "directory" option to export your jnlp application to, and check the option "package features and plug-ins as individual JAR archives". On the next page of the wizard, fill in the information relative to your keystore in the "Signing JAR Archives" section. Then in the "JNLP section", enter the name of the server that will serve up your application and the level of JRE required to start your application. That last value will be used to in the generated JNLP files to fill in the value of <j2se version="1.4+" /> . Click finish.
- Once the export is done you should have the following structure on disk
site/ (The root of your jnlp site) startup.jar features/ WrapperingFeature_1.0.0.jar WrapperingFeature_1.0.0.jnlp com.xyz.abc_1.0.0.jar com.xyz.abc_1.0.0.jnlp ... plugins/ org.eclipse.core.runtime_3.1.0.jar com.foo.baz_1.0.0.jnlp ...
- Using the same keystore than the one used to export the feature, sign startup.jar using jarsigner.
Tips: if you don't change keystore, replace the startup.jar from the feature with this signed version so you don't have to do this manual step on every export.
Step 3, creating the main jnlp file
A Java Web Start application is described by JNLP files. They replace the eclipse.exe and the config.ini files by some equivalent mechanism. For example, JNLP has its own mechanism to control splash screen, ways to pass parameters and define what constitutes the application.
When you did the export, all the simple JNLP files have been created, so you are left with writing the main file that will control the application. Because the majority of the main file is common to all applications, it is recommended to start from the following self documented template.
On the site serving up your application, the file must be located in the same folder than startup.jar. Once you will be done editing this file, your application will be ready.
<?xml version="1.0" encoding="UTF-8"?> <jnlp spec="1.0+" codebase="http://myCompany.org/jnlpServer" href="mail.jnlp"> <!-- URL to the site containing the jnlp application. It should match the value used on export. Href, the name of this file --> <information> <!-- user readable name of the application --> <title> Mail Application </title> <!-- vendor name --> <vendor>My company</vendor> <!-- vendor homepage --> <homepage href="My company website" /> <!-- product description --> <description>This is a mail client</description> <icon kind="splash" href="splash.gif"/> </information> <!--request all permissions from the application. This does not change--> <security> <all-permissions/> </security> <!-- The name of the main class to execute. This does not change--> <application-desc main-class="org.eclipse.core.launcher.WebStartMain"> <argument>-nosplash</argument> </application-desc> <resources> <!-- Reference to the startup.jar. This does not change --> <jar href="startup.jar"/> <!-- Reference to all the plugins and features consituting the application --> <!-- Here we are refering to the wrappering feature since it transitively refers to all the other plug-ins necessary --> <extension name="Wrappering feature" href="features/Wrappering_1.0.0.jnlp"/> <!-- Information usually specified in the config.ini --> <property name="osgi.instance.area" value="@user.home/Application Data/mail"/> <property name="osgi.configuration.area" value="@user.home/Application Data/mail"/> <!-- The id of the product to run, like found in the overview page of the product editor --> <property name="eclipse.product" value="mail.product"/> </resources> <!-- Indicate on a platform basis which JRE to use --> <resources os="Mac"> <j2se version="1.5+" java-vm-args="-XstartOnFirstThread"/> </resources> <resources os="Windows"> <j2se version="1.4+"/> </resources> <resources os="Linux"> <j2se version="1.4+"/> </resources> </jnlp>
Tips: once you have created this file, you can store it in the wrappering feature in the same folder than the startup.jar, such that on every export you will get the complete structure.
Plug-ins based application
Even though your RCP application does not use features, Java Web Start-ing it is possible.
To do so, it is recommended to create a wrappering feature in order to facilitate the creation of the main jnlp file and ease the deployement. This wrappering feature will list all the plug-ins of your application. Once the feature has been updated copy the generated JNLP file and modify it to become your main JNLP file.
Known limitations
- Eclipse Update and Java Web Start
Those two deployment technologies can work together but under the following restrictions: plug-ins installed by Java Web Start can not be updated by Update and vice-versa. Features and plug-ins installed by Java Web Start can't be refered in the prerequisites of features that needs to be installed by Update; - Help can not be deployed through Java Web Start. However it could be installed using eclipse Update, or the server serving your application could run a help server;
- Request to exit the application with a restart code are ignored;
- On the mac, applications can only be webstarted by clients using java 1.5.
Java Web Start
Java Web Start es la implementación de referencia de la especificación JNLP ( JSR 56, Java Networking Launching Protocol )[1] que define como ejecutar aplicaciones Java remotamente desde un entorno de red cualquiera.
Java Web Start revoluciona el concepto tradicional que tenemos de las aplicaciones. Normalmente cuando se quiere ejecutar una aplicación que no se encuentra instalada en un equipo, se descarga del servidor, se instala en dicho equipo y por último se ejecuta. Java Web Start intenta simplificar al máximo todo este proceso de modo que el usuario lo único que tiene que hacer para lanzar una aplicación sea simplemente pinchar en un enlace de su navegador, a partir de ese momento, todo el proceso relacionado con la descarga, instalación y ejecución del programa se realiza de una manera transparente.
A pesar de su parecido, una aplicación de Java Web Start no tiene nada que ver con un Applet. Java Web Start sólo utiliza el navegador como medio para que el usuario pueda ejecutar las aplicaciones. Una vez que el usuario pincha en un enlace de una aplicación, ésta se ejecuta en la máquina virtual del cliente como cualquier otra aplicación.
Java Web Start no forma parte del navegador web, es una aplicación independiente y por lo tanto no requiere del navegador para su funcionamiento. Una vez que el usuario pincha en un enlace para ejecutar una aplicación, puede continuar navegando o cerrar el navegador sin que esto interfiera en el funcionamiento de la aplicación que ha sido lanzada. Además, Java Web Start va guardando en una caché interna las aplicaciones que va ejecutando el usuario, de modo que éste pueda lanzarlas posteriormente sin la necesidad de abrir el navegador o incluso ejecutarlas localmente sin conectarse a ninguna red.
Las aplicaciones Java Web Start siguen el modelo de seguridad de la plataforma Java 2 por lo que la integridad de los datos que obtenemos a través de la red está garantizada. Como veremos, comúnmente las aplicaciones que se ejecuten han de estar debidamente firmadas y se requiere siempre que el usuario autorice su ejecución.
Java Web Start viene incluido de serie dentro en el JRE a partir de su versión 1.4. La última versión es la 1.2 (beta) que viene con el JRE 1.4.1 también beta. Como curiosidad reseñar que el sistema operativo OS X de Macintosh ya trae preinstalado soporte para aplicaciones Java Web Start. Aunque técnicamente es necesario que se encuentre instalado al menos un JRE dentro de la máquina cliente para poder ejecutar aplicaciones Java Web Start, lo cierto es que éstas se pueden configurar de manera que el JRE utilizado se descargue automáticamente si no se encuentra disponible con lo que se consigue una transparencia absoluta para el cliente.
Java Web Start no es la única implementación de la especificación JNLP. Una alternativa muy popular es OpenJNLP [10], una implementación Open Source de la especificación que está desarrollada completamente en Java y que utilizaremos en el último apartado de este artículo.
Ventajas y desventajas de Java Web Start
Como ya he dicho anteriormente, Java Web Start revoluciona por completo el concepto tradicional de aplicaciones. Las ventajas que ofrece tanto a los desarrolladores de las mismas como a los usuarios son muchas y muy importantes:
- Transparencia : El usuario no necesita pasar por un proceso traumático de descarga e instalación de la aplicación para poder ejecutarla. Únicamente tiene que pinchar un enlace en su navegador y la aplicación se descarga, se instala y se ejecuta de manera automática. Además, Java Web Start se encarga de crear los accesos directos correspondientes en el escritorio y menú de inicio del usuario.
- Mantenibilidad : Para los desarrolladores y administradores de sistema, Java Web Start es una bendición. Ahora ya no es necesario copiar la misma aplicación a todos los usuarios de una red cada vez que se realiza una pequeña modificación en la misma, sino que con actualizarla en el servidor web es suficiente para que los usuarios puedan utilizar la última versión de la misma.
- Control de versiones : Java Web Start se encarga automáticamente de realizar el control de versiones de las aplicaciones. Antes de ejecutar una aplicación, Java Web Start comprueba en el servidor web que no exista una versión más avanzada de la misma, en cuyo caso actualizará la vieja versión por la nueva automáticamente. Esto beneficia tanto a los usuarios que siempre ejecutan la última versión de su software, como a los desarrolladores que no tienen necesidad de distribuir las nuevas versiones a los usuarios o crear algún sistema interno de control de versiones.
- Independencia del servidor web y del navegador : Java Web Start puede funcionar en cualquier servidor web tan sólo añadiendo el tipo MIME correspondiente a los ficheros con extensión .jnlp, por otra parte, también funcionará en cualquier navegador aunque en algunos habrá que configurar el programa asociado a los ficheros con dicha extensión.
- Independencia del sistema operativo : Aunque Java Web Start no está disponible para todos los sistemas operativos para los que la plataforma Java se encuentra disponible, OpenJNLP que como dijimos está escrito en Java y que es una inciativa Open Source si que es totalmente independiente del sistema operativo.
- Automatiza la gestión de JREs : Cada aplicación puede decidir que JRE quiere utilizar para ejecutarse, es más, si ese JRE no existiese en el equipo del cliente, Java Web Start se encarga automáticamente de su descarga e instalación en el sistema.
- Transparencia al desarrollador : No es necesario modificar las aplicaciones existentes para que aprovechen esta tecnología. Para hacer una vieja aplicación compatible con Java Web Start, tan sólo hay que crear un pequeño descriptor XML con las características de la aplicación y colocarla en un servidor web. Las aplicaciones pueden seguir ejecutándose del modo tradicional sin ningún problema.
- Ejecución local de las aplicaciones : Java Web Start a diferencia de tecnologías como JSP/Servlets no necesita la red para ejecutar las aplicaciones. La red tan sólo es un medio para obtener dichas aplicaciones y sus actualizaciones. Una vez descargada una aplicación, ésta se ejecuta de manera local y tan sólo accede a la red si lo necesita para su funcionamiento.
Como toda tecnología, Java Web Start no está exenta de problemas:
- Una máquina virtual por aplicación : Este es quizás el problema más importante, aunque posteriormente veremos una posible solución. La especificación JNLP establece que cada aplicación se ha de ejecutar en una máquina virtual diferente. Obviamente esto es un gran obstáculo para entornos con recursos limitados y donde sea necesario ejecutar múltiples aplicaciones diferentes simultáneamente obligando a un consumo de recursos y de memoria innecesario.
- Problemas de flexibilidad : Java Web Start tiene varias limitaciones de flexibilidad : no se pueden pasar algunos parámetros a la máquina virtual ya que se comprometería la seguridad y la portabilidad ( ejemplo: los parámetros que comienzan con -X ), algunas opciones sólo se pueden configurar desde el ordenador del usuario ( como el tipo de máquina virtual a utilizar, registrar la salida, etc. )
- No soporta los JRE 1.1 e inferiores: Java Web Start basa su funcionamiento en el modelo de seguridad de la plataforma Java 2 por lo que no existe soporte para versiones anteriores.
Utilizando Java Web Start
En este apartado vamos a ver con una aplicación sencilla el uso de Java Web Start. El código fuente de la aplicación es el siguiente y todos los ficheros necesarios para ejecutar este ejemplo se encuentran en [26].
import javax.swing.*; import java.awt.event.*; import java.util.*; public class Main { private static int count; private static List buttons = new ArrayList(); private JButton button = new JButton(); public Main() { JFrame frame = new JFrame(); JButton button = new JButton(); buttons.add(button); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { count++; Iterator it = buttons.iterator(); while (it.hasNext()) { ((JButton)it.next()).setText("clicks = " + count); } } }); button.setText("clicks = " + count); frame.getContentPane().add(button); frame.setSize(300,300); frame.setLocation(400,300); frame.setVisible(true); // Importante. Si se hace un exit se cerrar? el loader frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); } public static void main(String[] args) { System.out.println("[Main] main class executed"); new Main(); } }
Como se puede apreciar, se trata de un ejemplo muy sencillo que muestra una ventana con un botón que al pulsarlo incrementa un contador. El contador es estático para poder comprobar fácilmente si nuestro programa se ejecuta en máquinas virtuales diferentes cuando lo lanzamos varias veces y que en el siguiente apartado utilizaremos para ver como las aplicaciones se ejecutan en la misma máquina virtual.
Una vez que hayamos creado nuestra aplicación y comprobado que funciona correctamente en modo local crearemos el fichero jar que contendrá la aplicación. Para ello simplemente ejecutamos la siguiente línea:
jar -cvf main.jar *.class
El siguiente paso es la creación del descriptor JNLP. Este descriptor es un fichero XML que contiene información sobre nuestra aplicación y sobre como ha de lanzarla Java Web Start. El descriptor de nuestra aplicación es muy sencillo y no presenta ningún problema incluso a los lectores no familiarizados con esta tecnología, sin embargo el número de parámetros y opciones que soporta dicho descriptor es bastante grande, por lo que no dude en consultar las referencias al final de este artículo para obtener una información más detallada sobre el mismo.
Lo vemos a continuación :
<?xml version="1.0" encoding="utf-8"?> <jnlp spec="1.0+" codebase="http://localhost:8080/jnlp/" href="jnlp.jnlp"> <information> <title>Ejemplo de JNLP</title> <vendor>JavaHispano</vendor> <homepage href="http://www.javahispano.com"/> <description>Ejemplo de JNLP</description> <description kind="short"> Esta aplicación es un pequeño ejemplo de la tecnología JNLP </description> <icon href="images/javahispano.jpg"/> <offline-allowed/> </information> <security> <all-permissions/> </security> <resources> <j2se version="1.4+"/> <jar href="lib/main.jar"/> </resources> <application-desc main-class="Main"> </application-desc> </jnlp>
Como se puede observar la mayoría de los campos son autodescriptivos. Quizás los más interesantes sean la etiqueta <jnlp> cuyos atributos especifican donde se encuentra el fichero JNLP, la etiqueta <jar> que permite especificar los diferentes archivos que componen nuestra aplicación, la etiqueta <j2se> cuyo atributo version especifica cuál es la máquina virtual que se usará para ejecutar la aplicación y la etiqueta <application-desc> cuyo atributo main-class especifica cual es la clase principal de la aplicación.
Java Web Start puede ejecutar las aplicaciones en dos modos diferentes. El primero, es el modo restringido, en el que las aplicaciones se ejecutan en un sandbox, modo en el que sólo pueden hacer uso de determinados recursos del sistema. El API de Java Web Start permite la utilización de diversos servicios programáticamente para poder saltarse algunas limitaciones de este modelo y permitir de este modo el acceso a ficheros, al portapapeles, a la descarga de archivos, etc.
El segundo modo, es el modo de confianza y es el que utilizaremos en este y el resto de ejemplos. En este modo las aplicaciones consiguen el acceso a todos los recursos del sistema. Para que una aplicación pueda conseguir dicho acceso, previamente ha de tener todos los ficheros jar de los que conste firmados digitalmente, de este modo, cuando el usuario quiera ejecutar la aplicación le aparecerá un certificado donde se solicita acceso no restringido al sistema, el usuario es el que decide si debe confiar o no en la fuente que emite el certificado.
El proceso de firma de los ficheros jar es muy sencillo. Lo primero que hay que hacer es crear una clave de autenticación con la herramienta keytool, por ejemplo:
keytool -genkey -keystore myKeyStore -alias myself
Esta herramienta nos pedirá información acerca de la clave y del emisor del certificado que le aparecerá al usuario al ejecutar la aplicación. La clave se almacenará en el almacén de claves que especifiquemos. Una vez creada la clave, tan sólo nos queda firmar todos nuestros ficheros jar, en este caso:
jarsigner -keystore myKeyStore main.jar myself
Por último, para pedir acceso no restringido al sistema habría que añadir las siguientes líneas al descriptor jnlp:
<security> <all-permissions/> </security>
Bien, la aplicación ya está preparada por completo, tan sólo falta configurar el servidor web. En este ejemplo y en el resto, utilizaré Apache Tomcat [19], aunque también se puede utilizar cualquier otro servidor. En el servidor que se utilice habrá que añadir soporte para el tipo MIME para los ficheros JNLP, en este caso, Tomcat ya lo trae incluido por lo que no hay que configurar absolutamente nada.
Para este y el resto de ejemplos crearé la siguiente estructura dentro del servidor web:
raiz del servidor |____ jnlp |____ jnlp.jnlp |____ lib |____ main.jar
Esta estructura se puede crear de muchas formas, ya sea creando un contexto partícular, un simple directorio a partir del raiz, etc.
Una vez arrancado el servidor web, se debería acceder a la aplicación escribiendo el enlace ( suponiendo que usamos Apache Tomcat ) :
htp://localhost:8080/jnlp/jnlp.jnlp
Según el navegador que se utilice para ejecutar el ejemplo quizás sea necesario configurarlo para que asocie el tipo MIME JNLP con la aplicación Java Web Start u OpenJNLP en caso de utilizar este último. Si por cualquier razón no se es capaz de configurar el navegador para ejecutar aplicaciones JNLP, siempre se pueden lanzar desde la línea de comandos, por ejemplo con Java Web Start tendríamos que escribir los siguiente:
directorio_de_Java_Web_Start/javaws http://localhost:8080/jnlp/jnlp.jnlp
Una vez hecho esto, si es la primera vez que se ejecuta la aplicación, aparecerá una alerta de seguridad en la que se le pregunta al usuario si quiere confiar en dicha aplicación y en la fuente que emite el certificado. En el mensaje hay una alerta de que no se puede verificar la autenticidad del certificado, esto es totalmente normal ya que no se ha comprado dicho certificado a ninguna autoridad de certificación. Para lanzar la aplicación definitivamente hay que pulsar el botón Iniciar.
Uno de los problemas de Java Web Start que ya mencioné anteriormente es que cada aplicación se ejecuta en una máquina virtual diferente. Una manera de comprobarlo es ejecutar dos veces la aplicación de ejemplo de este apartado y ver como al pulsar el botón de una de ellas el otro botón no se ve modificado, esto se debe a que las aplicaciones se están ejecutando cada una en su máquina virtual. En el siguiente apartado se verá una manera simple de sobrepasar este inconveniente y poder de este modo ejecutar gran cantidad de aplicaciones en la misma máquina virtual.
Ejecutando múltiples aplicaciones en la misma máquina virtual
Como se ha visto, una de las ventajas de Java Web Start es que permite lanzar aplicaciones desde la web de una manera transparente. A poco que pensemos, una de las consecuencias de esto es la posibilidad de crear portales empresariales que engloben aplicaciones de muy diferentes tipos.
A menudo, en las empresas encontramos aplicaciones de muy diversa índole. Es muy sencillo que en una misma empresa existan aplicaciones nativas ( ya sean del sistema operativo o viejas aplicaciones creadas por la empresa ), aplicaciones Java y aplicaciones basadas en tecnología web (JSP, Servlets, ASP, PHP, etc.). La diversidad de todas estas tecnologías hace que sea muy díficil la creación de un portal personalizado donde cada usuario pueda ejecutar estas aplicaciones y conseguir una alta mantenibilidad del sistema.
Java Web Start nos ofrece una buena posibilidad para realizar un portal de este estilo. Las aplicaciones web no plantean ningún problema, las aplicaciones Java tampoco son un problema ya que esta tecnología nos permite lanzarlas directamente desde el navegador mientras que las aplicaciones nativas pueden ejecutarse utilizando un pequeño lanzador de aplicaciones realizado en Java y que también podrá ejecutarse sin problemas desde el navegador. Las ventajas de un portal de este estilo son inmensas: mantenibilidad, centralización de la información, control sencillo de los permisos de acceso, personalización del contenido, etc.
Sin embargo todavía nos queda un problema: cada aplicación se ejecutará en una máquina virtual diferente, algo que es inadmisible cuando el número de aplicaciones a ejecutar es relativamente grande y el número de recursos es limitado.
En este apartado se muestra una posible solución a este problema basada en el uso de un lanzador de aplicaciones. Este lanzador actuará como un demonio de sistema que se quedará a la espera de aplicaciones Java para ejecutar. Cuando una aplicación Java quiera ejecutarse, el cargador iniciará un nuevo hilo en su máquina virtual y la lanzará.
Para conseguir hacer esto es necesario centralizar el acceso a las aplicaciones, es decir, antes, teníamos que cada descriptor JNLP se utilizaba para ejecutar una aplicación diferente, ahora cada descriptor JNLP se utilizará para ejecutar siempre nuestro lanzador de aplicaciones y a éste se le pasará como parámetro la aplicación que se quiera lanzar. Siguiendo con el ejemplo del apartado anterior el descriptor JNLP quedaría del siguiente modo:
<application-desc main-class="Loader"> <argument>Main</argument> </application-desc>
En este caso el lanzador se corresponde con la clase Loader. El primer argumento que recibe dicho lanzador es la clase principal de la aplicación que se quiere ejecutar, en este caso Main. El resto de parámetros de la aplicación se pasarían también como argumentos, eso sí, el primero siempre ha de ser la clase principal. El siguiente esquema muestra gráficamente el funcionamiento de este lanzador de aplicaciones:
Básicamente:
- Si es la primera vez que se ejecuta nuestro cargador de aplicaciones, éste se queda residente en equipo del usuario esperando por aplicaciones para ser lanzadas. En este caso depués de registrar el cargador se lanzaría la aplicación que se iba a ejecutar.
- Si el lanzador de aplicaciones ya se encuentra residente, entonces se le avisa de que se quiere ejecutar una nueva aplicación, posteriormente el lanzador ejecutará dicha aplicación en un nuevo hilo de su máquina virtual.
Para implementar este lanzador de aplicaciones residente existen muchas alternativas. En este caso se ha utilizado RMI[20,21,22,23,24,25] principalmente por su sencillez; otras alternativas podrían haber sido utilizar sockets o utilizar directorios compartidos.
De aquí en adelante se mostrará el código fuente del cargador de aplicaciones que se puede encontrar en [26]. La explicación se va realizando por partes para que sea más sencilla su comprensión.
Como objeto remoto que es el lanzador de aplicaciones ha de implementar una interfaz remota:
import java.rmi.*; public interface Loader extends Remote { public void launchApplication(String[] args) throws Exception; public void shutdown() throws RemoteException; }
Como se puede ver, el cargador es muy simple, tiene métodos para lanzar aplicaciones y para retirarse del sistema. Ahora voy a describir más a fondo la implementación del cargador. Primero empezaré con el método main:
public static void main(String[] args) { if (System.getProperty("shutdown-registry") != null) { __________________ *1 shutdownRegistry(); System.exit(0); } else if (System.getProperty("own-vm") != null) { __________________ *2 executeAppInOwnVM(args); } else { try { createRegistry(); __________________ *3 try { executeApp(args); } catch (Exception e) { e.printStackTrace(); } } catch (ExportException ee) { __________________ *4 System.out.println("[Loader] registry already created"); executeApp(args); System.exit(0); } catch (Exception e) { e.printStackTrace(); } } }
- · Lo primero que se hace en *1 es comprobar si lo que se quiere es cerrar el cargador, esto podría corresponderse con la típica opción de salir del sistema en un portal empresarial.
- · En *2 se comprueba si la aplicación ha de ejecutarse en su propia máquina virtual ya que puede que no queramos que alguna aplicación en concreto comparta la máquina virtual en la que se ejecutará con el resto de aplicaciones.
- · Si no se cumple ninguna de las dos condiciones anteriores, en *3 el cargador intenta hacerse residente en el sistema y una vez lo haya conseguido ejecuta la aplicación.
- · En caso de que ya se encuentre residente (*4) se ejecuta la aplicación.
En los siguientes puntos se muestra el código de los métodos más importantes:
public static void createRegistry() throws RemoteException, ExportException,InterruptedException { System.out.println("[Loader] craeting registry"); registry = LocateRegistry.createRegistry(PORT); System.out.println("[Loader] registry created"); }
El método createRegistry() que se ve arriba simplemente intenta crear un registro RMI en el puerto especificado del equipo del cliente. En ese registro es donde guardaremos el lanzador de aplicaciones.
El método executeApp() que aparece por debajo de estas líneas es el que se encarga de ejecutar la aplicación y añadir el cargador al registro RMI si es necesario:
private static void executeApp(String[] args) { try { registry = lookupRegistry(); __________________ *1 } catch (Exception e) { e.printStackTrace(); return; } Loader loader = null; try { loader = lookupLoader(); __________________ *2 loader.launchApplication(args); } catch (NotBoundException nbe) { System.out.println("[Loader] loader not bound"); try { bindLoader(); __________________ *3 loader = lookupLoader(); loader.launchApplication(args); } catch (Exception e) { e.printStackTrace(); return; } } catch (Exception e) { e.printStackTrace(); return; } }
- · Lo primero que se hace es intentar localizar el registro ( *1 ) donde se debería encontrar el lanzador de aplicaciones, si el registro no se encuentra se finaliza la ejecución del programa.
- · El siguiente paso es buscar el lanzador de aplicaciones dentro del registro ( *2 ), si lo encontramos se intentará lanzar la aplicación.
- · En caso de que no se cumpla la condición del punto anterior, se añade el lanzador de aplicaciones al registro ( *3 ), se busca para asegurarse de que se ha cargado correctamente y finalmente se intenta lanzar la aplicación. En caso de que en alguno de estos dos últimos puntos produzca una excepción el programa finalizará.
A continuación se pueden ver estos métodos más en detalle:
public static Registry lookupRegistry() throws RemoteException { System.out.println("[Loader] looking for registry"); Registry registry = LocateRegistry.getRegistry(PORT); System.out.println("[Loader] registry found successfully"); return registry; } public static Loader lookupLoader() throws RemoteException, NotBoundException, MalformedURLException { System.out.println("[Loader] looking for loader"); Loader loader = (Loader)Naming.lookup("//localhost:"+PORT+"/loader"); System.out.println("[Loader] loader found successfully"); return loader; } public static void bindLoader() throws RemoteException, AlreadyBoundException, MalformedURLException { System.out.println("[Loader] binding loader"); Naming.rebind("//localhost:"+PORT+"/loader",new LoaderImpl()); System.out.println("[Loader] loader bound on registry"); }
Como se puede apreciar, el código es muy simple y hacen uso de los mecanismos básicos de RMI[] para registrar y buscar los diferentes objetos.
El método launch(String[] args) es el verdadero encargado de lanzar la aplicación que se quiere ejecutar. Este método lo llama el cargador desde su máquina virtual, la misma que ejecuta todas las aplicaciones. Para ejecutar la aplicación, simplemente se obtiene el método main de la clase que queremos ejecutar utilizando el API Reflection y se llama a dicho método pasándole los argumentos necesarios; cualquier otra alternativa (llamada a un método concreto, a un constructor, a un inicializador estático, etc.), también habría sido posible y se realizaría de modo muy similar.
public static void launch(String[] args) throws Exception { if (args[0].length == 0) { throw new LoaderException("Not enough arguments"); } System.out.println("[Loader] loading arguments"); String classname = args[0]; String[] newArgs = new String[args.length-1]; if (newArgs.length != 0) { System.arraycopy(args,1,newArgs,0,newArgs.length); } System.out.println("[Loader] args array created"); Class appClass = Class.forName(classname); System.out.println("[Loader] class dinamically loaded"); Method mainMethod = appClass.getMethod("main", new Class[]{String[].class}); System.out.println("[Loader] main method loaded"); mainMethod.invoke(null, new Object[]{newArgs}); System.out.println("[Loader] main method invoked"); }
El conjunto de archivos que componen el ejemplo de este apartado se encuentra en [26]. Lo primero que hay que hacer es configurar el servidor web como se vio en el apartado anterior para que el fichero ejemplo2.jnlp y las librerías que contienen el lanzador de aplicaciones (loader.jar) y la aplicación que ejecutaremos (main.jar) sean accesibles. Por seguir con la estructura que se vio en el apartado anterior, el fichero ejemplo2.jnlp irá bajo el directorio jnlp mientras que los ficheros main.jar y loader.jar se colocarán bajo el directorio jnlp/lib/. Una vez configurado todo correctamente lo único que hay que hacer es arrancar nuestro navegador web y acceder al fichero ejemplo2.jnlp, momento en el que el lanzador de aplicaciones se hará residente para posteriormente lanzar la aplicación.
En la consola de Java Web Start se puede ver como se va realizando todo el proceso de creación y configuración del registro para acabar lanzando la aplicación. La aplicación lanzada es la misma que vimos en el apartado anterior, es decir, una ventana con un botón que muestra el valor de una variable estática que actúa como contador. Si se vuelve a lanzar el fichero ejemplo2.jnlp desde el navegador se verá como ahora ya no se crea el registro y se lanza directamente la aplicación.
Falta reseñar algunos puntos:
- El tiempo de carga de la segunda aplicación y posteriores es mucho menor que el de la primera ya que no es necesario crear una nueva máquina virtual para ejecutarla, por lo tanto tenemos una ganancia importante en tiempo de lanzamiento.
- Al ejecutarse todas las aplicaciones en una misma máquina virtual hay que tener especial cuidado con las variables estáticas. En el ejemplo se ve claramente este efecto ya que al abrir varias aplicaciones se observa como al pulsar en uno de los botones el resto de contadores del resto de aplicaciones también se actualizan.
Aprovechando el mecanismo de carga dinámica de aplicaciones que ofrece JNLP
Hace tiempo, me toco trabajar en un proyecto interesante, se trataba la creación de múltiples aplicaciones para gestionar una empresa y una de ellas era un escritorio desde el que se pudiesen lanzar todas estas aplicaciones. El principal problema al que nos enfrentábamos era el intentar que los usuarios pudiesen acceder a todas las aplicaciones y que éstas se ejecutasen en la misma máquina virtual para de este modo aprovechar más los recursos disponibles en los clientes y e incluso tener la posibilidad de compartir estructuras de datos entre las aplicaciones.
Hace unos días, uno de mis amigos de esa empresa en la que estuve me comentó que ahora se encontraban con un pequeño problema. Con el paso del tiempo, la cantidad de programas que se han ido añadiendo a ese escritorio ha sido muy grande, en el que el tamaño de la totalidad de aplicaciones hace complicada su actualización y mantenimiento. Habían pensado en Java Web Start por la flexibilidad que ofrece pero no veían la forma de utilizarlo ya que su escritorio es una aplicación Swing y no está pensado para ser ejecutado desde un navegador web, además no quieren tirar por la borda todo el trabajo que hicimos y quieren mantener el mismo lanzador de aplicaciones por lo que hacer un equivalente en navegador web no es una opción viable.
El objetivo que se persigue es que las aplicaciones se actualicen ellas mismas cada vez que sean invocadas, consiguiendo de este modo una transparencia absoluta al usuario y un ahorro considerable de mantenimiento para el equipo de desarrollo, y todo esto manteniendo el lanzador que existía previamente.
Por suerte, existe una solución muy sencilla que es aprovechar todos estos mecanismos de carga dinámica de aplicaciones que ofrece la especificación JNLP, esta solución pasa por utilizar OpenJNLP. OpenJNLP es un desarrollo Open Source, que está formado por un cargador de aplicaciones que viene a ser el equivalente a Java Web Start y por un conjunto de librerías que implementan la especificación de JNLP.
Hasta ahora, en el escritorio desde el que se lanzan las aplicaciones cada vez que se pulsaba en el icono de una de dichas aplicaciones, ésta se lanzaba en un hilo diferente. Con el nuevo planteamiento, en lugar de ejecutar la aplicación directamente, lo que se hará será realizar una llamada a una función de una de las librerías de OpenJNLP que se encargará de comprobar y actualizar la aplicación con nuevas versiones en el caso de existiesen y ejecutar la aplicación.
En [10] está el enlace desde donde se puede descargar OpenJNLP, una vez descargado es necesario añadir al CLASSPATH las librerías openjnlp-lib.jar y openjnlp-extra.jar. Es muy importante también bajar el Java Web Start Developer?s Pack [3], que contiene la librería jnlp.jar que también es necesario añadir al CLASSPATH.
El ejemplo se compone de una ventana que contiene un botón, cada vez que se pulsa el botón se carga la aplicación que hemos utilizado hasta ahora en todos los ejemplos, es decir, la que se encuentra en el fichero main.jar. El objetivo es ver como si se actualiza esta aplicación en el servidor el usuario siempre carga la última versión de manera transparente.
En el siguiente trozo de código se encuentra la parte en la que se lanza la aplicación al pulsar el botón, he suprimido toda la parte del interfaz gráfico ya que no tiene demasiado interés.
import org.nanode.jnlp.*; __________________ *1 import org.nanode.launcher.cache.FileCache; import org.nanode.launcher.cache.Cache; private void launch() { try { final Cache cache = FileCache.defaultCache(); __________________ *2 final URL url = new URL("http://localhost:8080/jnlp/jnlp.jnlp"); new Thread() { public void run() { try { JNLPParser.launchJNLP(cache,url,true); __________________ *3 } catch (ParseException pe) { pe.printStackTrace(); } } }.start(); } catch (MalformedURLException murle) { murle.printStackTrace(); } }
El proceso es muy sencillo:
- Lo primero que hay que hacer es importar las clases necesarias de la librería OpenJnlp (*1).
- Una vez hecho eso hay que establecer la caché de aplicaciones (*2), que es donde OpenJNLP irá almacenando las aplicaciones que un usuario va ejecutando para poder ejecutarlas cuando no exista una versión más actualizada en el servidor. La implementación por defecto de la caché de aplicaciones se basa en ficheros. El directorio donde se guardan dichas aplicaciones es el .jnlp/cache/vendor/title a partir del directorio que tenga como valor el atributo user.home, donde vendor y title se corresponden con los campos del descriptor jnlp.
- Para finalizar el proceso se lanza la aplicación (*3). El método launch se encarga automáticamente de comprobar si existen actualizaciones de la aplicación que especificamos en la url que se le pasa como parámetro y ejecuta dicha aplicación en un nuevo hilo.
El conjunto de archivos que componen este ejemplo se encuentra en [26]. Lo primero que hay que hacer es configurar nuestro servidor web como se vio en los anteriores apartados para que el fichero ejemplo3.jnlp y las librerías que contienen las diferentes versiones de la aplicación que se va a ejecutar (main.jar y main2.jar) sean accesibles.
Los dos ficheros jar contienen la misma aplicación que se ha utilizado hasta ahora como ejemplo salvo que la segunda versión dibuja el botón de color rojo. El proceso de prueba es el siguiente:
- Primero se ejecuta la clase Desktop.class con el comando java -cp path_a_librerías_jnlp Desktop. Esta clase se encargará de lanzar el descriptor JNLP ejemplo3.jnlp, que utiliza el fichero main.jar por lo que deberá aparecer la aplicación con el botón normal.
- A continuación, para poder observar todo lo que se ha comentado es necesario sobreescribir en el servidor el fichero main.jar con la nueva aplicación, main2.jar.
- Por último se vuelve a ejecutar la clase Desktop.class como se explicó en el primer punto. En este caso, como la aplicación ha sido modificada, se bajará la nueva versión y el botón aparecerá de color rojo.
En este caso, y a diferencia del apartado anterior, a pesar de ejecutarse todas las aplicaciones en la misma máquina virtual, cuando se pulsa en uno de los botones no se actualiza el contador en el resto de ventanas. ¿Por qué sucede esto si el contador es estático? Esto se debe a que OpenJNLP utiliza un cargador de clases (ClassLoader) diferente para cargar cada aplicación y en el lenguaje Java dos instancias de una misma clase que hayan sido cargadas por distintos cargadores de clase se comportan exactamente igual que si fueran clases diferentes.
El apartado anterior también podría haberse adaptado para que se produjese este efecto pero se ha dejado así por simplicidad y para mostrar que a veces es necesario tener cuidado con este tipo de variables estáticas.
Retomando el tema del escritorio empresarial, está claro que la solución a los problemas de mis amigos, y por extensión, de toda la gente que quiera aprovechar las ventajas de la carga dinámica de aplicaciones que ofrece JNLP es muy sencilla. En lugar de lanzar las aplicaciones de la manera tradicional ( creando una instancia de la aplicación en un nuevo hilo y ejecutándola ), se puede utilizar OpenJNLP para lanzar estas aplicaciones, de modo que el proceso de actualización y ejecución de las mismas se automatiza completamente. Además esta solución permite aprovechar todo el código que haya sido realizado y no obliga a crear un cargador de aplicaciones diferente, ni migrar hacia una especie de "escritorio web", sino que permite mantener esos sistemas ya disponibles, como el escritorio empresarial de este ejemplo, con tan sólo modificar la forma con la que cargan las aplicaciones.
Las ventajas de esta aproximación son grandísimas en cuanto a mantenibilidad y facilidad de despliegue de aplicaciones. Con esta solución, modificar una aplicación determinada no implica actualizarla en todos nuestros usuarios sino que la actualización se hará de una manera simple y transparente, al tiempo que nuestros usuarios siempre utilizan la última versión de nuestro software.