Skip to main content

GlassFish Modularity System, How extend GlassFish CLI and Web Administration Console (Part 2: Developing Sample Modules)

Posted by kalali on March 29, 2010 at 1:52 AM PDT

Extending GlassFish CLI and Administration Console, Developing the sample Modules

Administrators are always looking for a more effective, easier to use, and less time consuming tool to use as the interface sitting between them and what they supposed to administrate and manage. GlassFish provide effective, easy to access and easy to simple to navigate in administration channels which cover all day to day tasks that administrators need to perform. But when it comes to administration, administrators usually write their own shell scripts to automate some tasks; they use CRON or other schedulers to schedule automatic tasks, and so on to achieve their own customized administration flow.

By using GlassFish console, it is simply possible to use shell scripts, or CRON to further extends the administration and facilitate the daily tasks. But sometimes it is far better to have a more sophisticated and mature way to extend and customize the administration interfaces. GlassFish provides all necessary interfaces and required services to allow administrators or developers to develop new modules for GlassFish administration interfaces to add a new feature or enhance already available capabilities.

GlassFish administration channels extendibility is not only for easing the administrator tasks to customize the administration interfaces, but also it is present to let container developers develop administration interfaces for their containers fully integrated with already available interfaces and fully compatible with the present administration console look and feels. Using administration console extendibility developers can develop new administration console commands very easily by utilizing already available services and dependency injection which provide them with all available environmental objects that they need.

This article covers how we can develop new commands for the CLI console and how we can develop new modules to add some new pages and navigation nodes to web administration console.

 

 

1 CLI console extendability

GlassFish CLI can be considered the easiest to accesses administration console for experienced administrators as they are better the keyboard and like automated and scripted tasks instead of clicking the mouse in a web page to see the result.  Not only GlassFish CLI is very command and feature rich, but also its design is based on the same modularity concepts that the whole application server is based on. 

CLI modularity is very helpful for those who wants to extends GlassFish by providing new set of commands for their container, deploying new monitoring probes and providing the corresponding CLI commands for accessing the monitoring information,  developing new commands which may be required by administrators and are not already in place, and so on.

1.1 Prepare your development environment

This is a hands on article which involve us with developing some new modules for GlassFish application server and deploying the modules to GlassFish application server, therefore we should be able to compile the modules which usually uses HK2 services in the inside and are bundled as OSGI bundle for the deployment.

Although we can use an IDE like NetBeans to build the sample codes but we are going to use a more IDE agnostic way like using Apache Maven to ensure that we can simply manage the dependency hurdle and make the sample codes easily imported to IDE of your choice. Get Maven from http://maven.apache.org/download.html and install it according to the installation instruction provided in the same page. Then you are ready to further dig into developing modules.

 

1.2 CLI modularity and extendibility 

We discussed about how GlassFish utilize OSGI and HK2 for providing a modular architecture, one place which this modular architecture shows itself is CLI modularity. We know that all extendibility point in GlassFish are based on contracts and contracts providers which in simple though there are interfaces and interface implementation along with some annotation to mark and further configure the contract implementation.

We are talking about adding new commands to CLI dynamically by placing an OSGI bundle in the module directory of GlassFish, either the module just contains CLI commands or it contains CLI commands, web based administration console pages, and a container implementation. So, at first there is no predefined list of known commands and commands loads on-demand and as soon as you try to use one of them. For example when you issue ./asadmin list-commands CLI framework will check for all provider which implements the admin command contract and shows the list by extracting the information from each provider (implementation). CLI commands can use dependency injection provided by HK2 and also each command may extract some information and provide them to other components which might be interested in using them.

Like all providers in the GlassFish modularity system, we should annotated each command implementation using @Service annotation to ensure that HK2 will treat the implementation as a service for lifecycle management and service locating. CLI commands like any other HK2 service should have a scope which determines how the lifecycle of the command is managed by HK2.  Usually we should ensure that each command is just live in the context of its execution and not beyond that context. Therefore we annotate each command with a correct scope like PerLookup.class scope which result in initiating a command in each lookup.

All that we need to know for developing CLI commands is summarized in understanding some interfaces and helper classes.  Other required knowledge is around the AMX and HK2.

Each CLI command must implements a contract which is an interface named org.jvnet.hk2.annotations.Contract.AdminCommand which is shown in listing 1.

Listing 1 The AdminCommand interface which any CLI command have to implement @Contract                                           #1 public interface AdminCommand {           public void execute(AdminCommandContext context);            #2 }  

The AdminCommand interface has one method which is called by asadmin utility when we call the associated command. At #1 we define the interface as a contact which HK2 service providers can implements. At #2 we have the execute method which accept a argument of type org.glassfish.api.admin.AdminCommandContext which is shown in listing 2. This class is a set of utilities which allows developer to have access to some contextual variables like reporting, logging, and command parameters which the last one will be deprecated.

Listing 2 The AdminCommandContext class which is a bridge between inside the command and outside the command execution context public class AdminCommandContext implements ExecutionContext {         public  ActionReport report;     public final Properties params;     public final Logger logger;     private List<File> uploadedFiles;         public AdminCommandContext(Logger logger, ActionReport report, Properties params) {         this(logger, report, params, null);     }         public AdminCommandContext(Logger logger, ActionReport report, Properties params,             List<File> uploadedFiles) {         this.logger = logger;         this.report = report;         this.params = params;         this.uploadedFiles = (uploadedFiles == null) ? emptyFileList() : uploadedFiles;     }         private static List<File> emptyFileList() {         return Collections.emptyList();     }       public ActionReport getActionReport() {         return report;     }       public void setActionReport(ActionReport newReport) {         report = newReport;     }         public Properties getCommandParameters() {         return params;     }       public Logger getLogger() {         return logger;     }         public List<File> getUploadedFiles() {         return uploadedFiles;     } }    

