An analysis on the monthly increase of NetBeans IDE active users count

Few weeks ago I was talking to Jiří Kovalský about NetBeans and its 1,000,000 active users and I asked him to check whether it is possible to have the number of active users by month or not and today he got back to me with a nice chart showing number of active users for each month since Jan 2004. Jiri is  NetBeans community manager and also he is the first person in the NetBeans team I get acquaintance long ago during NetCat50.

Here I attached the chart and then I will do an short analysis of what the chart can represent.

Number of NetBeans Active users per month

Now a brief overview of the chart:

  • NetBeans team started to count the active users in Jan 04
  • During summer and new year eve there is a decline in the number of active users each year and as overall number of users growth, this decline can be seen more clearly.
  • Number of active users is increasing continuously.

Now I want to merge the above chart with another table which is different NetBeans versions release dates.

NetBeans versions release dates

  • The last line in the bar is for June 2011, The July numbers are not calculated yet.
  • It took NetBeans two years, until Jan 2006, to get 200,000 active users at the beginning but the growth in number of active users was accelerating from the beginning as chart suggests.
  • In its next 3 years, from Jan 2006 to Jan 2009 number of users increased by 400,000 to a total of 600,000 active users which means the user growth accelerated quite well. This is the post NetBeans 5 ear when each version’s  changelog had quite a large number  number of bug fixes, performance improvements and new features.
  • The biggest increase in the number of users in duration of one year can be seen between June 2010 and June 2011 with about 200,000 users. This is the second year that ORACLE was in charge of Sun and its products.
  • It looks like that after NetBeans 6.9 the number of active users is increasing faster than before and the reason is clearly the stability and performance improvement in addition to tons of new features in the core and support for PHP and C++.

As a long time user of NetBeans IDE I should say that NetBeans has come a long long way to become the IDE that we use in our daily jobs nowadays. The number of features introduced in the IDE and the number of bug fixes is enormous. You can find and try NetBeans 4 or 5 and compare it to NetBeans 7 to understand the huge distance between these two.

NetBeans seen several shifts in its direction specially during netbeans 6 time when More languages were being supported in IDE and diverse set of SOA and XML development features were being included in the IDE. Then Again another shift happened and all those features and language supported were dropped and NetBeans team put more effort into the core to make the core more stable and feature rich which as you can see in the chart has payed off pretty well.

The 1,000,000 active users number is not just a number, it shows that a vast, versatile and living community is behind NetBeans IDE as users, contributors, and the core development team. Long live the good community and the good IDE.

Aug 10th, 2011 | Filed under Java, Opinion, Oracle, Reporting

A walkthrough for the fork/join framework introduced in Java SE 7

Java SE 7 brought some neat features on the table for Java developers, one of these features is the fork/join framework or basically the new parallel programming framework we can use to easily to implement our divide and conquer solutions. The point is that a solution to a problem should be devised with the following characteristics to use the fork/join framework effectively:

  • The problem domain whether it is a file, list, etc to be processed or a computation should be dividable to smaller subtasks.
  • Processing chunks should be possible without requiring the result of other chunks.

To summarize it, solving or processing the problem domain should require no self-feedback to make it possible to use the framework. For example if you want to process a list and processing each element in the list require the result of processing previous element then it is impossible to use any parallel computing for doing that job. If you want to apply some FFT over a sound stream which require feedback for processing each pulse from the previous pulses it is not possible to speedup the processing using the fork/join framework, etc.

Well, before we start learning the fork/join framework we better know what it is and what it is not: What fork/join framework is:

  • A parallel programming framework for Java
  • Part of Java SE 7
  • Suitable for implementing parallel processing solutions, mostly data intensive with small or no shared resources between the workers who process the data chunks.
  • Suitable when no synchronization is required between the workers

What fork/join framework is not:

  • It is not a magic that turns your code to run fast on machines with multiple processors, you need to think and implement your solutions in a parallel manner.
  • It is not hard and obscure like other frameworks, MPI for example. Using the framework is way easier than anything I used before.

