This section discusses facilities that are provided as part of the Programming Model Extensions in WebSphere Application Server:
The exception-chaining and command packages are available as part of WebSphere Application Server Advanced Edition and Enterprise Edition; the localizable-text package is available as part of WebSphere Application Server Advanced Edition. All three packages are general-purpose utilities, designed to provide common functions in a reusable way. Although these facilities are described in the context of enterprise beans, they are available to any WebSphere Application Server Java application. They are not restricted to use with enterprise beans.
Distributed applications require a strategy for exception handling. As applications become more complex and are used by more participants, handling exceptions becomes problematic. To capture the information contained in every exception, methods have to rethrow every exception they catch. If every method adopts this approach, the number of exceptions can become unmanageable, and the code itself becomes less maintainable. Furthermore, if a new method introduces a new exception, all existing methods that call the new method have to be modified to handle the new exception. Trying to explicitly manage every possible exception in a complex application quickly becomes intractable.
In order to keep the number of exceptions manageable, some programmers adopt a strategy in which methods catch all exceptions in a single clause and throw one exception in response. This reduces the number of exceptions each method must recognize, but it also means that the information about the originating exception is lost. This loss of information can be desirable, for example, when you wish to hide implementation details from end users. However, this strategy can make applications much more difficult to debug.
The distributed-exception package provides a facility that allows you to build chains of exceptions. An exception chain encapsulates the stack of previous exceptions. With an exception chain, you can throw one exception in response to another without discarding the previous exceptions, so you can manage the number of exceptions without losing the information they carry. Exceptions that support chaining are called distributed exceptions.
This section provides a general description of the interfaces and classes in the exception-chaining package.
Figure 71. Code example: Constructors for the DistributedException class
... public class DistributedException extends Exception implements DistributedExceptionEnabled { // Constructors public DistributedException() {...} public DistributedException(String message) {...} public DistributedException(Throwable exception) {...} public DistributedException(String message,Throwable exception) {...} public DistributedException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText) {...} public DistributedException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText, Throwable exception) {...} // Other methods ... } |
When implementing the DistributedExceptionEnabled interface, you must declare a DistributedExceptionInfo attribute. This attribute provides implementations for most of these methods, so implementing them in your exception class consists of calling the corresponding methods on the DistributedExceptionInfo object. For more information, see Implementing the methods from the DistributedExceptionEnabled interface.
... import com.ibm.websphere.exception.*; public class MyDistributedException extends DistributedException { // Constructors public MyDistributedException() { super(); } public MyDistributedException(String message) { super(message); } public MyDistributedException(Throwable exception) { super(exception); } public MyDistributedException(String message, Throwable exception) { super(message, exception); } public MyDistributedException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText) { super(resourceBundleName, resourceKey, formatArguments, defaultText); } public MyDistributedException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText, Throwable exception) { super(resourceBundleName, resourceKey, formatArguments, defaultText, exception); } } |
... import javax.ejb.*; import com.ibm.websphere.exception.*; public class AccountCreateException extends CreateException implements DistributedExceptionEnabled { DistributedExceptionInfo exceptionInfo = null; // Constructors ... // Methods from the DistributedExceptionEnabled interface ... } |
... public class AccountCreateException extends CreateException implements DistributedExceptionEnabled { DistributedExceptionInfo exceptionInfo = null; // Constructors AccountCreateException() { super (); exceptionInfo = new DistributedExceptionInfo(this); } AccountCreateException(String msg) { super (msg); exceptionInfo = new DistributedExceptionInfo(this); } AccountCreateException(Throwable e) { super (); exceptionInfo = new DistributedExceptionInfo(this, e); } AccountCreateException(String msg, Throwable e) { super (msg); exceptionInfo = new DistributedExceptionInfo(this, e); } AccountCreateException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText) { super (); exceptionInfo = new DistributedExceptionInfo(resourceBundleName, resourceKey, formatArguments, defaultText, this); } AccountCreateException(String resourceBundleName, String resourceKey, Object[] formatArguments, String defaultText, Throwable exception) { super (); exceptionInfo = new DistributedExceptionInfo(resourceBundleName, resourceKey, formatArguments, defaultText, this, exception); } // Methods from the DistributedExceptionEnabled interface ... } |
Figure 75. Code example: Implementations of the methods in the DistributedExceptionEnabled interface
... public class AccountCreateException extends CreateException implements DistributedExceptionEnabled { DistributedExceptionInfo exceptionInfo = null; // Constructors ... // Methods from the DistributedExceptionEnabled interface String getMessage() { if (exceptionInfo != null) return exceptionInfo.getMessage(); else return null; } Throwable getPreviousException() { if (exceptionInfo != null) return exceptionInfo.getPreviousException(); else return null; } Throwable getOriginalException() { if (exceptionInfo != null) return exceptionInfo.getOriginalException(); else return null; } Throwable getException(String exceptionClassName) { if (exceptionInfo != null) return exceptionInfo.getException(exceptionClassName); else return null; } DistributedExceptionInfo getExceptionInfo() { if (exceptionInfo != null) return exceptionInfo; else return null; } void printStackTrace() { if (exceptionInfo != null) return exceptionInfo.printStackTrace(); else return null; } void printStackTrace(PrintWriter pw) { if (exceptionInfo != null) return exceptionInfo.printStackTrace(pw); else return null; } void printSuperStackTrace(PrintWriter pw) if (exceptionInfo != null) return super.printStackTrace(pw); else return null; } } |
You can catch exceptions that extend the DistributedException class or implement the DistributedExceptionEnabled interface separately. You can also test a caught exception to see if it has implemented the DistributedExceptionEnabled interface. If it has, you can treat it as any other distributed exception. Figure 76 shows the use of the instanceof method to test for exception chaining.
.... try { someMethod(); } catch (Exception e) { ... if (e instanceof DistributedExceptionEnabled) { ... } ... |
To add an exception to a chain, you must call one of the constructors for your distributed-exception class. This captures the previous exception information and packages it with the new exception. Figure 77 shows the use of the MyDistributedException(Throwable) constructor.
Figure 77. Code example: Adding an exception to a chain
void someMethod() throws MyDistributedException { try { someOtherMethod(); } catch (DistributedExceptionEnabled e) { throw new MyDistributedException(e); } ... }... |
Chained exceptions allow you to retrieve information about prior exceptions in the chain. For example, the getPreviousException, getOriginalException, and getException(String) methods allow you to retrieve specific exceptions from the chain. You can retrieve the message associated with the current exception by calling the getMessage method. You can also get information about the entire chain by calling one of the printStackTrace methods. Figure 78 illustrates calling the getPreviousException and getOriginalException methods.
Figure 78. Code example: Extracting exceptions from a chain
... try { someMethod(); } catch (DistributedExceptionEnabled e) { try { Throwable prev = e.getPreviousException(); } catch (ExceptionInstantiationException eie) { DistributedExceptionInfo prevExInfo = e.getPreviousExceptionInfo(); if (prevExInfo != null) { String prevExName = prevExInfo.getClassName(); String prevExMsg = prevExInfo.getClassMessage(); ... } } try { Throwable orig = e.getOriginalException(); } catch (ExceptionInstantiationException eie) { DistributedExceptionInfo origExInfo = null; DistributedExceptionInfo prevExInfo = e.getPreviousExceptionInfo(); while (prevExInfo != null) { origExInfo = prevExInfo; prevExInfo = prevExInfo.getPreviousExceptionInfo(); } if (origExInfo != null) { String origExName = origExInfo.getClassName(); String origExMsg = origExInfo.getClassMessage(); ... } } } ... |
In addition to giving you a way to reduce the number of remote invocations a client makes, the command package provides a generic way of making requests. A client instantiates the command, sets its input data, and tells it to run. The command infrastructure determines the target server and passes a copy of the command to it. The server runs the command, sets any output data, and copies it back to the client. The package provides a common way to issue a command, locally or remotely, and independently of the server's implementation. Any server (an enterprise bean, a Java Database Connectivity (JDBC) server, a servlet, and so on) can be a target of a command if the server supports Java access to its resources and provides a way to copy the command between the client's Java Virtual Machine (JVM) and its own JVM.
The command facility is implemented in the com.ibm.websphere.command Java package. The classes and interfaces in the command package fall into four general categories:
This section provides a general description of the interfaces and classes in the command package.
In practice, most commands implement the TargetableCommand interface, which allows the command to be executed remotely. Figure 79 shows the structure of a command interface for a targetable command.
Figure 79. Code example: The structure of an interface for a targetable command
... import com.ibm.websphere.command.*; public interface MySimpleCommand extends TargetableCommand { // Declare application methods here } |
The CompensableCommand interface allows the association of one command with another that can undo the work of the first. Compensable commands also typically implement the TargetableCommand interface. Figure 80 shows the structure of a command interface for a targetable, compensable command.
Figure 80. Code example: The structure of an interface for a targetable, compensable command
... import com.ibm.websphere.command.*; public interface MyCommand extends TargetableCommand, CompensableCommand { // Declare application methods here } |
You implement your command interface by writing a class that extends the TargetableCommandImpl class and implements your command interface. This class contains the code for the methods in your interface, the methods inherited from extended interfaces (the TargetableCommand and CompensableCommand interfaces), and the required (abstract) methods in the TargetableCommandImpl class. You can also override the default implementations of other methods provided in the TargetableCommandImpl class. Figure 81 shows the structure of an implementation class for the interface in Figure 80.
Figure 81. Code example: The structure of an implementation class for a command interface
... import java.lang.reflect.*; import com.ibm.websphere.command.*; public class MyCommandImpl extends TargetableCommandImpl implements MyCommand { // Set instance variables here ... // Implement methods in the MyCommand interface ... // Implement methods in the CompensableCommand interface ... // Implement abstract methods in the TargetableCommandImpl class ... } |
Common ways to implement the CommandTarget interface include:
Figure 82. Code example: The structure of a command-target entity bean
... import java.rmi.RemoteException; import java.util.Properties; import javax.ejb.*; import com.ibm.websphere.command.*; // Remote interface for the MyBean enterprise bean (also a command target) public interface MyBean extends EJBObject, CommandTarget { // Declare methods for the remote interface ... } // Entity bean class for the MyBean enterprise bean (also a command target) public class MyBeanClass implements EntityBean, CommandTarget { // Set instance variables here ... // Implement methods in the remote interface ... // Implement methods in the EntityBean interface ... // Implement the method in the CommandTarget interface ... } |
Although the CommandException class extends the DistributedException class, you do not have to import the distributed-exception package, com.ibm.websphere.exception, unless you need to use the features of the DistributedException class in your application. For more information on distributed exceptions, see The distributed-exception package.
The implementation class for your interface must contain implementations for the isReadyToCallExecute and reset methods. The execute method is implemented for you elsewhere; for more information, see Implementing command interfaces. Most commands do not extend the Command interface directly but use one of the provided extensions: the TargetableCommand interface and the CompensableCommand interface.
With the exception of the performExecute method, which you must implement, all of these methods are implemented in the TargetableCommandImpl class. This class also implements the execute method declared in the Command interface.
The CompensableCommand interface declares one method:
To create a compensable command, you write an interface that extends the CompensableCommand interface. Such interfaces typically extend the TargetableCommand interface as well. You must implement the getCompensatingCommand method in the implementation class for your interface. You must also implement the compensating command.
The example used throughout the remainder of this discussion uses an entity bean with container-managed persistence (CMP) called CheckingAccountBean, which allows a client to deposit money, withdraw money, set a balance, get a balance, and retrieve the name on the account. This entity bean also accepts commands from the client. The code examples illustrate the command-related programming. For a servlet-based example, see Writing a command target (client-side adapter).
Figure 83 shows the interface for the ModifyCheckingAccountCmd command. This command is both targetable and compensable, so the interface extends both TargetableCommand and CompensableCommand interfaces.
Figure 83. Code example: The ModifyCheckingAccountCmd interface
... import com.ibm.websphere.exception.*; import com.ibm.websphere.command.*; public interface ModifyCheckingAccountCmd extends TargetableCommand, CompensableCommand { float getAmount(); float getBalance(); float getOldBalance(); // Used for compensating float setBalance(float amount); float setBalance(int amount); CheckingAccount getCheckingAccount(); void setCheckingAccount(CheckingAccount newCheckingAccount); TargetPolicy getCmdTargetPolicy(); ... } |
Figure 84. Code example: The structure of the ModifyCheckingAccountCmdImpl class
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { // Variables ... // Methods ... } |
The class must declare any variables and implement these methods:
The ModifyCheckingAccountCmdImpl class declares the variables used by the methods in the class, including the remote interface of the CheckingAccount entity bean; the variables used to capture operations on the checking account (balances and amounts); and a compensating command. Figure 85 shows the variables used by the ModifyCheckingAccountCmd command.
Figure 85. Code example: The variables in the ModifyCheckingAccountCmdImpl class
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { // Variables public float balance; public float amount; public float oldBalance; public CheckingAccount checkingAccount; public ModifyCheckingAccountCompensatorCmd modifyCheckingAccountCompensatorCmd; ... } |
You must provide a way to instantiate the command. The command package does not specify the mechanism, so you can choose the technique most appropriate for your application. The fastest and most efficient technique is to use constructors. The most flexible technique is to use a factory. Also, since commands are implemented internally as JavaBeans components, you can use the standard Beans.instantiate method. The ModifyCheckingAccountCmd command uses constructors.
Figure 86 shows the two constructors for the command. The difference between them is that the first uses the default target policy for determining the target of the command and the second allows you to specify a custom policy. (For more information on targets and target policies, see Targets and target policies.)
Both constructors take a CommandTarget object as an argument and cast it to the CheckingAccount type. The CheckingAccount interface extends both the CommandTarget interface and the EJBObject (see Figure 95). The resulting checkingAccount object routes the command to the desired server by using the bean's remote interface. (For more information on CommandTarget objects, see Writing a command target (server).)
Figure 86. Code example: Constructors in the ModifyCheckingAccountCmdImpl class
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { // Variables ... // Constructors // First constructor: relies on the default target policy public ModifyCheckingAccountCmdImpl(CommandTarget target, float newAmount) { amount = newAmount; checkingAccount = (CheckingAccount)target; setCommandTarget(target); } // Second constructor: allows you to specify a custom target policy public ModifyCheckingAccountCmdImpl(CommandTarget target, float newAmount, TargetPolicy targetPolicy) { setTargetPolicy(targetPolicy); amount = newAmount; checkingAccount = (CheckingAccount)target; setCommandTarget(target); } ... } |
Figure 87 shows the implementation of the command-specific methods:
Figure 87. Code example: Command-specific methods in the ModifyCheckingAccountCmdImpl class
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { // Variables ... // Constructors ... // Methods in ModifyCheckingAccountCmd interface public float getAmount() { return amount; } public float getBalance() { return balance; } public float getOldBalance() { return oldBalance; } public float setBalance(float amount) { balance = balance + amount; return balance; } public float setBalance(int amount) { balance += amount ; return balance; } public TargetPolicy getCmdTargetPolicy() { return getTargetPolicy(); } public void setCheckingAccount(CheckingAccount newCheckingAccount) { if (checkingAccount == null) { checkingAccount = newCheckingAccount; } else System.out.println("Incorrect Checking Account (" + newCheckingAccount + ") specified"); } public CheckingAccount getCheckingAccount() { return checkingAccount; } ... } |
The ModifyCheckingAccountCmd command operates on a checking account. Because commands are implemented as JavaBeans components, you manage input and output properties of commands using the standard JavaBeans techniques. For example, initialize input properties with set methods (like setCheckingAccount) and retrieve output properties with get methods (like getCheckingAccount). The get methods do not work until after the command's execute method has been called.
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { ... // Methods from the Command interface public boolean isReadyToCallExecute() { if (checkingAccount != null) return true; else return false; } public void reset() { amount = 0; balance = 0; oldBalance = 0; checkingAccount = null; targetPolicy = new TargetPolicyDefault(); } ... } |
In addition, the ModifyCheckingAccountCmdImpl class overrides the default implementation of the setOutputProperties method.
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { ... // Method from the TargetableCommand interface public void performExecute() throws Exception { CheckingAccount checkingAccount = getCheckingAccount(); oldBalance = checkingAccount.getBalance(); balance = oldBalance+amount; checkingAccount.setBalance(balance); setHasOutputProperties(true); } public void setOutputProperties(TargetableCommand fromCommand) { try { if (fromCommand != null) { ModifyCheckingAccountCmd modifyCheckingAccountCmd = (ModifyCheckingAccountCmd) fromCommand; this.oldBalance = modifyCheckingAccountCmd.getOldBalance(); this.balance = modifyCheckingAccountCmd.getBalance(); this.checkingAccount = modifyCheckingAccountCmd.getCheckingAccount(); this.amount = modifyCheckingAccountCmd.getAmount(); } } catch (Exception ex) { System.out.println("Error in setOutputProperties."); } } ... } |
... public class ModifyCheckingAccountCmdImpl extends TargetableCommandImpl implements ModifyCheckingAccountCmd { ... // Method from CompensableCommand interface public Command getCompensatingCommand() throws CommandException { modifyCheckingAccountCompensatorCmd = new ModifyCheckingAccountCompensatorCmd(this); return (Command)modifyCheckingAccountCompensatorCmd; } } |
An application that uses a compensable command requires two separate commands: the primary command (declared as a CompensableCommand) and the compensating command. In the example application, the primary command is declared in the ModifyCheckingAccountCmd interface and implemented in the ModifyCheckingAccountCmdImpl class. Because this command is also a compensable command, there is a second command associated with it that is designed to undo its work. When you create a compensable command, you also have to write the compensating command.
Writing a compensating command can require exactly the same steps as writing the original command: writing the interface and providing an implementation class. In some cases, it may be simpler. For example, the command to compensate for the ModifyCheckingAccountCmd does not require any methods beyond those defined for the original command, so it does not need an interface. The compensating command, called ModifyCheckingAccountCompensatorCmd, simply needs to be implemented in a class that extends the TargetableCommandImpl class. This class must:
Figure 91 shows the structure of the implementation class, its variables (references to the original command and to the relevant checking account), and the constructor. The constructor simply instantiates the references to the primary command and account.
Figure 91. Code example: Variables and constructor in the ModifyCheckingAccountCompensatorCmd class
... public class ModifyCheckingAccountCompensatorCmd extends TargetableCommandImpl { public ModifyCheckingAccountCmdImpl modifyCheckingAccountCmdImpl; public CheckingAccount checkingAccount; public ModifyCheckingAccountCompensatorCmd( ModifyCheckingAccountCmdImpl originalCmd) { // Get an instance of the original command modifyCheckingAccountCmdImpl = originalCmd; // Get the relevant account checkingAccount = originalCmd.getCheckingAccount(); } // Methods from the Command and Targetable Command interfaces .... } |
Figure 92 shows the implementation of the inherited methods. The implementation of the isReadyToCallExecute method ensures that the checkingAccount variable has been instantiated.
The performExecute method verifies that the actual checking-account balance is consistent with what the original command returns. If so, it replaces the current balance with the previously stored balance by using the ModifyCheckingAccountCmd command. Finally, it saves the most-recent balances in case the compensating command needs to be undone. The reset method has no work to do.
Figure 92. Code example: Methods in ModifyCheckingAccountCompensatorCmd class
... public class ModifyCheckingAccountCompensatorCmd extends TargetableCommandImpl { // Variables and constructor .... // Methods from the Command and TargetableCommand interfaces public boolean isReadyToCallExecute() { if (checkingAccount != null) return true; else return false; } public void performExecute() throws CommandException { try { ModifyCheckingAccountCmdImpl originalCmd = modifyCheckingAccountCmdImpl; // Retrieve the checking account modified by the original command CheckingAccount checkingAccount = originalCmd.getCheckingAccount(); if (modifyCheckingAccountCmdImpl.balance == checkingAccount.getBalance()) { // Reset the values on the original command checkingAccount.setBalance(originalCmd.oldBalance); float temp = modifyCheckingAccountCmdImpl.balance; originalCmd.balance = originalCmd.oldBalance; originalCmd.oldBalance = temp; } else { // Balances are inconsistent, so we cannot compensate throw new CommandException( "Object modified since this command ran."); } } catch (Exception e) { System.out.println(e.getMessage()); } } public void reset() {} } |
To use a command, the client creates an instance of the command and calls the command's execute method. Depending on the command, calling other methods can be necessary. The specifics will vary with the application.
In the example application, the server is the CheckingAccountBean, an entity enterprise bean. In order to use this enterprise bean, the client gets a reference to the bean's home interface. The client then uses the reference to the home interface and one of the bean's finder methods to obtain a reference to the bean's remote interface. If there is no appropriate bean, the client can create one using a create method on the home interface. All of this work is standard enterprise bean programming covered elsewhere in this document.
Figure 93 illustrates the use of the ModifyCheckingAccountCmd command. This work takes place after an appropriate CheckingAccount bean has been found or created. The code instantiates a command, setting the input values by using one of the constructors defined for the command. The null argument indicates that the command should look up the server using the default target policy, and 1000 is the amount the command attempts to add to the balance of the checking account. (For more information on how the command package uses defaults to determine the target of a command, see The default target policy.) After the command is instantiated, the code calls the setCheckingAccount method to identify the account to be modified. Finally, the execute method on the command is called.
Figure 93. Code example: Using the ModifyCheckingAccountCmd command
{ ... CheckingAccount checkingAccount ... try { ModifyCheckingAccountCmd cmd = new ModifyCheckingAccountCmdImpl(null, 1000); cmd.setCheckingAccount(checkingAccount); cmd.execute(); } catch (Exception e) { System.out.println(e.getMessage()); } ... } |
To use a compensating command, you must retrieve the compensator associated with the primary command and call its execute method. Figure 94 shows the code used to run the original command and to give the user the option of undoing the work by running the compensating command.
Figure 94. Code example: Using the ModifyCheckingAccountCompensator command
{ ... CheckingAccount checkingAccount .... try { ModifyCheckingAccountCmd cmd = new ModifyCheckingAccountCmdImpl(null, 1000); cmd.setCheckingAccount(checkingAccount); cmd.execute(); ... System.out.println("Would you like to undo this work? Enter Y or N"); try { // Retrieve and validate user's response ... } ... if (answer.equalsIgnoreCase(Y)) { Command compensatingCommand = cmd.getCompensatingCommand(); compensatingCommand.execute(); } } catch (Exception e) { System.out.println(e.getMessage()); } ... } |
The example application implements the CommandTarget interface in an enterprise bean. (For a servlet-based example, see Writing a command target (client-side adapter).) The target enterprise bean can be a session bean or an entity bean. You can write a target enterprise bean that forwards commands to a specific server, such as another entity bean. In this case, all commands directed at a specific target go through the target enterprise bean. You can also write a target enterprise bean that does the work of the command locally.
Make an enterprise bean the target of a command by:
... import com.ibm.websphere.command.*; import javax.ejb.EJBObject; import java.rmi.RemoteException; public interface CheckingAccount extends CommandTarget, EJBObject { float deposit (float amount) throws RemoteException; float deposit (int amount) throws RemoteException; String getAccountName() throws RemoteException; float getBalance() throws RemoteException; float setBalance(float amount) throws RemoteException; float withdrawal (float amount) throws RemoteException, Exception; float withdrawal (int amount) throws RemoteException, Exception; } |
Figure 96. Code example: The bean class for the CheckingAccount entity bean, also a command target
... public class CheckingAccountBean implements EntityBean, CommandTarget { // Bean variables ... // Business methods from remote interface ... // Life-cycle methods for CMP entity beans ... // Method from the CommandTarget interface public TargetableCommand executeCommand(TargetableCommand command) throws RemoteException, CommandException { try { command.performExecute(); } catch (Exception ex) { if (ex instanceof RemoteException) { RemoveException remoteException = (RemoteException)ex; if (remoteException.detail != null) { throw new CommandException(remoteException.detail); } throw new CommandException(ex); } } if (command.hasOutputProperties()) { return command; } return null; } } |
The command package needs to be able to identify the target of a command. Because there is more than one way to specify the target and because different applications can have different requirements, the command package does not specify a selection algorithm. Instead, it provides a TargetPolicy interface with one method, getCommandTarget, and a default implementation. This allows applications to devise custom algorithms for determining the target of a command when appropriate.
If it finds no target, it returns null. The TargetPolicyDefault class provides methods for managing the assignment of commands with targets (registerCommand, unregisterCommand, and listMappings), and a method for setting a default name for the target (setDefaultTargetName). The default target name is com.ibm.websphere.command.LocalTarget, where LocalTarget is a class that runs the command's performExecute method locally. Figure 97 shows the relevant variables and the methods in the TargetPolicyDefault class.
Figure 97. Code example: The TargetPolicyDefault class
... public class TargetPolicyDefault implements TargetPolicy, Serializable { ... protected String defaultTargetName = "com.ibm.websphere.command.LocalTarget"; public CommandTarget getCommandTarget(TargetableCommand command) { ... } public Dictionary listMappings() { ... } public void registerCommand(String commandName, String targetName) { ... } public void unregisterCommand(String commandName) { ... } public void seDefaultTargetName(String defaultTargetName) { ... } } |
The example in Figure 98 uses the same constructor to set the target explicitly. This example differs from Figure 93 as follows:
Figure 98. Code example: Identifying a target with CommandTarget
{ ... CheckingAccount checkingAccount .... try { ModifyCheckingAccountCmd cmd = new ModifyCheckingAccountCmdImpl(checkingAccount, 1000); cmd.execute(); } catch (Exception e) { System.out.println(e.getMessage()); } ... } |
Figure 99. Code example: Identifying a target with CommandTargetName
{ ... CheckingAccount checkingAccount .... try { ModifyCheckingAccountCmd cmd = new ModifyCheckingAccountCmdImpl(null, 1000); cmd.setCheckingAccount(checkingAccount); cmd.setCommandTargetName("com.ibm.sfc.cmd.test.CheckingAccountBean"); cmd.execute(); } catch (Exception e) { System.out.println(e.getMessage()); } ... } |
Figure 100 shows the registration of a command with a target. The names of the command class and the target are explicit in the code, but in practice, these values would come from fields in a user interface or arguments to a command-line tool. If a program creates a command as shown in Figure 93, with a null for the target, when the default target policy traverses its choices, it finds a null for the first and second choices and a mapping for the third.
Figure 100. Code example: Mapping a command to a target in an external application
{ ... targetPolicy.registerCommand( "com.ibm.sfc.cmd.test.ModifyCheckingAccountImpl", "com.ibm.sfc.cmd.test.CheckingAccountBean"); ... } |
So far, the target of all the commands has been a checking-account entity bean. Suppose that someone introduces a session enterprise bean (MySessionBean) that can also act as a command target. Figure 101 shows a simple custom policy that sets the target of every command to MySessionBean.
Figure 101. Code example: Creating a custom target policy
... import java.io.*; import java.util.*; import java.beans.*; import com.ibm.websphere.command.*; public class CustomTargetPolicy implements TargetPolicy, Serializable { public CustomTargetPolicy { super(); } public CommandTarget getCommandTarget(TargetableCommand command) { CommandTarget = null; try { target = (CommandTarget)Beans.instantiate(null, "com.ibm.sfc.cmd.test.MySessionBean"); } catch (Exception e) { e.printStackTrace(); } } } |
Since commands are implemented as JavaBeans components, using custom target policies requires importing the java.beans package and writing some elementary JavaBeans code. Also, your custom target-policy class must also implement the java.io.Serializable interface.
Figure 102. Code example: Using a custom target policy
{ ... CheckingAccount checkingAccount .... try { CustomTargetPolicy customPolicy = new CustomTargetPolicy(); ModifyCheckingAccountCmd cmd = new ModifyCheckingAccountCmdImpl(null, 1000, customPolicy); cmd.setCheckingAccount(checkingAccount); cmd.execute(); cmd.reset(); } catch (Exception e) { System.out.println(e.getMessage()); } } |
In this example, the client implements the CommandTarget interface locally. Figure 103 shows the structure of the client-side class; it implements the CommandTarget interface by implementing the executeCommand method.
Figure 103. Code example: The structure of a client-side adapter for a target
... import java.io.*; import java.rmi.*; import com.ibm.websphere.command.*; public class ServletCommandTarget implements CommandTarget, Serializable { protected String hostName = "localhost"; public static void main(String args[]) throws Exception { .... } public TargetableCommand executeCommand(TargetableCommand command) throws CommandException { .... } public static final byte[] serialize(Serializable serializable) throws IOException { ... } public String getHostName() { ... } public void setHostName(String hostName) { ... } private static void showHelp() { ... } } |
The main method in the client-side adapter constructs and intializes the CommandTarget object, as shown in Figure 104.
Figure 104. Code example: Instantiating the client-side adapter
public static void main(String args[]) throws Exception { String hostName = InetAddress.getLocalHost().getHostName(); String fileName = "MyServletCommandTarget.ser"; // Parse the command line ... // Create and initialize the client-side CommandTarget adapter ServletCommandTarget servletCommandTarget = new ServletCommandTarget(); servletCommandTarget.setHostName(hostName); ... // Flush and close output streams ... } |
Figure 105. Code example: A client-side implementation of the executeCommand method
public TargetableCommand executeCommand(TargetableCommand command) throws CommandException { try { // Serialize the command byte[] array = serialize(command); // Create a connection to the servlet URL url = new URL ("http://" + hostName + "/servlet/com.ibm.websphere.command.servlet.CommandServlet"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); // Set the properties of the connection ... // Put the serialized command on the output stream OutputStream outputStream = httpURLConnection.getOutputStream(); outputStream.write(array); // Create a return stream InputStream inputStream = httpURLConnection.getInputStream(); // Send the command to the servlet httpURLConnection.connect(); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); // Retrieve the command returned from the servlet Object object = objectInputStream.readObject(); if (object instanceof CommandException) { throw ((CommandException) object); } // Pass the returned command back to the calling method return (TargetableCommand) object; } // Handle exceptions .... } |
Figure 106. Code example: Running the command in the servlet
... import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import com.ibm.websphere.command.*; public class CommandServlet extends HttpServlet { ... public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { ... // Create input and output streams InputStream inputStream = request.getInputStream(); OutputStream outputStream = response.getOutputStream(); // Retrieve the command from the input stream ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); TargetableCommand command = (TargetableCommand) objectInputStream.readObject(); // Create the command for the return stream Object returnObject = command; // Try to run the command's performExecute method try { command.performExecute(); } // Handle exceptions from the performExecute method ... // Return the command with any output properties ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(returnObject); // Flush and close output streams ... } catch (Exception ex) { ex.printStackTrace(); } } } |
In this example, the target invokes the performExecute method on the command, but this is not always necessary. In some applications, it can be preferable to implement the work of the command locally. For example, the command can be used only to send input data, so that the target retrieves the data from the command and runs a local database procedure based on the input. You must decide the appropriate way to use commands in your application.
Users of distributed applications can come from widely varying areas; they can speak different languages, represent dates and times in regionally specific ways, and use different currencies. An application intended to be used by such an audience must either force them all to use the same interface (for example, an English-based interface), or it can be written in such a way that it can be configured to the linguistic conventions of the users, so English-speaking users can use the English interface but French-speaking users can interact with the application through a French interface.
An application that can present information to users in formats that abide by the users' linguistic conventions is said to be localizable: the application can be configured to interact with users from different localities in linguistically appropriate ways. In a localized application, a user in one region sees error messages, output, and interface elements (like menu options) in the requested language. Additionally, other elements that are not strictly linguistic, like date and time formats and currencies, are presented in the appropriate style for users in the specified region. A user in another region sees output in the conventional language or format for that region.
Historically, the creation of localizable applications has been restricted to large corporations writing complex systems. The strategies for writing localizable code, collectively called internationalization techniques, have traditionally been expensive and difficult to implement, so they have been applied only to major development efforts. However, given the rise in distributed computing and in use of the World Wide Web, application developers have been pressured to make a much wider variety of applications localizable. This requires making internationalization--the techniques for writing localizable programs--much more accessible to application developers. The WebSphere localizable-text package is a set of Java classes and interfaces that can be used by WebSphere application developers to localize distributed WebSphere applications easily. Language catalogs for distributed WebSphere applications can be stored centrally, so the catalogs can be maintained and administered efficiently.
In a nonlocalizable application, parts of the application that a user sees are unalterably coded into the application. For example, a routine that prints an error message simply prints a string, probably in English, to a file or the console. A localizable program adds a layer of abstraction into the design. Instead of going simply from error condition to output string, a localizable program represents error messages with some language-neutral information; in the simplest case, each error condition corresponds to a key. In order to print a usable error string for the user, the application looks up the key in the configured message catalog. A message catalog is a list of keys with corresponding strings. Different message catalogs provide the strings in different languages. The application looks up the key in the appropriate catalog, retrieves the corresponding error message in the desired language, and prints this string for the user.
The technique of localization can be used for far more than translating error messages. For example, by using keys to represent each element--button, label, menu item, and so forth--in a graphical user interface and by providing a message catalog containing translations of the button names, labels, and menu items, the graphical interface can be automatically translated into multiple languages. In addition, extending support to additional languages requires providing message catalogs for those languages; the application itself requires no modification.
Localization of an application is driven by two variables, the time zone and the locale. The time zone variable indicates how to compute the local time as an offset from a standard time like Greenwich Mean Time. The locale is a collection of information that indicates a geographic, political, or cultural region. It provides information on language, currency, and the conventions for presenting information like dates, and in a localizable program, the locale also indicates the message catalog from which an application retrieves messages. A time zone can cover many locales, and a single locale can span time zones. With both time zone and locale, the date, time, currency, and language for users in a specific region can be determined.
To write a localizable application, an application developer must determine which aspects of the application need to be translatable. These are typically the parts of an application a user must read and understand. Application developers must consider the parts of an application with which all users directly interact, like the application's interface, and the parts serving more specialized purposes, like messages in log files. Good candidates for localization include:
After identifying each element of the application to be localized, application developers must assign a unique key to each element and provide a message catalog for each language to be supported. Each message catalog consists of keys and the corresponding language-specific strings. The key, therefore, is the link between the program and the message catalog; the program internally refers to localizable elements by key and uses the message catalog to generate the output seen by the user. Translated strings are generated by calling the format method on a LocalizableTextFormatter object, which represents a key and a resource bundle (a set of message catalogs). The locale setting of the program determines the message catalog in which to search for the key.
After identifying each element to be localized, message catalogs must be created for each language to be supported. These catalogs, which are implemented as Java resource bundles, can be created in two ways, either as subclasses of the ResourceBundle class or as Java properties files. Resource bundles have a variety of uses in Java; for message catalogs, the properties-file approach is more common. If properties files are used, support for languages to be added or removed without modifying the application code, and catalogs can be prepared by people without programming expertise.
A message catalog implemented in a properties file consists of a line for each key, where a key identifies a localizable element. Each line in the file has the following structure:
key = String corresponding to the key
For example, a grapical user interface for a banking system can have a pull-down menu to be used for selecting a type of account, like savings or checking. The label for the pull-down menu and the account types on the menu are good choices for localization. There are three elements that require keys: the label for the account menu and the two items on the menu. If the keys are accountString, savingsString, and checkingString, the English properties file associates each with an English string.
Figure 107. Three elements in an English message catalog
accountString = Accounts savingsString = Savings checkingString = Checking ... |
In the German properties files, each key is given a corresponding German value.
Figure 108. Three elements in a German message catalog
accountString = Konten savingsString = Sparkonto checkingString = Girokonto ... |
Properties files can be added for any other needed languages, as well.
To enable resolution to a specific properties file, Java specifies naming conventions for the properties files in a resource bundle: resourceBundleName_localeID.properties
Each file takes a fixed extension, .properties. The set of files making up the resource bundle is given a collective name; for a simple banking application, an obvious name, like BankingResources, suffices for the resource bundle. Each file is given the name of the resource bundle with a locale identifier; the specific value of the locale ID varies with the locale. These are used internally by the Java.util.ResourceBundle class to match files in a resource bundle to combinations of locale and time-zone settings. The details of the algorithm vary with the release of the JDK; see your Java documentation for information specific to your installation.
In the banking application, typical files in the BankingResources resource bundle include BankingResources_en.properties for the English message catalog and BankingResources_de.properties for the German catalog. Additionally, a default catalog, BankingResources.properties, is provided for use when the requested catalog cannot be found. The default catalog is often the English-language catalog.
Resource bundles containing message catalogs for use with localizable text need to be installed only on the systems where the formatting of strings is actually performed. The resource bundles are typically placed in an application's JAR file. See WebSphere support for more information.
The Java package com.ibm.websphere.i18n.localizabletext contains the classes and interfaces constituting the localizable-text package. This package makes extensive use of the internationalization and localization features of the Java language; programmers using the WebSphere localizable-text package must understand the underlying Java support, which are not documented in any detail here.
The WebSphere localizable-text package relies primarily on the following Java components:
This list is not exhaustive. WebSphere and these Java classes can also use related Java classes, but the related classes--for example, java.util.Calendar--are typically special-purposes classes. This section briefly describes only the primary classes.
A Locale object in Java encapsulates a language and a geographic region, for example, the java.util.Locale.US object contains locale information for the United States. An application that specifies a locale can then take advantage of the locale-sensitive formatters built into the Java language. These formatters, in the java.text package, handle the presentation of numbers, currency values, dates, and times.
A TimeZone object in Java encapsulates a representation of the time and provides methods for tasks like reporting the time and accommodating seasonal time shifts. Applications use the time zone to determine the local date and time.
A resource bundle is a named collection of resources--information used by the application, for example, strings, fonts, and images--used by a specific locale. The ResourceBundle class allows an application to retrieve the named resource bundle appropriate to the locale. Resource bundles are used to hold the messages catalogs, as described in Writing localizable programs. Resource bundles can be implemented in two ways, either as subclasses of the ResourceBundle class or as Java properties files.
The MessageFormat class can be used to construct strings based on parameters. As a simple example, suppose a localized application represents a particular error condition with a numeric key. When the application reports the error condition, it uses a message formatter to convert the numeric key into a meaningful string. The message formatter constructs the output string by looking up the code (the parameter) in an appropriate resource bundle and retrieving the corresponding string from the message catalog. Additional parameters--for example, another key representing the program module--can also be used in assembling the output message.
The WebSphere localizable-text package wraps the Java support and extends it for efficient and simple use in a distributed environment. The primary class used by application programmers is the LocalizableTextFormatter class. Objects of this class are created, typically in server programs, but clients can also create them. LocalizableTextFormatter objects are created for specific resource-bundle names and keys. Client programs that receive LocalizableTextFormatter objects call the object's format method. This method uses the locale of the client application to retrieve the appropriate resource bundle and assemble the locale-specific message based on the key.
For example, suppose that a WebSphere client-server application supports both French and English locales; the server is using an English locale and the client, a French locale. The server creates two resource bundles, one for English and one for French. When the client makes a request that triggers a message, the server creates a LocalizableTextFormatter object containing the name of the resource bundle and the key for the message, and passes the object back to the client.
When the client receives the LocalizableTextFormatter object, it calls the object's format method, which returns the message corresponding to the key from the French resource bundle. The format method retrieves the client's locale and, using the locale and name of the resource bundle, determines the resource bundle corresponding to the locale. (If the client has set an English locale, calling the format method results in the retrieval of an English message.) The formatting of the message is transparent to the client. In this simple client-server example, the resource bundles reside centrally with the server. The client machine does not have to install them. Part of what the WebSphere localizable-text package provides is the infrastructure to support centralized catalogs. WebSphere uses an enterprise bean, a stateless session bean provided with the localizable-text package, to access the message catalogs. When the client calls the format method on the LocalizableTextFormatter object, the following events occur internally:
The LocalizableTextFormatter class, found in the package com.ibm.websphere.i18n.localizabletext, is the primary programming interface for using the localizable-text package. Objects of this class contain the information needed to create language-specific strings from keys and resource bundles.
Applications written with the WebSphere localizable-text package can store message catalogs locally or remotely. In a distributed environment, the use of remote, centrally stored catalogs is appropriate. All applications can use the same catalogs, and administration and maintenance of the catalogs are simplified; each component does not need to store and maintain copies of the message catalogs. Local formatting is useful in test situations and appropriate under some circumstances. In order to support both local and remote formatting, a LocalizableTextFormatter object must indicate the name of the formatting application. For example, when an application formats a message by using remote, centrally stored catalogs, the message is actually formatted by a simple enterprise bean (see WebSphere support for more information). Although the localizable-text package contains the code to automate looking up the enterprise bean and issuing a call to it, the application needs to know the name of the formatting enterprise bean. Several methods in the LocalizableTextFormatter class use a value described as application name; this refers to the name of the formatting application, which is not necessarily the name of the application in which the value is set.
The LocalizableTextFomatter object can optionally cache formatted messages so that they do not have to be reformatted when needed again. By default, caching is not used, but the LocalizableTextFormatter.setCacheSetting method can be used to enable caching. When caching is enabled and the LocalizableTextFormatter.format method is called, the method determines whether the message has already been formatted. If so, the cached message is returned. If the message is not found in the cache, the message is formatted and returned to the caller, and a copy of the message is cached for future use.
If caching is disabled after messages have been cached, those messages remain in the cache until the cache is cleared by a call to the LocalizableTextFormatter.clearCache method. The cache can be cleared at any time. The cache within a LocalizableTextFormatter object is automatically cleared when any of the following methods are called on the object:
Under some circumstances, it can be impossible to format a message. The localizable-text package implements a fallback strategy, making it possible to get some information even if a message cannot be correctly formatted into the desired language. The LocalizableTextFomatter object can optionally store a fallback value for a message string, the time zone, and the locale. These can be ignored unless the LocalizableTextFormatter object throws an exception.
The localizable-text package provides native support for localization based on time zone and locale, but application developers can construct messages on the basis of other values as well. The localizable-text package provides an illustrative class, LocalizableTextDateTimeArgument, which reports the date and time. The date and time information is localized by using the locale and time-zone values, but the class also uses additional variables to determine how the output is presented. The date and time information can be requested in a variety of styles, from the fully detailed to the terse. In this example, the construction of message strings is driven by three variables: the locale, the time zone, and the style. Applications can use any number of variables in addition to locale and time zone for constructing messages. See Using optional arguments for more information.
To develop a WebSphere application that uses localizable text, application developers must do the following:
These tasks were described previously. See Identifying localizable text and Creating message catalogs for more information.
This section describes these tasks.
Server programs typically create LocalizableTextFormatter objects, which are sent to clients as the result of some operation; clients format the objects at the appropriate time. Less typically, clients can create LocalizableTextFormatter objects locally. To create a LocalizableTextFormatter object, applications use one of the constructors in the LocalizableTextFormatter class:
The LocalizableTextFormatter object must have values set for the name of the resource bundle, the key, the name of the formatting application, and for any optional values so the object can be formatted. The LocalizableTextFormatter object can be created and the values set in one step by using the constructor that takes the necessary arguments, or the object can be created and the values set in separate steps. Values are set by using methods on the LocalizableTextFormatter object; for setting the values manually, rather than by using a constructor, use these methods:
A LocalizableTextFormatter object also has methods that can be used to set values that cannot be set when the object is created, for example:
(See Fallback information for more information.)
Each of these set methods also has a corresponding get method for retrieving the value. The clearLocalizableTextFormatter method unsets all values, returning the LocalizableTextFormatter object to a blank state. After clearing the object, reuse the object by setting new values and calling the format method again.
Figure 109 creates a LocalizableTextFormatter object by using the default constructor and uses methods on the new object to set values for the key, name of the resource bundle, name of the formatting application, and fallback string on the object.
Figure 109. Code example: Creating a LocalizableTextFormatter object and setting values on it
import com.ibm.websphere.i18n.localizabletext.LocalizableException; import com.ibm.websphere.i18n.localizabletext.LocalizableTextFormatter; import java.util.Locale; public void drawAccountNumberGUI(String accountType) { ... LocalizableTextFormatter ltf = new LocalizableTextFormatter(); ltf.setPatternKey("accountNumber"); ltf.setResourceBundleName("BankingSample.BankingResources"); ltf.setApplicationName("BankingSample"); ltf.setFallBackString("Enter account number: "); ... } |
The application requesting a localized message can specify the locale and time zone for which the message is to be formatted, or the application can use the default values set for the JVM. For example, a graphical user interface can allow users to select the language in which to display the menus. A default value must be set, either in the environment or programmatically, so the menus can be generated when the application first starts, but users can then change the menu language to suit their needs. Figure 110 illustrates how to change the locale used by the application based on the selection of a menu item.
Figure 110. Code example: Setting the locale programmatically
import java.awt.event.ActionListener; import java.awt.event.ActionEvent; ... import java.util.Locale; public void actionPerformed(ActionEvent event) { String action = event.getActionCommand(); ... if (action.equals("en_us")) { applicationLocale = new Locale("en", "US"); ... } else if (action.equals("de_de")) { applicationLocale = new Locale("de", "DE"); ... } else if (action.equals("fr_fr")) { applicationLocale = new Locale("fr", "FR"); ... } ... } |
When an application calls a format method, it can specify no arguments, which causes the message to be formatted using the JVM's default values for locale and time zone, or a combination of locale and time zone can be specified to override the JVM's defaults. (See Generating the localized text for more information on the arguments to the format methods.)
After the LocalizableTextFormatter object has been created and the appropriate values set, the object can be formatted to generate the string appropriate to the locale and time zone. The format methods in the LocalizableTextFormatter class perform the work necessary to generate a string from a set of message keys and resource bundles, based on locale and time zone. The LocalizableTextFormatter class provides four format methods. Each format method returns the formatted message string. The methods take a combination of java.util.Locale and java.util.TimeZone objects and throw LocalizableException objects:
The format method with no arguments uses the locale and time-zone values set as defaults for the JVM. The other format methods can be used to override either or both of these values.
Figure 111 shows the creation of a localized string for the LocalizableTextFormatter object created in Figure 109; formatting is based on the locale set in Figure 110. If the formatting fails, the application retrieves and uses the fallback string instead of the localized string.
Figure 111. Code example: Formatting a LocalizableTextFormatter object
import com.ibm.websphere.i18n.localizabletext.LocalizableException; import com.ibm.websphere.i18n.localizabletext.LocalizableTextFormatter; import java.util.Locale; public void drawAccountNumberGUI(String accountType) { ... LocalizableTextFormatter ltf = new LocalizableTextFormatter(); ltf.setPatternKey("accountNumber"); ltf.setResourceBundleName("BankingSample.BankingResources"); ltf.setApplicationName("BankingSample"); ltf.setFallBackString("Enter account number: "); try { msg = new Label (ltf.format(this.applicationLocale) , Label.CENTER); } catch (LocalizableException le) { msg = new Label(ltf.getFallBackString(), Label.CENTER); } ... } |
Both of these approaches quickly become intractable if a variable can take many values or if a string has several variable components. Instead, the localizable text package supports substitution of variables in strings with optional arguments. A string in a message catalog uses integers in braces--for example, {0} or {1}--to represent variable components. Figure 112 shows an example from an English message catalog for a string with a single variable substitution. (The same key in message catalogs for other languages has a translation of this string with the variable in the appropriate location for the language.)
Figure 112. A message-catalog entry with a variable substring
successfulTransaction = The operation on account {0} was successful. |
The values that are substituted into the string come from an array of optional arguments. One of the constructors for LocalizableTextFormatter objects takes an array of objects as an argument, and such an array of objects can be set within any LocalizableTextFormatter object. The array is used to hold values for variable parts of a string. When a format method is called on the object, the array is passed to the format method, which takes an element of the array and substitutes it into a placeholder with the matching index in the string. The value at index 0 in the array replaces the {0} variable in the string, the value at index 1 replaces {1}, and so forth.
Figure 113 shows the creation of a single-element argument array and the creation and use of a LocalizableTextFormatter. The element in the argument array is the account number entered by the user. The LocalizableTextFormatter is created by using a constructor that takes the array of optional arguments; this can also be set directly by using the setArguments method on the LocalizableTextFormatter object. Later in the code, the application calls the format method. The format method automatically substitutes values from the array of arguments into the string returned from the appropriate message catalog.
Figure 113. Code example: Formatting a message with a variable substring
public void updateAccount(String transactionType) { ... Object[] arg = { new String(this.accountNumber)}; ... LocalizableTextFormatter successLTF = new LocalizableTextFormatter("BankingResources", "successfulTransaction", "BankingSample", arg); ... successLTF.format(this.applicationLocale); ... } |
Figure 114 shows a message string in an English catalog with two variables, one of which will be localized, and the keys for two possible values. (The second variable in the string, the account number, is simply a number that must be substituted into the string; it does not need to be localized.)
Figure 114. A message-catalog entry with two variable substrings
sucessfulTransaction = The {0} operation on account {1} was successful. depositOpString = deposit withdrawOpString = withdrawal |
To support localization of substrings, the localizable-text package allows the nesting of LocalizableTextFormatter objects. This is done simply by inserting a LocalizableTextFormatter object into the array of arguments for another LocalizableTextFormatter. When the format method does the variable substitution, it formats any LocalizableTextFormatter objects as it substitutes array elements for variables. This allows substrings to be formatted independently of the string in which they are embedded.
Figure 115 modifies the example in Figure 113 to format a message with a localizable substring. First, a LocalizableTextFormatter object for the localizable substring (referring to a deposit operation) is created. This object is inserted, along with the account-number information, into the array of arguments. The array of arguments is then used in constructing the LocalizableTextFormatter object for the complete string; when the format method is called, the embedded LocalizableTextFormatter object is formatted to replace the first variable, and the account number is substituted for the second variable.
Figure 115. Code example: Formatting a message with a localizable variable substring
public void updateAccount(String transactionType) { ... // Successful Deposit. LocalizableTextFormatter opLTF = new LocalizableTextFormatter("BankingResources, "depositOpString", "BankingSample"); Object[] args = {opLTF, new String(this.accountNumber)}; LocalizableTextFormatter successLTF = new LocalizableTextFormatter("BankingResources", "successfulTransaction", "BankingSample", args); ... successLTF.format(this.applicationLocale); ... } |
User-defined classes used as optional arguments provide application-specific format methods, which programmers can use to perform localization on the basis of any number of values, not just locale and time zone. These user-defined classes need to be available only on the systems where they are constructed and inserted into LocalizableTextFormatter objects and where the actual formatting is done; client applications do not need to install these classes.
The localizable-text package provides an example of such a user-defined class in the LocalizableTextDateTimeArgument class. This class allows date and time information to be selectively formatted according to the style values defined in the java.text.DateFormat class and according to the constants defined by the LocalizableTextDateTimeArgument class.
The DateFormat styles determine how information is reported about a date. For example, when the DateFormat.FULL style is chosen, the twenty-second day of February in 2000 is represented in English as Tuesday, February 22, 2000. When the DateFormat.SHORT style is used, the same date is represented as 2/22/00. The valid values are:
The LocalizableTextDateTimeArgument class defines constants that can be used to request only date or time information, or both, either in date-time order or in time-date order. The defined values are:
An object of a user-defined class like the LocalizableTextDateTimeArgument class can be set in the optional-argument array of a LocalizableTextFormatter object, and when the LocalizableTextFormatter object attempts to format the user-defined object, it calls the format method on that object. That format method, written by the application developer, can do whatever is appropriate with the application-specific values. In the case of the LocalizableTextDateTimeArgument class, the format method determines if date, time, or both are required, formats them according to the DateFormat value, and assembles them in the order requested in the LocalizableTextDateTimeArgument style. The date and time information are also affected by the locale and time-zone values, but the refinements in the formatting are accomplished by the DateFormat class and the user-defined values.
The string assembled from a user-defined class like the LocalizableTextDateTimeArgument class can then be substituted into a larger string, just as the return values of nested LocalizableTextFormatter objects can be. When writing such user-defined classes, it is helpful to think of them as specialized versions of the generic LocalizableTextFormatter class, and the way in which the LocalizableTextFormatter class is written provides a model for writing user-defined classes.
For example, the LocalizableTextDateTimeArgument class implements only the LocalizableTextLTZ interface, as shown in Figure 116.
Figure 116. Code example: The structure of the LocalizableTextDateTimeArgument class
package com.ibm.websphere.i18n.localizabletext; import java.util.Locale; import java.util.Date; import java.text.DateFormat; import java.util.TimeZone; import java.io.Serializable; public class LocalizableTextDateTimeArgument implements LocalizableTextLTZ, Serializable { ... } |
A user-defined class must contain a constructor and an implementation of the format methods as defined in the localizable-text interfaces that the class implements. It can also contain other methods as needed. The LocalizableTextDateTimeArgument class contains a constructor, a single format method, an equality method, a hash-code generator, and a string-conversion method.
Figure 117. Code example: The methods in the LocalizableTextDateTimeArgument class
... public class LocalizableTextDateTimeArgument implements LocalizableTextLTZ, Serializable { public final static int DATE = 1; public final static int TIME = 2; public final static int DATEANDTIME = 3; public final static int TIMEANDDATE = 4; private Date date = null; private dateTimeStyle = LocalizableTextDateTimeArgument.DATE; private int dateFormatStyle = DateFormat.FULL; ... public LocalizableTextDateTimeArgument(Date date, int dateTimeStyle, int dateFormatStyle) { ... } public boolean equals(Object param) { ... } public format (Locale locale, TimeZone timeZone) throws IllegalArgumentException { ... } public int hashCode() { ... } public String toString() { ... } } |
Each format method in the user-defined class can do whatever is appropriate for the application. In the LocalizableTextDateTimeArgument class, the format method (see Figure 118 for the implementation) examines the setting of the date-time style set within the object, for example, DATEANDTIME. It then assembles the requested information in the requested order, according to the date-format value.
Figure 118. Code example: The format method in the LocalizableTextDateTimeArgument class
public format (Locale locale, TimeZone timeZone) throws IllegalArgumentException { String returnString = null; switch(dateTimeStyle) { case LocalizableTextDateTimeArgument.DATE : { returnString = DateFormat.getDateInstance(dateFormatStyle, locale).format(date); break; } case LocalizableTextDateTimeArgument.TIME : { df = DateFormat.getTimeInstance(dateFormatStyle, locale); df.setTimeZone(timeZone); returnString = df.format(date); break; } case LocalizableTextDateTimeArgument.DATEANDTIME : { dateString = DateFormat.getDateInstance(dateFormatStyle, locale).format(date); df = DateFormat.getTimeInstance(dateFormatStyle, locale); df.setTimeZone(timeZone); timeString = df.format(date); returnString = dateString + " " + timeString; break; } case LocalizableTextDateTimeArgument.TIMEANDDATE : { dateString = DateFormat.getDateInstance(dateFormatStyle, locale).format(date); df = DateFormat.getTimeInstance(dateFormatStyle, locale); df.setTimeZone(timeZone); returnString = timeString + " " + dateString; break; } default : { throw new IllegalArgumentException(); } } return returnString; } |
An application can create a LocalizableTextDateTimeArgument object (or an object of any other user-defined class) and place it in the optional-argument array of a LocalizableTextFormatter object. When the LocalizableTextFormatter object reaches the user-defined object, it will attempt to format it by calling the object's format method. The returned string is then substituted for a variable as the LocalizableTextFormatter processes each element in the array of optional arguments.
The localizable-text package provides a command-line Java tool, LocalizableTextEJBDeploy, for deploying the localizable-text session bean, and the package provides all the code necessary to run the session bean. An administrator uses the tool to deploy and name the formatting bean. The name given to the bean must match the name specified in LocalizableTextFormatter objects as the name of the formatting application. The tool can also be used to remove deployed beans when they are no longer needed.
LocalizableTextEJBDeploy -a <appName> -h <hostName> -i <installationDir> -x <action> [ -s <serverName> ] [ -c <containerName> ]
The required arguments, which can be specified in any order, follow:
The optional arguments, which can also be specified in any order, follow:
Figure 119. Deploying a formatting enterprise bean
% java LocalizableTextEJBDeploy -a CheckingApplication -x create -h ResourcesHost1 -i /usr/WebSphere/AppServer C:\java LocalizableTextEJBDeploy -a CheckingApplication -x create -h ResourcesHost2-i C:\WebSphere\AppServer |
When the formatting bean is no longer needed, it can be deleted with the LocalizableTextEJBDeploy tool. Figure 120 shows the command for removing the formatting bean deployed in Figure 119 from one of the machines.
Figure 120. Deleting a deployed formatting enterprise bean
C:\java LocalizableTextEJBDeploy -a CheckingApplication -x delete -h ResourcesHost2-i C:\WebSphere\AppServer |