The only unfamiliar class is the org.glassfish.api.ActionReport abstract class which has several sub classes that allow us as CLI command developers to report the result of our command execution to the original caller. Several sub classes are defined to make it possible to report back the result of the execution in different environment like returning an HTML, JSON or plain text report about the execution. The report may include exit code, possible exceptions, failure or the success, and so on based on the reporter type. Important sub classes of the ActionReport are listed in table 1

Table 1 all available reporters which can be used to report the result of a command execution

Reporter class

Description

HTMLActionReporter

Generate an HTML document containing report information

JsonActionReporter

A JSON document containing the report information

PlainTextActionReporter

A plain text report which provides necessary report information as plain text

PropsFileActionReporter

A properties file containing all report elements

SilentActionReport

No report at all

XMLActionReporter

A machine readable XML document containing all report elements

 

All of these reporters except the SilentActionReport which is placed inside org.glassfish.embed.impl are placed in the com.sun.enterprise.v3.common package.

Now we need a way to pass some parameters to our command, determine which parameters are necessary and which one are optional, which one need argument and what should be the type of the argument. There is an annotation which let us annotate properties of the AdminCommand implementation class to mark them as parameters. The annotation which we should use to mark a property as a parameter is @Param and is placed in org.glassfish.api package.

Listing 3 The Parameters annotation which we can use to have a parameterized CLI command @Retention(RUNTIME) @Target({METHOD,FIELD}) public @interface Param {     public String name() default "";     public String acceptableValues() default "";     public boolean optional() default false;     public String shortName() default "";     public boolean primary() default false; } 

As you can see, the annotation can be place either on a setter method or on a filed. Annotation has several elements including the parameter name which by default is the same as the property name, a list of comma separated acceptable values, the short name for the parameter, whether the parameter is optional or not and finally whether the parameter name is required to be included or not.  In each command only one parameter can be primary.

When we mark a property or a setter method with @Param annotation, CLI framework will try to initialize the parameter with the given value and then it will call the execute method. During the initialization, CLI framework check different attributes of a parameter like its necessity, short name, its acceptable value and so on.

The last thing which we should think about a CLI command is its internationalization and the possibility to show the localized version of text messages about the command and parameters to the client. This requirement is covered using another annotation named @I18n, the associated interface is placed in org.glassfish.api. Listing 3 shows the I18n interface declaration.

Listing 4 I18n Interface which we should use to provide internationalized messages for our CLI command @Retention(RUNTIME) @Target({TYPE,METHOD,FIELD}) public @interface I18n {     public String value(); }  

The annotation can be used both for properties and for annotating methods. At runtime the CLI framework will extract and inject the localized value into the variable based on the current locale of the JVM.

The localization file which is a standard resource bundle should follow the same key=value format which we used for any properties file. The standard name for the file is LocalStrings_Language_Country.properties an example file can be LocalStrings_en_US.properties for United States English or LocalStrings.properties as the default bundle for any locale which has no corresponding locale file in the bundle.

1.3 A CLI command which returns the operating system details

First we will develop a very simple CLI command in this section to get more familiar with the abstracts that we discussed before and then we will develop a more sophisticated command which will teach us more details about CLI framework and capability.

First command that we are going to develop just shows us what is operating system which our application server is running on and it will shows some information about the OS like architecture, JVM version, JVM vendor and so on. Our command name is get-platform-details.

First let’s look at the maven build file and analyze its content, although I am not going to scrutinize on the Maven build file but we will look at elements related to our CLI command development.

Listing 5 Maven build file for creating a get-platform-details CLI command <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">     <modelVersion>4.0.0</modelVersion>                       #1     <parent>         <groupId>org.glassfish.pluggability</groupId>         <artifactId>pluggability</artifactId>         <version>3.0-SNAPSHOT</version>     </parent>     <artifactId>glassfish.book.chapter platformDetailsCommand </artifactId>     <packaging>hk2-jar</packaging>                             #2     <name>platform-details-command</name>     <description>GlassFish in Action, Chapter 13, Platform Details Command </description>     <dependencies>          <dependency>             <groupId>org.glassfish.admin</groupId>             <artifactId>cli-framework</artifactId>             <version>${project.parent.version}</version>         </dependency>         <dependency>             <groupId>org.glassfish.common</groupId>             <artifactId>common-util</artifactId>             <version>${project.parent.version}</version>         </dependency>     </dependencies>         <build>         <resources>             <resource>                 <directory>src/main/resources</directory>         #3                 <includes>                     <include>**/*.1</include>                 </includes>             </resource>         </resources>     </build> </project> 

You will find more details in the POM file included in the sample source code accompanying the book, but what is shown in the listing is enough to build a CLI command module.  At #1 we define the OSGI Manifest version. at #2 we are ensuring that the bundle which we are creating is built using the HK2 module specification which means inclusion of all OSGI related description like imported and exported packages in the MANIFEST.MF file which will be generated by Maven and resides inside the META-INF folder of the final JAR file. At #3 we ensure that Maven will include our help file for the command. The help file name should be similar to the command name and it can include anything textual.  We usually follows a UNIX like man files structure to create help files for the commands. The help file will be shown when we call ./asadmin get-platform-details --help. Figure 1 shows the directory layout of our sample command project.  As you can see we have two similar directory layout for the manual file and java source codes and the POM.XML file is in the root of the cli-platform-details-command directory.