If you want to learn the mechanics behind the fork/join framework you can read the original article written by Doug Le which explains the motive and the design. The article is available at http://gee.cs.oswego.edu/dl/papers/fj.pdf. If you want to see how we can use the framework then continue on reading this article.

First let’s see what are the important classes that one need to know in order to implement a divide and conquer solution using fork/join framework and then we will start using those classes.

  • The ForkJoinPool: This is the workers pool where you can post your ForkJoinTask to be executed. The default parallelism level is the number of processors available to the runtime.
  • The RecursiveTask<V>: This is a task, subclass of the ForkJoinTask which can return some value of type V. For example processing a list of DTOs and returning the result of process.
  • The RecursiveAction: Another subclass of the ForkJoinTask without any return value, for example processing an array…

I looked at this new API mainly for data pipelining in which I need to process a pretty huge list of object and turn it to another format to keep the processing result of one library consumable for the next one in the data flow and I am happy with the result pretty easy and straight forward.

Following is an small sample showing how to process a list of Row objects and convert them a list of Entity Objects. In my case it was something similar with processing Row objects and turning them to OData OEntity objects.


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 *
 * @author Masoud Kalali <mkalali>
 */
class RowConverter extends RecursiveTask<List<Entity>> {

    //if more than 5000 we will use the parallel processing
    static final int SINGLE_TREAD_TOP = 5000;
    int begin;
    int end;
    List<Row> rows;

    public RowConverter(int begin, int end, List<Row> rows) {
        this.begin = begin;
        this.end = end;
        this.rows = rows;
    }

    @Override
    protected List<Entity> compute() {

        if (end - begin <= SINGLE_TREAD_TOP) {
            //actual processing happens here
            List<Entity> preparedEntities = new ArrayList<Entity>(end - begin);
            System.out.println("  beging: " + begin + " end: " + end);
            for (int i = begin; i < end; ++i) {
                preparedEntities.add(convertRow(rows.get(i)));
            }
            return preparedEntities;
        } else {
            //here we do the dividing the work and combining the results
            // specifies the number of chunks you want to break the data to
            int divider = 5000;
            // one can calculate the divider based on the list size and the number of processor available 
            // using the http://download.oracle.com/javase/7/docs/api/java/lang/Runtime.html#availableProcessors()
            // decrease the divider number and examine the changes.

            RowConverter curLeft = new RowConverter(begin, divider, rows);
            RowConverter curRight = new RowConverter(divider, end, rows);
            curLeft.fork();
            List<Entity> leftReslt = curRight.compute();
            List<Entity> rightRes = curLeft.join();
            leftReslt.addAll(rightRes);
            return leftReslt;
        }
    }

    //dummy converted method converting one DTO to another
    private Entity convertRow(Row row) {

        return new Entity(row.getId());
    }
}

// the driver class which own the pool 
public class Fjf {

    public static void main(String[] args) {

        List<Row> rawData = initDummyList(10000);
        ForkJoinPool pool = new ForkJoinPool();
        System.out.println("number of worker threads: " + pool.getParallelism());


        List<Entity> res = pool.invoke(new RowConverter(0, rawData.size(), rawData));

        // add a breakpoint here and examine the pool object. 
        //check how the stealCount, which shows number of subtasks taken on by available workers, 
        //changes when you use an smaller divider and thus produce more tasks
        System.out.println("processed list: " + res.size());

    }

    /**
     * creates a dummy list of rows
     * 
     * @param size number of rows int he list
     * @return the list of @see Row objects
     */
    private static List<Row> initDummyList(int size) {

        List<Row> rows = new ArrayList<Row>(size);

        for (int i = 0; i < size; i++) {
            rows.add(new Row(i));
        }
        return rows;
    }
}

//dummy classes which should be converted from one form to another
class Row {

    int id;

