Tuesday, April 24, 2012

Saving on the fly generated PDF to file or database (Seam 2.2 application in JBoss AS 5.1)

With Seam, you can create a pdf file on the fly using only xhtml pages, but if you want to get the PDF file’s binary data, that is much harder task. The basic problem, that this function was not designed to provide the binary data, rather than just generate the PDF file using JSF custom tags. You can read the solution in a lot of blogs and forums: just create a mock FacesContext, use that for render the page, then extract the binary data. I have tried the solutions described in various places, but for JBoss AS 5.1 and Seam 2.2.0 GA they didn’t worked. The main problem was to create the mock FacesContext, because you have to have the javax.faces.application.Application and javax.faces.render.RenderKitFactory instances which I had no luck to get at the time of the creation of the mock FacesContext. But by implementing a servlet context listener, I was able to capture both values. You need to register the DocumentServlet and the custom servlet context listener into the web.xml, create the /simple.xhtml page (see later) and put the xhtml code in paragraph #3 into an other page (the menu, for example). The other classes should be put into the ejb module of your application, into the org.example.pdf package. Let's see the steps in detail:

1.Servlet Context Listener and Document Servlet registration in web.xml

In order the PDF generation on the fly to be able to work, you must add the followings servlet mapping to the web.xml. The servlet context listener is used to capture the javax.faces.application.Application and javax.faces.render.RenderKitFactory instances when the servlet context initialized.
<servlet>
    <servlet-name>Document Store Servlet</servlet-name>
    <servlet-class>org.jboss.seam.document.DocumentStoreServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Document Store Servlet</servlet-name>
    <url-pattern>*.pdf</url-pattern>
</servlet-mapping>

  <listener>
    <listener-class>
        org.example.pdf.MockServletContextListener
    </listener-class>
  </listener>

2. Page that generates PDF

You must create a an xhtml page ("/simple.xhtml"), to test the export function:
<p:document xmlns:p="http://jboss.com/products/seam/pdf">                                                      
    <p:chapter>Hello PDF!</p:chapter>
</p:document>

3. Testing xhtml code

In order to test the PDF export function, the following link could be placed somewhere (e.g,: into the menu). This will call the mockPDFFileSaver’s savePDF(fileName, viewName) method, which simply generated the pdf then saves the results into the given file. This example will save the pdf into "d:\simple.pdf"
<s:link 
       includePageParams="false" 
       propagation="none" 
       value="Simple PDF test" 
       action="#{mockPDFFileSaver.
         savePDF('d:\\simple.pdf','/simple.xhtml')}">
</s:link>

4.Servlet Context Listener

The listener can capture the „Application” and „Render Kit Factory” instances. It then saves into the Seam component „factoryData”. Lifecycle.beginCall() initializes Seam.
package org.example.pdf;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.jboss.seam.Component;
import org.jboss.seam.contexts.Lifecycle;

/**
 * ServletContextListener implementation to capture the JSF Application and
 * renderKitFactory. Should be registered into web.xml to work
 * 
 * @author anemeth
 * 
 */
public class MockServletContextListener implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent event) {
 }

 public void contextInitialized(ServletContextEvent event) {

  Lifecycle.beginCall();

  Application application = ((ApplicationFactory) FactoryFinder
    .getFactory(FactoryFinder.APPLICATION_FACTORY)).getApplication();

  RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
    .getFactory(FactoryFinder.RENDER_KIT_FACTORY);

  FactoryData factoryData = (FactoryData) Component.getInstance("factoryData", true);
  factoryData.setApplication(application);
  factoryData.setRenderKitFactory(renderKitFactory);

 }

}

5. Mock Servlet Context

The MockServletContext from Seam 2.2 was modified in order to work under JBoss AS 5.1
package org.example.pdf;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.jboss.seam.Component;
import org.jboss.seam.contexts.Lifecycle;

/**
 * ServletContextListener implementation to capture the JSF Application and
 * renderKitFactory. Should be registered into web.xml to work
 * 
 * @author anemeth
 * 
 */
public class MockServletContextListener implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent event) {
 }

 public void contextInitialized(ServletContextEvent event) {

  Lifecycle.beginCall();

  Application application = ((ApplicationFactory) FactoryFinder
    .getFactory(FactoryFinder.APPLICATION_FACTORY)).getApplication();

  RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
    .getFactory(FactoryFinder.RENDER_KIT_FACTORY);

  FactoryData factoryData = (FactoryData) Component.getInstance("factoryData", true);
  factoryData.setApplication(application);
  factoryData.setRenderKitFactory(renderKitFactory);

 }

}

6. FactoryData

This is a simple Seam component for holding the „javax.faces.application.Application” and „javax.faces.render.RenderKitFactory”
package org.example.pdf;