Figure 1 Directory layout of the get-platform-details command project.

 

Now let’s see what the source code for the command is and what would be the outcome of the build process. Listing 6 shows the source code for the command itself which when we execute ./asadmin get-platform-details GlassFish CLI will try to call its execute method. The result after executing the command is similar to figure 2, as you can see our command has executed successfully.

Figure 2 Sample output for calling get-platform-details with runtime as the parameter

Now you can develop your own command and test it to see a similar result in the terminal window.

Listing 6 Source code for PlatformDetails.java which is a CLI command @Service(name = "get-platform-details")               #1 @Scoped(PerLookup.class)                                  #2 public class PlatformDetails implements AdminCommand {           #3       ActionReport report;     OperatingSystemMXBean osmb = ManagementFactory.getOperatingSystemMXBean();                  #4     RuntimeMXBean rtmb = ManagementFactory.getRuntimeMXBean();     #5     // this value can be either runtime or os for our demo     @Param(name = "detailsset", shortName = "DS", primary = true, optional = false)                                                        #6     String detailsSet;       public void execute(AdminCommandContext context) {         try {               report = context.getActionReport();             StringBuffer reportBuf;             if (detailsSet.equalsIgnoreCase("os")) {                 reportBuf = new StringBuffer("OS details: \n");                 reportBuf.append("OS Name: " + osmb.getName() + "\n");                 reportBuf.append("OS Version: " + osmb.getVersion() + "\n");                 reportBuf.append("OS Architecture: " + osmb.getArch() + "\n");                 reportBuf.append("Available Processor: " + osmb.getAvailableProcessors() + "\n");                 reportBuf.append("Average Load: " + osmb.getSystemLoadAverage() + "\n");                 report.setMessage(reportBuf.toString());            #7                 report.setActionExitCode(ExitCode.SUCCESS);         #8               } else if (detailsSet.equalsIgnoreCase("runtime")) {                   reportBuf = new StringBuffer("Runtime details: \n");                 reportBuf.append("Virtual Machine Name: " + rtmb.getVmName() + "\n");                 reportBuf.append("VM Vendor: " + rtmb.getVmVendor() + "\n");                 reportBuf.append("VM Version: " + rtmb.getVmVersion() + "\n");                 reportBuf.append("VM Start Time: " + rtmb.getStartTime() + "\n");                 reportBuf.append("UpTime: " + rtmb.getUptime() + "\n");                 report.setMessage(reportBuf.toString());                 report.setActionExitCode(ExitCode.SUCCESS);               } else {                 report.setActionExitCode(ExitCode.FAILURE);         #9                 report.setMessage("Th given value for " +                         "detailsset parameter is not acceptable, the value " +                         "can be either 'runtime' or 'os'");             }             } catch (Exception ex) {               report.setActionExitCode(ExitCode.FAILURE);             report.setMessage("Command failed with the following error:\n" + ex.getMessage());                                context.getLogger().log(Level.SEVERE, "get-platform-details " + detailsSet + " failed", ex);                     }       } }   

At #1 we are marking the implementation as a provider which implements a contract. The provided service name is get-platform-details which are the same as our CLI command name. At #2 we are setting a correct scope for the command as we do not like to see the same result every time that we initiate the command either for different domains. At #3 we are implementing the contract interface. At #4 and #5 we get MBeans which provides the information that we need, this MBeans can be either our local server MBeans or the remote running instance if we use the --host and --port  to execute the command against a remote instance. At #6 we are defining a parameter which is not optional, the parameter name is detailsset which we will use it by --detailsset when we want to execute the command. The short name for the parameter is DS which we will use as –DS when we want to execute the command. The command is primary so we do not need to name the parameter, we can just pass the value and CLI framework will assign the value to this parameter. Pay attention to the parameter name and the containing variable. If we do not use name element of the @Param annotation the parameter name will be the same as the variable name. At #7 we set the output which we want to show to the user as the report object’s message. At #8 we set the exit condition to successful as we managed to execute the command successfully. At #9 we set the exit code as a failure as the value passed for the parameter is not recognizable, we show a suitable message to user to help him diagnose the problem. At #10 we faced an exception, so we log the exception and set the exit command as a failure.

GlassFish CLI commands fall under two broad categories, one is the local commands like create-domain or start-domain commands which execute locally and you can not pass remote connection parameters including --host, --port parameters and expect to see the command executed on a remote glassfish installation. These commands extend com.sun.enterprise.cli.framework.Command abstract class or one of subclasses like com.sun.enterprise.admin.cli.BaseLifeCycleCommand.  And do not need anything like an already running GlassFish instance.

Next command which we will study is another command which will list some details about the JMS service of target domain, but this time we will discuss resource injection and some details around localization of command messages.

Listing 7 shows source code for a command which shows some information about domain JMS service including starting arguments, JMS service type, address list behavior and so on. Although the source code shows how we can get some details but setting the values for each attribute is the same as getting them.

Figure 3 shows the result of executing this command on a domain created with default parameters.