    public Row(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

class Entity {

    int id;

    public Entity(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

Just copy and paste the code into your IDE and try running and examining it to get deeper understanding of how the framework can be used. post any comment and possible questions that you may have here and I will try to help you own with them.

Aug 2nd, 2011 | Filed under How To, Java, Java SE, Learning

How REST interface covers for the absence of JMX/AMX administration and management interface in GlassFish 3.1

For sometime I wanted to write this entry and explain what happened to GlassFish JMX/AMX management and administration interface but being busy with other stuff prevented me from doing so. This article here can be an upgrade to my other article about GlassFish 3.0 JMX administration interface which I wrote while ago. Long story short, in GlassFish 3.1 the AMX/JMX is no longer available and instead we can use the REST interface to change the server settings and perform all administration/management and monitoring activities we need. There are fair number of articles and blog entries all over the web about the RESTful interface which I included them at the end of this blog. Firs of all the rest interface is available trough the administration console application meaning that we can access the interface using a URL similar to: http://localhost:4848/management/domain/ The administration console and the rest interface  are running on a separate virtual server and therefore a separate HTTP Listener and if required transport configuration. What I will explain here will be the following items:

  • How to use Apache HttpClient to perform administration tasks using GlassFish REST interface
  • How to find the request parameters for different commands.
  • GlassFish administration, authentication and transport security

How to use Apache HttpClient to interact with GlassFish administration and management application

Now back to the RESTful interface, this is a HTTP based interaction channel with the GlassFish administration infrastructure which basically allows us to do almost anything possible to do using asadmin trhough HTTP in a RESTful manner. We can use basically any programming language capable to writing on a socket to interact with the RESTFul interface. Here we will use Apache HTTPClient to take care of sending the commands to GlassFish RESTFul console. When using GlassFish REST management we can use any of the POST/GET and DELETE methods to perform the following tasks:

  • POST: create and partially update a resource
  • GET: get information like details of a connection pool
  • DELETE: to delete a resource

Following sample code shows how to use the  to perform some basic operations including updating a resource, getting some resources list, creating a resource and finally deleting it.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.logging.Logger;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;

/**
 *
 * @author Masoud Kalali
 */
public class AdminGlassFish {

    //change the ports to your own settng
    private static final String ADMINISTRATION_URL = "http://localhost:4848/management";
    private static final String MONITORING_URL = "http://localhost:4848/monitoring";
    private static final String CONTENT_TYPE_JSON = "application/json";
    private static final String CONTENT_TYPE_XML = "application/xml";
    private static final String ACCEPT_ALL = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
    private static final Logger LOG = Logger.getLogger(AdminGlassFish.class.getName());

    public static void main(String args[]) throws IOException, HttpException, URISyntaxException {

        //just chaning the indent level for the JSON and XML output to make them readable, for humans...
        String prettyFormatRestInterfaceOutput = "{"indentLevel":2}";
        String response = postInformation("/domain/configs/config/server-config/_set-rest-admin-config", prettyFormatRestInterfaceOutput);
        LOG.info(response);
        //getting list of all JDBC resources
        String jdbcResources = getInformation("/domain/resources/list-jdbc-resources");
        LOG.info(jdbcResources);

//        creating  a JDBC resource on top of the default pool
        String createJDBCResource = "{"id":"jdbc/Made-By-Rest","poolName":"DerbyPool"}";
        String resourceCreationResponse = postInformation("/domain/resources/jdbc-resource", createJDBCResource);
        LOG.info(resourceCreationResponse);

//        deleting a JDBC resource
        String deletionReponse = deleteResource("/domain/resources/jdbc-resource/jdbc%2FMade-By-Rest");
        LOG.info(deletionReponse);

    }

    //using HTTP get
    public static String getInformation(String resourcePath) throws IOException, AuthenticationException {
        DefaultHttpClient httpClient = new DefaultHttpClient();
        HttpGet httpG = new HttpGet(ADMINISTRATION_URL + resourcePath);
        httpG.setHeader("Accept", CONTENT_TYPE_XML);
        HttpResponse response = httpClient.execute(httpG);
        HttpEntity entity = response.getEntity();
        InputStream instream = entity.getContent();
        return isToString(instream);
    }

    //using HTTP post for creating and partially updating resources
    public static String postInformation(String resourcePath, String content) throws IOException {
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost(ADMINISTRATION_URL + resourcePath);
        StringEntity entity = new StringEntity(content);

        //setting the content type
        entity.setContentType(new BasicHeader(HTTP.CONTENT_TYPE, CONTENT_TYPE_JSON));
        httpPost.addHeader("Accept",ACCEPT_ALL);
        httpPost.setEntity(entity);
        HttpResponse response = httpClient.execute(httpPost);

        return response.toString();
    }

    //using HTTP delete to delete a resource
    public static String deleteResource(String resourcePath) throws IOException {
        HttpClient httpClient = new DefaultHttpClient();
        HttpDelete httpDelete = new HttpDelete(ADMINISTRATION_URL + resourcePath);
        httpDelete.addHeader("Accept",
                ACCEPT_ALL);
        HttpResponse response = httpClient.execute(httpDelete);
        return response.toString();

    }

//converting the get output stream to something printable
    private static String isToString(InputStream in) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(in), 1024);
        for (String line = br.readLine(); line != null; line = br.readLine()) {
            sb.append(line);
        }
        in.close();
        return sb.toString();
    }
}

You may ask how could one know what are the required attributes names, there are several ways to do it:

  • Look at the reference documents…
  • View the source code of the html page representing that kind of resource, for example for the JDBC resource it is like http://localhost:4848/management/domain/resources/jdbc-resource which if you open it in the browser you will see an html page and viewing its source will give you the names of different attributes. I think the label for the attributes is also the same as the attributes themselves.

  • Submit the above page and monitor the request using your browser plugin, for example in case of chrome and for the JDBC resource it is like the following picture

GlassFish administration, authentication and transport security

By default when we install GlassFish or we create a domain the domain administration console is not protected by authentication nor it is protected by HTTPS so whatever we send to the application server from through this channel will be readable by someone sniffing around. Therefore you may need to enable authentication using the following command:

./asadmin change-admin-password

You may ask now that we enabled the authenticaiton for the admin console, how we can use it by our sample code, the answer is quite simple,  Just set the credentials for the request object and you are done. Something like:

UsernamePasswordCredentials cred = new UsernamePasswordCredentials("admin", "admin");
httpget.addHeader(new BasicScheme().authenticate(cred, httpget));

Make sure that you are using correct username and passwords as well as correct request object. In this case the request object is httpGet

Now about the HTTPs to have have encryption during the transport we need to enable the SSL for the admin HTTP listener. following steps show how to use the RESTFul interface through a browser to enable HTTPS for the admin console. When using the browser, GlassFish admin console shows basic HML forms for different resources.

  1. Open the http://localhost:4848/management/domain/configs/config/server-config/network-config/protocols/protocol/admin-listener in the browser
  2. Select true for security-enabled option element
  3. Click Update to save the settings.
  4. Restart server using http://localhost:4848/management/domain/restart

The above steps have the same effect as

./asadmin enable-secure-admin

This will enable the SSL layer for the admin-listener but it will use the default, self singed certificate. Here I explained how to install a GoDaddy digital certificate into GlassFish application server to be sure that none can listen during the transport of command parameters and on the other hand the certificate is valid instead of being self signed. And here I explained how one can use the EJBCA to setup and use an small inter-corporate certificate authority with GlassFish, though the manual is a little old but it will give you enough understanding to use the newer version of EJBCA.

If you are asking about how our small sample application can work with this dummy self signed certificate of GlassFish you need to wait till next time that I will explain how to bypass the invalid certificate installed on our test server.

In the next part of this series I will cover more details on the monitoring part as well as discussing  how to bypass the self signed Digital certificate during the development…

Updating Web application’s Spring Context from Beans definitions partially stored in the database…

As you know spring security providers can get complex as  you may need several beans like, implementations of UserDetailsService, SaltSource, PasswordEncoder, the JDBC setup and so on. I was working on an spring based application which needed to load the security configuration from the database because the system administrator was able to select the security configuration from several pre-defined templates like LDAP, JDBC, File Based, etc. change some attributes like LDAP address or file location, etc. to fit the template in the environment and then apply the new configuration to be used by the application.

I was to port some parts of the application to the web and 3 tier architecture and so I had to have the authentication configured for the web application from the database and current implementations of the required beans for the security providers configurations.

It is plain and simple, load all of the context configuration by adding them to the web.xml and let the spring filter use them to initialize the context or extend XmlWebApplicationContext or its siblings and return the configuration file addresses by overriding the getConfigLocations method. This works perfectly when everything is in plain XML file and you have access to everything… It wont work when some of context configuration files are stored in the database and the only means of accessing the database is the spring context and that needs to be initialized before you could access the database through it.

What I needed to do was putting together a basic authentication in front of the web application while using the ProviderManager which its configuration is stored in the database. Without the ProviderManager you cannot have the security filters and thus no security will be applied over the context.

The first part, creating the security configuration and specifying the URL patterns which are needed to be protected is straight forward. The filters use the ProviderManager which is not there and thus the context initialization will fail. To solve this I used the following workaround which might help someone else as well. In all of our templates the ProviderManager bean name was the same so I could simply devise the following solution. Create a temporary basic security provider definition file with the following beans:

  • A basic UserDetailsService bean based on InMemoryDaoImpl
  • An AuthenticationProvider on top of the above UserDetailsService
  • A ProviderManager which uses the above AuthenticationProvider.

The complete file look like this:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
    default-lazy-init="true">

    <bean id="tmpUserDetailsService"
          class="org.springframework.security.core.userdetails.memory.InMemoryDaoImpl">
        <property name="userMap">
            <value>
            </value>
        </property>
    </bean>

    <bean id="tmpAuthenticationProvider"
          class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
        <property name="userDetailsService" ref="tmpUserDetailsService"/>
    </bean>

    <bean id="authenticationManager"
        class="org.springframework.security.authentication.ProviderManager">
        <property name="providers">
            <list>
                <ref local="tmpAuthenticationProvider"/>
            </list>
        </property>
    </bean>
</beans>

Using this file, the spring context will get initialized and thus no NoSuchBeanDefinitionException will be thrown at your face for the least.  You may say, ok why you are not loading the entire security definitions and configurations after the context is initialized so you wont need to have the temporary security provider, the answer to this question is that having no security applied right after the context initialization is finished is a security risk because at the brief moment before the context get updated with the security definitions, people can access the entire system without any authentication or access control. Let’s say that brief moment is negliable but a bigger factor here is the possible failure of loading the definitions after the context is initialized means that the application will remain without any security restriction if we do not lock down the application with the temporary provider.

Now that spring context can get initialized you can hook into spring context initialization by a listener and load the security provider from the database into the context to override the temporary beans with the actual one stored in the database.

Too hook into spring context initialization process you need to follow the below steps:

  • Implement your ApplicationListener, following snippet shows how:
  • public class SpringContextEventListener implements ApplicationListener {
    
        private XmlWebApplicationContext context;
    
        public void onApplicationEvent(ApplicationEvent e) {
    
            if (e instanceof ContextRefreshedEvent) {
                context = (XmlWebApplicationContext) ((ContextRefreshedEvent) e).getApplicationContext();
                loadSecurityConfigForServer();
            }
        }
    
        private void loadSecurityConfigForServer() {
    
            AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
            BeanDefinitionRegistry registry = (BeanDefinitionRegistry) factory;
            String securityConfig = loadSecurityConfigFromDatabase();
            XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(registry);
            xmlReader.loadBeanDefinitions(new ByteArrayResource(securityConfig.getBytes()));
        }
    
        private String loadSecurityConfigFromDatabase() {
            //use the context and load the configuration from the database
        }
    }
    }
    