import javax.faces.application.Application;
import javax.faces.render.RenderKitFactory;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;

/**
 * Class for holding the JSF "Application" instance and the renderKitFactory.
 * @author anemeth
 *
 */
@Name("factoryData")
@Scope(ScopeType.APPLICATION)
public class FactoryData {
 
 private Application application;

 private RenderKitFactory renderKitFactory;
 
 /**
  * @return the application
  */
 public Application getApplication() {
  return this.application;
 }

 /**
  * @return the renderKitFactory
  */
 public RenderKitFactory getRenderKitFactory() {
  return this.renderKitFactory;
 }

 /**
  * @param application
  *            the application to set
  */
 public void setApplication(Application application) {
  this.application = application;
 }

 /**
  * @param renderKitFactory
  *            the renderKitFactory to set
  */
 public void setRenderKitFactory(RenderKitFactory renderKitFactory) {
  this.renderKitFactory = renderKitFactory;
 }

 /**
  * @see java.lang.Object#toString()
  */
 @Override
 public String toString() {
  StringBuilder builder = new StringBuilder();
  builder.append("FactoryData [application=");
  builder.append(this.application);
  builder.append(", renderKitFactory=");
  builder.append(this.renderKitFactory);
  builder.append("]");
  return builder.toString();
 }
}

7. MockPDFFileSaver

This is a simple Seam component to demonstrate the PDF export function
package org.example.pdf;

import java.io.FileOutputStream;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;

@Name("mockPDFFileSaver")
public class MockPDFFileSaver {

 @In(create = true)
 PdfExporter pdfExporter;

 /**
  * Saves the PDF generated by the pageName to the file denoted by the
  * fileName
  * 
  * @param fileName
  *            the file name
  * @param pageName
  *            the page name
  */
 public void savePDF(String fileName, String pageName) {
  byte[] pdfBytes = pdfExporter.pdfExport(pageName);
  try {
   FileOutputStream fos = new FileOutputStream(fileName);

   fos.write(pdfBytes);

   fos.close();
  } catch (Exception e) {

   e.printStackTrace();
  }
 }

}

8. PDFExporter

The pdf export functionality is implemented in this Seam component. This works by rendering the given view into a mock context. The DocumentStore then contains the rendered pdf. The real question is what the id of the generated pdf. The DocumentStrore is a Conversation scoped component so basically the Id will be started from 1 when a new conversation starts. But for sure, the Id can be queried, in the case more document is generated per conversation.
package org.example.pdf;

import java.io.ByteArrayOutputStream;

import org.jboss.seam.annotations.Name;
import org.jboss.seam.document.DocumentData;
import org.jboss.seam.document.DocumentStore;
import org.jboss.seam.faces.Renderer;

@Name("pdfExporter")
public class PdfExporter {
 
 /**
  * returns the byte array of the PDF file generated using the page xhtml 
  * @param page the page name of the xhtml for the pdf
  * @return the byte array containing the PDF file data
  */
 public byte[] pdfExport(String page) {

  byte[] pdfBytes = null;

  EmptyFacesContext emptyFacesContext = null;

  try {

   emptyFacesContext = new EmptyFacesContext();

   try {

    Renderer renderer = Renderer.instance();
    renderer.render(page);
    
    DocumentStore store = DocumentStore.instance();

    String nextId = store.newId();
    long docId = Long.parseLong(nextId) - 1;

    DocumentData data = store.getDocumentData("" + docId);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    data.writeDataToStream(baos);

    pdfBytes = baos.toByteArray();

   } catch (Exception e) {
    e.printStackTrace();
   }
  } catch (Exception e) {
   e.printStackTrace();

  } finally {
   emptyFacesContext.restore();
  }

  return pdfBytes;
 }

}

Friday, April 6, 2012

Localization Tool for Seam 2 applications

Internationalization and Localization in Seam applications

Seam uses the JSF's resource bundle approach to localization. Resource bundles are property files usually in the WEB-INF/classes folder. JSF can determine the locale using the followings (from Seam documentation):

  • If there is a locale associated with the HTTP request (the browser locale), and that locale is in the list of supported locales from faces-config.xml, use that locale for the rest of the session.
  • Otherwise, if a default locale was specified in the faces-config.xml, use that locale for the rest of the session.
  • Otherwise, use the default locale of the server.


To change the locale manually you should place some similar code into your application (e.g.: in the menu bar):
<h:selectOneMenu value="#{localeSelector.language}" 
    onchange="submit()">
    <f:selectItem itemLabel="#{messages.menu_language_en}" 
        itemValue="en"/>
    <f:selectItem itemLabel="#{messages.menu_language_de}" 
        itemValue="de"/>
    <f:selectItem itemLabel="#{messages.menu_language_hu}" 
        itemValue="hu"/>