Listing 7 Source code for remote command named get-jms-details @Service(name = "get-jms-details") @Scoped(PerLookup.class) @I18n("get-jms-details")                       #1 public class JMSDetails implements AdminCommand {    @Inject                                      #2    JmsService jmsService;    @Inject    JmsHost jmsHost;    @Inject    JmsAvailability jmsAvailability;    ActionReport report;    @Param(name = "detailsset", acceptableValues = "all,service,host,availability",    shortName = "DS", primary = true, optional = false) #3    @I18n("get-jms-details.details_set")                #4    String detailsSet;    final private static LocalStringManagerImpl localStrings = new LocalStringManagerImpl(JMSDetails.class);             #5    public void execute(AdminCommandContext context) {        try {            report = context.getActionReport();            StringBuffer reportBuf = new StringBuffer("");            if (detailsSet.equalsIgnoreCase("all") || detailsSet.equalsIgnoreCase("service")) {                reportBuf.append("Default Host: " + jmsService.getDefaultJmsHost() + "\n");                reportBuf.append("MQ Service: " + jmsService.getMqService() + "\n");                reportBuf.append("Reconnection Attempts: " + jmsService.getReconnectAttempts() + "\n");                reportBuf.append("Startup Arguments: " + jmsService.getStartArgs() + "\n");            } else if (detailsSet.equalsIgnoreCase("all") || detailsSet.equalsIgnoreCase("host")) {                reportBuf.append("Host Address: " + jmsHost.getHost() + "\n");                reportBuf.append("Port Number: " + jmsHost.getPort() + "\n");            } else if (detailsSet.equalsIgnoreCase("all") || detailsSet.equalsIgnoreCase("availability")) {                reportBuf.append("Is Availability Enabled: " + jmsAvailability.getAvailabilityEnabled() + "\n");                reportBuf.append("Availability Storage Pool Name: " + jmsAvailability.getMqStorePoolName() + "\n");            }            report.setMessage(reportBuf.toString());            report.setActionExitCode(ExitCode.SUCCESS);        } catch (Exception ex) {            report.setActionExitCode(ExitCode.FAILURE);            report.setMessage(localStrings.getLocalString("get-jms-details.failed", "Command failed to execute with the {0} as given parameter", detailsSet) + ". The exception message is:" + ex.getMessage());  #6            context.getLogger().log(Level.SEVERE, "get-jms-details " + detailsSet + " failed", ex);        }    } }   

You are right the code looks similar to the first sample but more complex in using some non familiar classes and use of resource injection and localization stuff. We used resource injection in order to access GlassFish configurations. All configuration interfaces which we can obtain using injection are inside com.sun.enterprise.config.serverbeans. Now let’s analyze the code where it is important and somehow unfamiliar.  At #1 we are telling the framework that this command short help message key in the localization file is get-jms-details. At #2 we are injecting some configuration Beans into our variables. At #3 we ensure that the CLI framework will check the given value for the parameter to see whether it is one of the acceptable values or not. At #4 we are determining the localization key for the parameter help message. At #5 we initialize the string localization manager to get appreciated string values from the bundle files. At #6 we are showing a localized error message using the string localization manager.

Listing 5 shows LocalStrings.properties file content which we will shortly discuss where it should be placed to allows the CLI framework to find it and load the necessary messages from it.

Listing 8 An Snippet of localization file content for a command named restart-domain

get-jms-details=Getting the Detailed information about JMS service and host in the target domain.                #1

get-jms-details.details_set=The set of details which is required, the value can be all, service, host, or availability.           #2

get-jms-details.failed=Command failed to execute with the {0} as given parameter.          #3

 

 

At #1 we include a description of the command; at #2 we include description of the detailsset parameter using the assigned localization key. At #3 we include the localized version of the error message which we want to show when the command fails. As you can see we used place holders to include the parameter value. Figure 4 shows the directory layout of the get-jms-details project. As you can see we placed the LocalStrings.properties file next to the command source code.

 

Figure 4 Directory layout of the get-platform-details command project.

Developing more complex CLI commands follow the same rule as developing these simple commands. All that you need to develop a new command is starting a new project using the given maven POM files and discovering the new things that you can do.

 

 

2 Administration console pluggability

You know that administration console a very easy to use channel for administrating a single instance or multiple clusters with tens of instances and variety of managed resources. All of these functionalities were based on navigation tree, tabs, and content pages which we could use to perform our required tasks. You may wonder how we can extend these functionalities without changing the main web application which is known as admingui to get new functionalities in the same look and feel that already available functionalities are presented.  The answer lies again in the application server overall design, HK2 and OSGI along with some JSF Templating and use of Woodstock JSF component suite. We are going to write an administration console which let us upload and install new OSGI bundles by using browser from remote locations.

2.1 Administration console architecture

Before we start discussing how we can extend the administration console in a non-intrusive way we should learn more about the console architecture and how it really works. Administration console is a set of OSGI bundles and each bundle includes some administration console specific artifacts in addition to the default OSGI manifest file and Maven related artifacts.

One of the most basic artifacts which are required by an administration console plugin is a descriptor which defines where our administration console navigation items should appear. For example are going to add a new node to the navigation tree, or we want to add a new tab to a currently available tab set like Application Server page tab set.  The file which describes this navigation items and their places is named console-config.xml and should be placed inside META-INF/admingui folder of the final OSGI bundle.

The second thing which is required to make it possible for our module to be recognized by the HK2 kernel is the HK2 administration console plugin’s contract implementation. So, the second basic item in the administration console plug-in is an HK2 service provider which is nothing more than a typical Java class, annotated with @Service annotation that implements the org.glassfish.api.admingui.ConsoleProvider interface. The console provider interface has only one method named getConfiguration. We need to implement this method if we want to use a non-standard place for the console-config.xml file.