  • Now that you have a listener which listen for ApplicationContext event and load your security configuration  you can hook this listener to you context because by its own it wont do anything :-)
  • To add the listener to your application context, just add something similar to the following snippet to one of the context configuration files and you will be done.

    <bean id="applicationListener" class="your.package.SpringContextEventListener"/>
    

    This is all you needed to do in order to get the context updated after web application starts without imposing any security hole on the application for lack of security definitions right after the application startup.

    GlassFish inspired wallpapers for your desktops…

    Well, I saw Markus did some nice wallpapers for GlassFish and I tried to mimic him and do some wallpapers. I should say that he is the man of many talents and my wallpapers are, well, screaming far behind his. The main background, the texture, is coming from  ImageAbstraction and are available under  (CC BY-NC 2.0). The GlassFish logo is available at GlassFish.org

    The images themselves are included in the following album in 3 sizes, 1440*900, 1680*1050 and 1600*1200.

    Jul 13th, 2011 | Filed under GlassFish, Java

    Proposed features and functionalities of JavaTM Message Service 2.0, JSR-343

    This is the second entry on the series of articles I will write on JMS 2.0. You can find the first entry here. In this article I will iterate over the suggested features which might be included in the JMS 2.0 based on the final decisions by the expert group.

    I classified the features into some meaningful categoriz which are as follow:

    The API ease of use

    The first set that I am going to cover is around the ease of use of the API which despite being elegant is a little hard to start with specially for new developers whom look for the asynchronous communication solutions in Java EE platform. The features and changes proposed for this part are as follow:

    • As usual the backward compatibility is a trend which the EG is going to adhere. Any application working with JMS 1.1 will continue to work with JMS 2.
    • The API wont get harder to use despite the additional features which are going to be included in the next version.
    • Using JMS from Java SE will be possible with JMS 2.0 as it is with JMS 1.1
    • API is going to get simpler by using more annotations and integration of the JMS API with the CDI
    • Using the MDBs is  going to get simpler and MDBs is going to see a revamp as did other EJB types in previous versions of Java EE.
    • The API should make less use of unchecked exceptions

    Changes interesting to vendors

    These are a set of proposed changes to make it easier for the JMS vendors and application server vendors to implement the JMS interface to their JMS brokers as well as integrating the broker into different application servers.

    • An standard interface will be defined to make it possible for any any compliant application server to integrate with any of compliant JMS implementation. It is more likely to happen through the JCA adapter and making it mandatory.
    • “Clarify how the JMS provider should interact with Transaction Managers. In particular, the responsibilities of the JMS Provider, the TM and the App Server during recovery need to be clearly defined.”
    • Clarifying the relation between JMS provider and other Java EE services and APIs specially the transaction manager. It will be more about documentation rather than implementations.

    Other enhancements and changes

    • Defining the transport security at the API level for sake of more portability.
    • Message compression for faster transport…
    • Timed messages which will be delivered by the provider to the consumer on schedule specified for the message.
    • Asynchronous send with call back enabled acknowledge in order not to block the client until server sends back the acknowledge.
    • supporting message consumption from the same session by multiple threads
    • Deprecate JMS 1.0 Queue and Topic specific interfaces in favor of JMS 1.1 unified interfaces
    • Ability to send objects directly, without need to wrap in a javax.jms.Message
    • Standardise the “connection string” to define server IPs, timeouts, etc, as in ActiveMQ, OpenMQ, etc.
    • Standardizing the interaction between Clint and the provider in clustered environment to make the applications more portable from one farm of JMS providers to another.

    I am member of the JMS 2.0 expert group but this post or any other post in my personal blog does not reflect the expert group opinion or the opinion of my employer on the subject unless you could not see this paragraph at the end of the post :-) . The EG is not going to be held responsible to provide any of these features in the JMS 2.0 as the spec is still open and nothing is finalised. It is just to give you a overview of what is being discussed in the EG. To find more information about the JMS 2.0, take a look at the JCP page for the spec at  http://www.jcp.org/en/jsr/detail?id=343 or its homepage at Java.net at http://java.net/projects/jms-spec/pages/Home

    Jul 11th, 2011 | Filed under Java