</h:selectOneMenu>

In the above example three languages used: english (en), german (de) and hungarian (hu). The labels for the  selection list are also coming from the resource bundle, so they are displayed in the selected language. In the english resource bundle this looks like this way:


menu_language_de=German
menu_language_en=English
menu_language_hu=Hungarian

while in the german resource bundle:


menu_language_de=Deutsch
menu_language_en=English
menu_language_hu=Ungarisch

Seam will use the resource bundle of choice.

Enabling the localization in a Seam application


Modify the WEB-INF/components.xml to include the following:


<!-- Remember the locale selected -->
<international:locale-selector cookie-enabled="true"/>


where international is defined as

xmlns:international="http://jboss.com/products/seam/international"

For more information on internationalization please see Seam documentation: http://docs.jboss.com/seam/latest/reference/en-US/html/i18n.html


Pratical Localization

Let's suppose you have a Seam application developed in English language, and you have to add new languages. There could be String values in the Java code and in the xhtml pages. This tool deals only with the String literals in the xhtml pages. Every String which contains at least one letter (outside the EL expressions) are put into the resource bundle property files "messages_XX.properties". At the same time the tool changes the String literals in the xhtml pages to EL expression.

For example a h:commandButton without localization looks this way:

<h:commandButton action="#{providerHome.persist}" 
id="save" rendered="#{!providerHome.managed}" 
value="Create"/>

After the tool processed the xhtml code, the above excrept looks like this:

<h:commandButton action="#{providerHome.persist}" 
id="save" rendered="#{!providerHome.managed}" 
value="#{messages.provider_commandButton_Create}"
/>
You can see that the key for the "Create" String literal from the command button is:
provider_commandButton_Create. The first part "provider" is the name of the xhtml file without extension. The second part is the name of the JSF tag without namespace ("commandButton"), the last part is the String literal converted into a format suitable for being a key ("Create"). The non letter characters are converted into underscore. (Minus sign is not usable, because it is evaluated by the EL engine).

The provider_commandButton_Create is added to the messages_en.properties:
provider_commandButton_Create=Create
and for example the messages_de properties file:
provider_commandButton_Create=Erstellen
This kind of naming convention is applied to String literals inside value attribute of JSF tags. When the literal is a text node in the xhtml file, the middle part will be "TEXT":

<s:decorate id="nameField" template="layout/edit.xhtml">
     <ui:define name="label">#{messages.provider_TEXT_Name}
     </ui:define>
     <h:inputtext id="name" required="true" value="#{providerHome.instance.name}">
</h:inputtext></s:decorate>

Localizing date/time formats

The localization would not be complete without date formats. For example in hungary the following format is used: 2012-04-05 12:33:17 , while the traditional german format would be: 05-04-2012 12:33:17. The tool inserts the following key to the resource bundle property files: date_time_format. This will be used in 

<h:outputtext style="align: center;" value="#{user.registrationDate}">
    <f:convertdatetime pattern="#{messages.date_time_format}" timezone="CET">
</f:convertdatetime></h:outputtext>


Localization Workflow

  1. Develop your application in your preferred language (e.g,: english).
  2. Create message_XX.properties files for the languages needed for localization, by copying the messages_en.properties file's content under new names, eg.: messages_de.properties. If you previously translated Seam's messages and other JSF related messages into the preferred language, you can copy the already existing properties files from other projects.
  3. Make a backup. This is important since I18nGen tool overwrites all xhtml files and all messages_XX.properties files it founds.
  4. Run I18nGen tool. This will populate the messages_XX.properties files with string literals from the xhtml files with the languge used for development.
  5. Check the properties files. The tool will use the "old" values from the property file. There are three categories listed in the property file:
    1. new property: The property was not found in the original property file, but was on the xhtml page
    2. old but used property: The property was in the original property file but it is also coming from the xhtml page. In this case the old value is not overwritten. If there is a conflict between the new and the old value, this will be listed in the beginning of the property file as a commented line.
    3. old and not used: This means that the property was in the original property file, but not found in the xhtml pages. This could mean the property is for Seam's or JSF messages, but could also mean that you have copied the property from an other project and in your current project that property is not used, so could delete it. It is important to note that if you delete a property, the tool won't place it again in the property file, because in the xhtml page, the String literal was converted into an EL expression, and the tool skips EL expressions.
  6. When you prepared the property file for translation, you should decide if you translate the messages yourself or you pass them to a translator. In the later case, things are a bit complicated because  the property file should be encoded in ISO-8859-1 (Latin-1 charset), and characters not in this character set should be expressed as unicode escape sequences, for example the greek letter  Π (pi) should be expressed as \u03A0, but an usual translator can hardly find every non Latin-1 characrers unicode escape codes, nor he or she can use a developer tool, such as Eclipse's property editor which is capable of handling the character encoding problem. The simplest solution is to send the file as Word document, with the note that existing lines should not be broken and the EL expressions should not be changed either.
  7. The translator does his or her job, then sends back the translated messages in a Word document.
  8. You should convert back the results into the property file. This can be done by opening the property file in Eclipse, then copy-paste the translated content into the opened property file. Eclipse will handle the unicode escaping.
  9. Build your application and deploy it.
  10. You can test the translation by going to every page in your application.
  11. If something is not translated or the meaining is not exactly what you think, you can change the property files. When new pages are added to the application, you can re-run the tool, which will add new properties to the resource bundle property files, and you can restart the translation process.