In addition to the basic requirement we have some other artifacts which should be present in order to see some effects in the administration console from our plug-in side. There requirements includes, JSF fragment files to add the node, tab, content, and common task button to administration console on places which are described in the console-config.xml file.  These JSF fragments uses JSFTemplating project tags to define nodes, tabs, and buttons which should appear in the integration points.

Until now we include just a node in a navigation tree, a tab in one of the tab sets, or a common task button or group in the common tasks page. What else we need to include? We need some pages to show when administrators clicked on a tree node or on a common task button. So we should add JSF fragments implemented using JSFTemplating tags to show the real content to the administrators. For example imagine that we want to write a module to deploy OSGI bundles, we will need to show a page which let users select a file along with a button which they can press to upload and install the bundle. 

Now that we shown the page which let users select a file, we should have some business logic or handlers which receives the uploaded file and write it in the correct place. As we use JSFTemplating for developing pages, we will JSFTemplating handlers to handle the business logic.

Our functionality related artifacts are mentioned, but everything is not summarized in functionalities, we should provide localized administration pages to our administrators whom like to use their local languages when dealing with administration related tasks. We also need to provide some help files which will guide the administrators when they need to read a manual before digging into the action.

After we have all of this content and configurations in place, the administration console’s add on service query for all providers which implements the ConsoleProvider interface, then the service tries to get the configuration file if it couldn’t find the file in its default place by calling the getConfiguration method of the contract implementation. After that the administration console framework uses the configuration file and provided integration points template files and later on the content template files and handlers.

2.2 JSFTemplating

 

JSFTemplating is a sun sponsored project hosted in https://jsftemplating.dev.java.net/ which provides templating for JSF. By utilizing JSFTemplating we can create web pages or components using template files, template files. Inside template files we can use JSFTemplating, and Facelets syntax, other syntax support may be provided in future. All syntaxes support all of JSFTemplating's features such as accessing page session, event triggering and event handler’s support and dynamic reloading of page content. Let’s analyze a template file which is shown in listing 9 to see what we are going to use when we develop administration console plug-ins.

Listing 9 A simple JSFTemplating template file <!initPage     setResourceBundle(key="i18n" bundle="mypackage.resource"); />                                 #1 <sun:page>     <sun:html>         <sun:head id="head" />         <sun:body> #include /header.inc             <sun:form id="form">                 "<p>#{i18n["welcome.msg"]}</p>        #2                 <sun:label value="#{anOut}">        #3                     <!beforeEncode                         GiA.getResponse(userInput="#{in}"                   response=>$pageSession{anOut});                      />                 </sun:label>                 "<br /><br />                 <sun:textField id="in" value="#{pageSession.in}" /> #4                 "<br /><br />                 <sun:button text="$resource{i18n.button.text}" />                 <sun:hyperlink text="cheat">                     <!command                         setPageSessionAttribute(key="in" value="sds");  #5                     />                 </sun:hyperlink>             </sun:form>    #include /footer.inc          #6         </sun:body>     </sun:html> </sun:page>     

At #1 we determine the resource file which we want to use to get localized text content.  We define a key for accessing the object. At #2 we are simply using our localization resource file to show a welcome message and as you have already noticed it is Facelet syntax. At #3 we are using a handler for one of the predefined events of JSFTemplating events. In the event we are sending out the value of in variable to our method and after getting back the method’s execution result we set the result into a variable named anOut in the page scope. And we use the same variable to initialize the Label component. You can see how a handler will look like in listing 10.  The event that we used makes it possible to change the text before it gets displayed. There are several pre-defined events which some of them are listed in table 2. At #4 we are using the in variable’s value to initialize the text field content. At #5 we are using a pre-defined command as the handler for the hyperlink click. At #6 we are including another template file into this template file. All of the components that we used in this template file are equivalent of WoodStock project’s components.

 

Table 2 JSFTemplating pre-defined events which can call a handler

Event

Description

AfterCreate, BeforeCreate

Event handling commences before or after the component is created

AfterEncode, BeforeEncode

Event handling commences before or after content of a component get displayed

Command

Command has invoked, for example a link or a button is clicked

InitPage

Page initialization phase, when components values are getting sets

Listing 10 The GiA.getResponse handler which has been used in listing 9     @Handler(id = "GiA.getResponse",     input = {         @HandlerInput(name = "in", type = String.class)},     output = {         @HandlerOutput(name = "response", type = String.class)             })             public static void getResponse(HandlerContext handlerCtx) {     }  

In listing 10 you can see that the handler id were using in JSF page instead of the method fully qualified name. During the compilation, all handlers id, their fully qualified name, and their input and output parameters will get extracted by apt into a file named Handler.map which resides inside a folder named jsftemplating. The jsftemplating folder is placed inside the META-INF folder. Handler.map structure is similar to standard properties file containing variable=value pairs.

Now that we have some knowledge about the JSFTemplating project and the administration console architecture we can create our plug-in which let us install any GlassFish module in form of OSGI bundle using administration console. After we saw how a real plug-in can be developed we can proceed to learn more details about integration points and changing administration console theme and brand.

13.2.3                       OSGI bundle installer plug-in

Now we want to create a plug-in which will let us upload an OSGI module and install it into the application server by copying the file into modules directory. First let’s see figure 5 which shows the file and directory layout of the plug-in source codes, and then we can discuss each artifact in details.

In figure 5, at the top level we have out Maven build file with one source directory containing standard Maven directory structure for Jar files which is a main directory with java and resources directory inside it. The java directory is self describing as it contains the handler and service provider implementation.  The resources directory again is standard for Maven Jar packager. Maven will copy the content of the resources folder directly in the root of the Jar file. Content of the resources folder is as follow:

     glassfish folder: this folder contains a directory layout similar to java folder, the only file stored in this folder is our localization resource file which is named Strings.properties

  • images: graphic file which we want to use in our plug-in for example in the navigation tree
  • js: a JavaScript file which contains some functions which we will use in the plug-in
  • META-INF: during the development time we will have only one folder named admingui inside the META-INF folder, this folder holds the console-config.xml. After bulding the project some other folders including maven, jsftemplating and inhabitants will be created by the maven.
  • pages: this folder contains all of our JSF template files.

Figure 5 directory layout of the administration console plug-in to install OSGI bundles

Some folders are mandatory like META-INF but we can put other folders’ content directly inside the resources folder. But we will end up with an unorganized structure. Now we can discuss each artifact in details. You can see content of the console-config.xml in listing 11.

<?xml version="1.0" encoding="UTF-8"?>  <console-config id="GiA-OSGI-Module-Install">                  #1     <integration-point  id="CommonTask-Install-OSGI"          #2 type="org.glassfish.admingui:commonTask"          #3 parentId="deployment"                           #4 priority="300"                                   #5 content="pages/CommonTask.jsf" />             #6     <integration-point  id="Applications-OSGI" type="org.glassfish.admingui:treeNode"       #7 priority="500" parentId="applications"                       #8 content="pages/TreeNode.jsf" />                #9 </console-config>          

content: The content for the integration point, typically a JavaServer Faces page.

   

This XML file describe which integration points we want to use, and what is the priority our navigation node in comparison with already existing navigation nodes. At #1 we give our plugin a unique ID because later on we will access our web pages and resources using this ID. At #2 we define an integration point again with a unique ID. At #3 we determine that this integration point is a common task which we want to add under deployment tasks group #4. At #5 the priority number says that all integration points with an smaller priority number will be proceed before this integration point and therefore our common tasks button will be placed after them. At #6 we determine which template file should be proceed to fill in the integration point place holder, in this integration point the template file contains a button which will fill the place holder.

At #7 we determine that we want to add some tree node to the navigation tree by using org.glassfish.admingui:treeNode as integration point type. At #8 we determine that our tree node should be placed under the applications node. At #9 we are telling that the template page which will fill the place holder is pages/TreeNode.jsf. To summarize we can say generally each console-config.xml file consists of several integration points’ description elements and each integration point element has 4 or 5 attributes. These attributes are as follow:

     Id: An identifier for the integration point.

     parentId:  The ID of the integration point's parent. You can see A list of all parentid(s) in listing 3

     type: The type of the integration point. You can see A list of all parentid(s) in listing 3

     priority:  A numeric value that specifies the relative ordering of integration points for add-on components that specify the same parentId . This attribute is optional.

The second basic artifact which we discussed is the service provider which is simple Java class implementing the org.glassfish.api.admingui.ConsoleProvider interface. The listing 12 shows InstallOSGIBundle bundle which is the service provider for our plug-in.

Listing 12 Content of the InstallOSGIBundle.java, the plug-in service provider @Service(name = "install-OSGI-bundle") #A @Scoped(PerLookup.class)  #b public class InstallOSGIBundle implements ConsoleProvider {  #c       public URL getConfiguration() {         return null;     } }

#A: Service has name

#B: Service is scoped

#C: Service implements the interface

After we saw the basic artifacts, including the service provider implementation and how we can define where in the administration console navigational system we want to put our navigational items, we can see what the content of template files which we use in the console-config.xml file is. Listing 13 shows the TreeNode.jsf file content.

 

Listing 13 Content of the TreeNode.jsf template file which create a node in navigation tree

setResourceBundle(key="GiA" bundle="glassfish.book.chapteradmingui.plugin.Strings") #1

<!initPage setResourceBundle(key="GiA" bundle="glassfish.book.chapteradmingui.plugin.Strings") #1 />     <sun:treeNode id="GFiA_TreeNode"            #2               imageURL="resource/images/icon.png"  #3               text="$resource{GiA.OSGI.install.tree.title}" #4               url="GiA-OSGI-Module-Install/pages/installOSGI.jsf" #5               target="main"         #6               expanded="$boolean{false}">       #7               </sun:treeNode>

You can see the changes that this template will cause in figure 6, but the description of the code is as follow. At #1 we are using a JSFTemplating event to initialize the resource bundle. At #2 we are telling that we have a tree node with the GFiA_TreeNode as its ID which let us access the node using JavaScript. At #3 we determine the icon which we want to appear next to the tree node, you can see that we are using resource/images/icon.png as the path to the icon, the resource prefix lead us to the root of the resources folder. At #4 we are telling that we want the title of the tree node to be fetched from the resource bundle. At #5 we are determining which page should be loaded when administrator clicked the button. We are using facesContext.externalContext.requestContextPath we are getting the web application path, the GiA-OSGI-Module-Install is our plug-in id and we can access whatever we have inside the resources folder by prefixing its path with GiA-OSGI-Module-Install, you can see GiA-OSGI-Module-Install in listing 11. At #6 we are telling that the page should opens in the main frame (the content frame) and at #7 we are telling that the node should not be expanded by default. #7 effects are visible when we have some sub nodes.

Figure 6 the effects of TreeNode.jsf template file on the administration console navigation tree node