I18nGen tool


You can download the tool & source code in the form of an executable jar file from here: I18nGen.jar

Usage

From command line the syntax is the following:


java -jar I18nGen.jar <Full path for the directory for xhtml files> <Full path for the directory for the messages_XX.properties>

Example:


java -jar c:\tools\I18nGen.jar c:\workspace\myprj\WebContent c:\workspace\myprj\src


The tool will recursively scan all files with ".xhtml" extension and collects the String literals. The xhtml format is basically XML, so in terms of XML, the tool collects string literals from text nodes and from attributes, where the attribute name is "value", except from the graphicImage tag. The tool excludes literals which has no letters on it (so the Strings which contains only whitespaces and EL expressions are missed out).


The attribute value for the pattern attribute of the convertDateTime tag will be set to 
"#{messages.date_time_format}" and the property date_time_format with the value of "yyyy-MM-dd HH:mm:ss z" will be added.


The messages_XX.properties files will be rewritten in the following form. The first part is the header with a date/time. Then property "collision" information follows. The next part is the section where are properties which is used in the xhtml files but were also in the previous version of the property file. New properties are such properties which are not found in the previous version of the property file, only in the xhtml pages. The last section is for properties which were found in the previous version of the property file, but not in the xhtml pages, so you can think about deleting them (if their names are not begin with "javax.", "validator" or "org.jboss.seam", because these are JSF or Seam related messages)

##############################################
# Generated on Fri Apr 06 13:57:39 CEST 2012
##############################################
# Comments: 
# Property: "configuration_TEXT_Keystore_Password" already exist with value: "3. Keystore Password", the new value would have been: "1. Keystore Password"
# Property: "configuration_TEXT_Certificate_Alias" already exist with value: "3. Certificate Alias", the new value would have been: "1. Certificate Alias"
# Property: "configuration_TEXT_Keystore_File" already exist with value: "3. Keystore File", the new value would have been: "2. Keystore File"
# Property: "configuration_TEXT_Keystore_Password" already exist with value: "3. Keystore Password", the new value would have been: "2. Keystore Password"
# Property: "configuration_TEXT_Certificate_Alias" already exist with value: "3. Certificate Alias", the new value would have been: "2. Certificate Alias"

###############################################
# OLD but USED properties
###############################################

# error
error_TEXT_Error=Error
error_TEXT_Something_bad_happened_=Something bad happened :-(
error_param_false=false

############################################
# NEW properties
############################################

# resource
resource_TEXT_Content_Type=Content-Type
resource_TEXT_Description=Description
resource_TEXT_Download=Download
resource_commandButton_Create=Create
resource_commandButton_Delete=Delete
resource_commandButton_Update=Update

##############################################
# OLD and UNUSED properties (some entries may 
# be removable, except JSF and seam properties)
##############################################

# validator
validator.assertFalse=ellen\u0151rz\u00E9s sikertelen
validator.assertTrue=ellen\u0151rz\u00E9s sikertelen
validator.email=j\u00F3l form\u00E1zott email c\u00EDmnek kell lennie
validator.future=j\u00F6v\u0151beli d\u00E1tumnak kell lennie
validator.length=a hossznak {min} \u00E9s {max} k\u00F6z\u00F6tt kell lennie
validator.max=kisebb vagy egyenl\u0151nek kell lennie a k\u00F6vetkez\u0151n\u00E9l: {value}
validator.min=nagyobbnak vagy egyenl\u0151nek kell lennie a k\u00F6vetkez\u0151n\u00E9l: {value}
validator.notNull=may not be null
validator.past=m\u00FAltb\u00E9li d\u00E1tumnak kell lennie
validator.pattern=meg kell felelnie a k\u00F6vetkez\u0151nek: "{regex}"
validator.range=a k\u00F6vetkez\u0151k k\u00F6z\u00E9 kell esnie: {min} - {max}
validator.size=a m\u00E9retnek a k\u00F6vetkez\u0151k k\u00F6z\u00F6tt kell lennie: {min} - {max}