You can see that we have our own icon which is loaded directly from the images folder which is resided inside the resources directory. The tree node text is fetched from the resource bundle file.

Listing 14 shows CommonTask.jsf which causes the administration console service to place a button in the common task section under the deployment group, the result of this fragment on the common tasks page is shown in figure 7.

Listing 14 Content of the CommonTask.jsf file which is a template to fill the navigation item place <!initPage setResourceBundle(key="GiA" bundle="glassfish.book.chapteradmingui.plugin.Strings")         /> <sun:commonTask                                             #1          text="$resource{GiA.OSGI.install.task.title}"     toolTip="$resource{GiA.OSGI.install.tree.Tooltip}"     onClick="admingui.nav.selectTreeNodeById('form:tree:application:GFiA_TreeNode');                                                       #2     parent.location='#{facesContext.externalContext.requestContextPath}/GiA-OSGI-Module-Install/pages/installOSGI.jsf'; return false;"     #3   > </sun:commonTask>  

At #1 we are using a commonTask component of the JSFTemplating framework, we use a localized string for its text and tool tip. At #2 we are changing the state of the tree node defined in the console-config.xml when the component receives a click; this is for ensuring that the navigation tree shows where the use is. And at #4 we are telling that we want to load pages/installOSGI.jsf which this button is clicked.

Figure 7 Effects of the listing 13 on the common task page of the administration console

We fetched the button title and its tool tip from the resource file which we included in the resource folder as described in the sample project folder layout in figure 5.

Next file which we will discuss is the actual operation file which provides the administrators with an interface to select upload and install an OSGI bundle. The file as we already discussed is named installOSGI.jsf and listing 15 shows its content.

Listing 15 The installOSGI.jsf file content, this file provide interface for uploading the OSGI file <sun:page id="install-osgi-bundle-page" >  #1     <!beforeCreate     setResourceBundle(key="GiA" bundle="glassfish.book.chapteradmingui.plugin.Strings");     />     <sun:html>         <sun:head id="propertyhead" title="$resource{GiA.OSGI.install.header.title}">      #2               <sun:script url="/resource/js/glassfishUtils.js" /> #3         </sun:head>         <sun:body>             <sun:form id="OSGI-Install-form" enctype="multipart/form-data">                 <sun:title id="title" title="$resource{GiA.OSGI.install.form.title}" helpText="$resource{GiA.OSGI.install.form.help.title}">                     <sun:upload id="fileupload" style="margin-left: 17pt" columns="$int{50}"                                 uploadedFile="#{requestScope.uploadedFile}"                                 > #4                     </sun:upload>                     <sun:button id="uploadButton" text="$resource{GiA.OSGI.install.form.Install_button}"                                 onClick="javascript:                                 return submitAndDisable(this, '$resource{GiA.OSGI.install.form.cancel_processing}');                                 "> #5                         <!command                         GiA.uploadFile(file="#{uploadedFile}");                           redirect(page="#{request.contextPath}/GiA-OSGI-Module-Install/pages/OSGIModuleInstalled.jsf");                         />  #6                     </sun:button>                     </sun:button>                 </sun:title>             </sun:form>         </sun:body>     </sun:html> </sun:page>

I agree that the file content may look scary, but if you look more carefully you can see many familiar elements and patterns. Figure 8 shows this page in the administration console. In the listing 14, at #1 we are starting a page component and we use its beforeCreate event to load the localized resource bundle. At #2 we are giving the page a title which is loaded from the resource bundle. At #3 we are load a JavaScript file which contains one helper JavaScript method. As you can see we prefixed the file path with resource. At #4 we are using a fileUpload component and the binary content of the uploaded files goes to uploadedFile in the request scope. At #5 we use the helper JavaScript method to submit the form and disable the Install button. At #6 we are calling a custom handler with GiA.uploadFile as its ID. We pass the request scoped uploadedFile variable to the handler method. After the command executed, we #6 we redirect to a simple page which shows a message indicating that the module is installed.

Figure 8 The installOSGI.jsf page in the administration console

Now, let’s see what is behind this page, how this handler works, how it can find the module directory and how the installation process commences. Listing 16 shows the InstallOSGIBundleHandlers class which contains only one handler for writing the uploaded file in the modules directory of GlassFish installation which owns the running domain.

 

Listing 16 The content of InstallOSGIBundleHandlers which is file uploading handler public class InstallOSGIBundleHandlers {  #A       @Handler(id = "GiA.uploadFile",  #1     input = {         @HandlerInput(name = "file", type = UploadedFile.class)}) #2     public static void uploadFileToTempDir(HandlerContext handlerCtx) { #3         try {             UploadedFile uploadedFile = (UploadedFile) handlerCtx.getInputValue("file"); #4             String name = uploadedFile.getOriginalName();             String asInstallPath = AMXRoot.getInstance().getDomainRoot().getInstallDir(); #5             String modulesDir = asInstallPath + "/modules";             String serverSidePath = modulesDir + "/" + name;             uploadedFile.write(new File(serverSidePath));         } catch (Exception ex) {             GuiUtil.handleException(handlerCtx, ex); #6         }     } }

#A Class Declaration

As you can see in the listing, it is really simple to write handlers and working with GlassFish APIs. at #1 we are defining a handler, the handler ID element allows us to use it in the template file. At #2 we define the handler input parameter named file the parameter type is com.sun.webui.jsf.model.UploadedFile. At #3 we are implementing the handler method. At #4 we are extracting the file content from the context. At #5 we are using and AMX utility class to find the GlassFish installation path which owns the currently running domain. There many utility classes in the org.glassfish.admingui.common.util package. At #6 we are handling any possible exception using the GlassFish way. We can define output parameters for our handler and after the command executed, decide where to redirect the user based on the value of the output parameter.

There are 3 other files which we will briefly discuss, the first file which is shown in listing 17 is the JavaScript file which I used in installOSGI.jsf and is placed inside the js directory. We discussed the file in listing 15 discussions.

Listing 17 Content of the glassfishUtils.js file function submitAndDisable(button, msg, target) {     button.className='Btn1Dis';     button.disabled=true;     button.form.action += "?" + button.name + "=" + encodeURI(button.value);     button.value=msg;     if (target) {               button.form.target = target;     }     button.form.submit();     return true; }

The function receives three parameters, the button which it will disable, the message which it will set as the button title when the button disabled and finally the target that the button’s owner form will submit to.

Listing 17 shows the next file which we should take a look at.  String.properties is our localization file located deep in resources directory under  glassfish.book.chapteradmingui.plugin package. Although it is a standard localization file, but it is not bad to take a look and remember the first time that we faced with resource bundle files.

Listing 18 Content of the Strings.properties file OSGI.install.header.title=Install OSGI bundle by uploading the file into the server. OSGI.install.form.title=Install OSGI bundle by uploading the file into the server. OSGI.install.form.help.title=Browse and select your OSGI bundle to install it in the server's modules directory OSGI.install.form.button_processing=Processing OSGI.install.form.Install_button=Install OSGI.install.tree.title=Install OSGI Bundle OSGI.install.task.title=Install OSGI Bundle OSGI.install.tree.Tooltip=Simply install OSGI bundle by uploading the bundle from the administration console.

 

And finally our Maven build file, the build file as we discussed in the beginning of the section is placed in the top level directory of our project. If you want to review the directory layout, take a look at figure 5.

Listing 19 The plug-in project Maven build file, pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">     <modelVersion>4.0.0</modelVersion>     <parent>         <groupId>org.glassfish.admingui</groupId> #A         <artifactId>admingui</artifactId>         <version>3.0-SNAPSHOT</version>     </parent>     <artifactId>glassfish.book.chapterAdminConsolePlugin</artifactId>    <packaging>hk2-jar</packaging>           #B     <name>OSGI-bundle-Installation-Adming-Plugin</name>     <description>GlassFish in Action, Chapter 13, Admin console plugin to install OSGI bundle</description>     <dependencies>         <dependency>             <groupId>org.glassfish.common</groupId>             <artifactId>glassfish-api</artifactId>             <version>${project.version}</version>            </dependency>         <dependency>             <groupId>org.glassfish.admingui</groupId>             <artifactId>console-common</artifactId>             <version>${project.version}</version>            </dependency>         <dependency>             <groupId>org.glassfish.admingui</groupId>             <artifactId>console-plugin-service</artifactId>             <version>${project.version}</version>            </dependency>         <dependency>             <groupId>org.glassfish.common</groupId>             <artifactId>amx-api</artifactId>             <version>${project.version}</version>            </dependency>         <dependency>             <groupId>org.glassfish.deployment</groupId>             <artifactId>deployment-client</artifactId>             <version>${project.version}</version>            </dependency>     </dependencies>     </project>

#A Project Group ID

#B HK2 Special Packagin

The pom.xml file which is available for download in www.manning.com/kalali is more complete with some comment which explains other elements, but the 19 listings content is sufficient for building the plug-in.

Now that you have a solid understanding of how an administration console plug-in works, we can extends our discussion to other integration points and their corresponding JSFTemplating components.

Table 3 shows integration points and the related JSFTemplating component along with some explanation about each item. As you can see we have 5 more types of integration points with different attributes and navigational places.

 

Table 3 List of all integration points along with the related JSFTemplating compoent

Integration point*

Values for ParentID attribute

Corresponding JSFTemplating compoent

treeNode

tree, applicationServer

applications, webApplications

resources, configuration

node,  webContainer

httpService

 

sun:treeNode which

is equivalent of the Project Woodstock tag  webuijsf:treeNode

serverInstTab

serverInstTab, add tabs and sub tabs to Application Server page

sun:tab which is equivalent of the project Woodstock tag webuijsf:tab

commonTask

deployment, monitoring,updatecenter, documentation

sun:commonTask which

is equivalent of the Project Woodstock tag  webuijsf:commonTask.

commonTask

commonTasksSection, it allows us to create our own common tasks group

sun:commonTasksGroup .

which

is equivalent of the Project Woodstock tag  webuijsf:commonTasksGroup

customtheme

masthead, loginimage, loginform, versioninfo

This integration point let us change the login form, header and version information of the application server for changing the application server brand

* All of the first column values have org.glassfish.admingui: as prefix

 

 

Now you are quipped with enough knowledge to develop extensions for GlassFish administration console to facilitate your won daily tasks or provide your plug-ins to other administrators which are interested in same administration tasks which you are performing using your developed plug-ins.

3 Summary

Understanding an administration console from end user point of view is something and understanding it from a developer’s perspective is another, using the first one you can administrate the application server and using the second one you can manage, enhance and administrate the administration console itself. In this article we learned how we can develop new asadmin or CLI commands to ease our daily tasks or extends the GlassFish application server as a 3rd party partner. We learned how we can develop and extend the web based administration console which leads us to reviewing the JSFTemplating project and a fast pointing to AMX helper classes.