Saturday, December 31, 2011

JAX-RS Tip of the Day: Using YUI2 and YUI3 DataTables with Jersey and jQuery

This is a great example (if I do say so myself) that demonstrates some really cool capabilities of Jersey combined with Yahoo User Interface versions 2 and 3 combined with jQuery to demonstrate interoperability of multiple technologies.

The example code that I have included uses GlassFish 3.1.1 with Jersey 1.11 and was developed using NetBeans 7.1 RC2. The data is from the default sample database included in NetBeans. Here is the code: jsonp-database.zip

The YUI2, YUI3, and jQuery libraries are provided by Yahoo and Google CDN. This is a further example of a truly RESTful application where the application is provided as a mashup of multiple web resources.

Yahoo User Interface (YUI) is an incredibly well developed UI framework for the web. The DataSource implementation is easy to use and is extremely adaptable. It is also very stable, and Yahoo eats its own dogfood like Google so it is battle tested. YUI3 is the next generation code, and the DataTable implementation includes a number of new features.

jQuery is an extremely popular Javascript framework with great AJAX functionality. I have included two examples (YUI2/YUI3) in which jQuery provides JSON data to the Yahoo DataSource.

Jersey provides the heavy lifting. In all of the examples I provide the data in a variety of formats, but I focus on providing both JSON and JSONP. JSONP allows us to use have cross-domain capabilities. The code also demonstrates how to use Jettison to encapsulate POJOs. The best part is that the data can be tested not only using your browser, but can also be tested using curl. This provides a very simple visual means of verifying data.

So what does it look like... look at the images below. The results speak for themselves.

YUI2 DataTable using JSP

YUI3 DataTable with JSONP and JAX-RS (Jersey)

Now for the most important part, how do you get here.

Technical Details

The CustomerFacadeREST class is the most interesting. The JSP and HTML pages have the details of how to use the various YUI DataSources along with jQuery. I will not print the pages out, and suggest that you simply download the code and examine it.

CustomerFacadeREST.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/*
 * Copyright 2011 Blue Lotus Software, LLC.
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example.jaxrs.resources;
 
import com.bluelotussoftware.example.jaxrs.model.Customer;
import com.sun.jersey.api.json.JSONWithPadding;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.*;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
@Stateless
@Path("customers")
public class CustomerFacadeREST extends AbstractFacade<customer> {
 
    @PersistenceContext(unitName = "demoPU")
    private EntityManager em;
 
    public CustomerFacadeREST() {
        super(Customer.class);
    }
 
    @POST
    @Override
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public void create(Customer entity) {
        super.create(entity);
    }
 
    @PUT
    @Override
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public void edit(Customer entity) {
        super.edit(entity);
    }
 
    @DELETE
    @Path("{id}")
    public void remove(@PathParam("id") Integer id) {
        super.remove(super.find(id));
    }
 
    @GET
    @Path("json/{id}")
    @Produces({MediaType.APPLICATION_JSON})
    public JSONObject findJSONObject(@PathParam("id") Integer id) {
        Customer c = super.find(id);
        JSONObject jSONObject = new JSONObject();
        try {
            jSONObject.put("customerId", c.getCustomerId());
            jSONObject.put("name", c.getName());
            jSONObject.put("addressLine1", c.getAddressline1());
            jSONObject.put("addressLine2", c.getAddressline2());
            jSONObject.put("city", c.getCity());
            jSONObject.put("state", c.getState());
            jSONObject.put("phone", c.getPhone());
            jSONObject.put("fax", c.getFax());
            jSONObject.put("email", c.getEmail());
            jSONObject.put("creditLimit", c.getCreditLimit());
        } catch (JSONException ex) {
            throw new WebApplicationException(ex, Response.Status.INTERNAL_SERVER_ERROR);
        }
        return jSONObject;
    }
 
    @GET
    @Path("{id}")
    @Produces({"application/javascript", "application/ecmascript", "application/x-ecmascript",
        "application/x-javascript"})
    public JSONWithPadding find(@PathParam("id") Integer id,
            @QueryParam(value = "callback") String callback) {
        return new JSONWithPadding(super.find(id), callback);
    }
 
    @GET
    @Produces({"application/javascript", "application/ecmascript", "application/x-ecmascript",
        "application/x-javascript"})
    public JSONWithPadding getCustomers(@QueryParam("callback") String callback) {
        return new JSONWithPadding(new GenericEntity<List<customer>>(super.findAll()) {
        }, callback);
    }
 
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("json")
    public List<customer> getCustomersJSON() {
        return super.findAll();
    }
 
    @GET
    @Produces({"application/javascript", "application/ecmascript", "application/x-ecmascript",
        "application/x-javascript"})
    @Path("alternate")
    public JSONWithPadding getCustomersAlternate1(@QueryParam("callback") String callback) {
        return new JSONWithPadding(super.findAll(), callback);
    }
 
    //FIXME This returns an unexpected result of toString() on the Customer objects.
    @GET
    @Produces({"application/javascript", "application/ecmascript", "application/x-ecmascript",
        "application/x-javascript"})
    @Path("alternate2")
    public JSONWithPadding getCustomersAlternate2(@QueryParam(value = "callback") String callback) {
        JSONArray jSONArray = new JSONArray(super.findAll());
        return new JSONWithPadding(jSONArray, callback);
    }
 
    @GET
    @Path("{from}/{to}")
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public List<customer> findRange(@PathParam("from") Integer from, @PathParam("to") Integer to) {
        return super.findRange(new int[]{from, to});
    }
 
    @GET
    @Path("count")
    @Produces("text/plain")
    public String countREST() {
        return String.valueOf(super.count());
    }
 
    @java.lang.Override
    protected EntityManager getEntityManager() {
        return em;
    }
}

CODE


Here is the code: jsonp-database.zip

JAX-RS Tip of the Day: Changing JSON-JAXB Default Mapping

JSON-JAXB marshalling and un-marshalling by default uses a MAPPED configuration by default. However this can be changed by creating a @Provider which can change the default JSONConfiguration.

The example below changes the configuration to use BadgerFish which provides a better representation of an XML document. This representation does not provide the visual clarity that JSON generally provides, and is not generally human readable beyond basic notation. However, as noted that is not the point.

JSONJAXBContextResolver.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
 * Copyright 2011 Blue Lotus Software, LLC.
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example.jaxrs.providers;
 
import com.bluelotussoftware.example.jaxrs.model.Customer;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.api.json.JSONJAXBContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
@Provider
public class JSONJAXBContextResolver implements ContextResolver<JAXBContext> {
 
    private final JAXBContext context;
    private final Set<Class> types;
    private final Class[] cs = {Customer.class};
 
    public JSONJAXBContextResolver() throws JAXBException {
        this.types = new HashSet(Arrays.asList(cs));
        //Change the JSONConfiguration below to chnage the mapping type.
        this.context = new JSONJAXBContext(JSONConfiguration.badgerFish().build(), cs);
    }
 
    @Override
    public JAXBContext getContext(Class<?> type) {
        return (types.contains(type)) ? context : null;
    }
}
This class specifically handles the Customer object, and maps it to use BadgerFish.
1
curl -X GET -H "Accept: application/json" http://localhost:8080/jsonp-database/resources/customers/json/1
Output:
1
{"customerId":1,"name":"Jumbo Eagle Corp","addressLine1":"111 E. Las Olivas Blvd","addressLine2":"Suite 51","city":"Fort Lauderdale","state":"FL","phone":"305-555-0188","fax":"305-555-0189","email":"jumboeagle@example.com","creditLimit":100000}

Friday, December 30, 2011

JAX-RS Tip of the Day: Automatic Mapping of Jettison JSON Objects

One of the more interesting features in JAX-RS (Jersey) is the ability to automatically "wrap" POJOs. This combined with automatic wrapping of JAXB annotated beans, and using Jackson to create JSON objects is a powerful mix. However, sometimes we want to wrap an object, and add some additional metadata.

This tip is a very simple tip which allows you to create Jettison JSONObject, and JSONArray objects to create new objects, "wrap" existing objects, or add additional metadata. I mention "wrap" the objects, please don't confuse this with JSONP.

To enable automatic POJO mapping, and Jettison objects add the following to the web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<servlet>
    <servlet-name>ServletAdaptor</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
        <description>
            Allow automatic POJO Mapping features.
        </description>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <description>
            Set the packages to point to resources which you want to allow
            mapping of Jettison based objects. Separate the values for multiple
            packages with a comma (,).
        </description>
        <param-name>com.sun.jersey.config.property.packages</param-name>
        <param-value>com.bluelotussoftware.example.jaxrs.resources</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
This simple configuration will give you considerable flexibility in how you handle your objects. This will now allow you to produce JSONObjects like the code example below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@GET
@Path("{id}")
@Produces({MediaType.APPLICATION_JSON})
public JSONObject findJSONObject(@PathParam("id") Integer id) {
    Customer c = super.find(id);
    JSONObject jSONObject = new JSONObject();
    try {
        jSONObject.put("customerId", c.getCustomerId());
        jSONObject.put("name", c.getName());
        jSONObject.put("addressLine1", c.getAddressline1());
        jSONObject.put("addressLine2", c.getAddressline2());
        jSONObject.put("city", c.getCity());
        jSONObject.put("state", c.getState());
        jSONObject.put("phone", c.getPhone());
        jSONObject.put("fax", c.getFax());
        jSONObject.put("email", c.getEmail());
        jSONObject.put("creditLimit", c.getCreditLimit());
    } catch (JSONException ex) {
        throw new WebApplicationException(ex, Response.Status.INTERNAL_SERVER_ERROR);
    }
    return jSONObject;
}

Wednesday, December 28, 2011

Multiple File Upload Examples

I received an email today from a developer who was looking for examples of how to do multiple file uploads. I had written a post previously Multiple File Upload Options which detailed some of the options available. The option I recommended was Andrew Valums ajax-upload. The developer had indicated that the servlet I contributed was not working with Internet Explorer correctly. He was correct.

The code that I provided to Andrew Vallums was expecting application/octet-stream. However Internet Explorer and Opera were sending multipart/form-data. This would not work with the servlet example I posted since the handling mechanism is considerably different.

Alan O'Driscoll and I discovered the differences using Wireshark to sniff the packets and see what was being sent over the wire. As a result, I have written a new servlet to handle the differences between browsers.

The example application I wrote has a number of different examples included with minor variations on the theme. The version most folks are interested in is example #6 which uses the ajax-upload, and the new servlet. Here is the Apache Maven project which was developed on NetBeans and tested on GlassFish v 2.1.1.

Here is a link to the Mercurial project: apache-file-upload-ee5.

Here is another resource for file upload plugins: 9 Powerful jQuery File Upload Plugins. I discovered it while I was looking for something else.

MultiContentServlet.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.bluelotussoftware.apache.commons.fileupload.example;
 
import java.io.*;
import java.util.List;
import java.util.ListIterator;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
 
/**
 *
 * @author John Yeary
 * @author Allan O'Driscoll
 * @version 1.0
 */
public class MultiContentServlet extends HttpServlet {
 
    private static final long serialVersionUID = -2045199313944348406L;
    private static final String DESTINATION_DIR_PATH = "files";
    private static String realPath;
    private static final boolean debug = false;
 
    /**
     * {@inheritDoc}
     *
     * @param config
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        realPath = getServletContext().getRealPath(DESTINATION_DIR_PATH) + "/";
    }
 
    /**
     * Handles the HTTP
     * <code>POST</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter writer = null;
        InputStream is = null;
        FileOutputStream fos = null;
 
        try {
            writer = response.getWriter();
        } catch (IOException ex) {
            log(MultiContentServlet.class.getName() + "has thrown an exception: " + ex.getMessage());
        }
 
        boolean isMultiPart = ServletFileUpload.isMultipartContent(request);
 
        if (isMultiPart) {
            log("Content-Type: " + request.getContentType());
            // Create a factory for disk-based file items
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
 
            /*
             * Set the file size limit in bytes. This should be set as an
             * initialization parameter
             */
            diskFileItemFactory.setSizeThreshold(1024 * 1024 * 10); //10MB.
 
 
            // Create a new file upload handler
            ServletFileUpload upload = new ServletFileUpload(diskFileItemFactory);
 
            List items = null;
 
            try {
                items = upload.parseRequest(request);
            } catch (FileUploadException ex) {
                log("Could not parse request", ex);
            }
 
            ListIterator li = items.listIterator();
 
            while (li.hasNext()) {
                FileItem fileItem = (FileItem) li.next();
                if (fileItem.isFormField()) {
                    if (debug) {
                        processFormField(fileItem);
                    }
                } else {
                    writer.print(processUploadedFile(fileItem));
                }
            }
        }
 
        if ("application/octet-stream".equals(request.getContentType())) {
            log("Content-Type: " + request.getContentType());
            String filename = request.getHeader("X-File-Name");
 
            try {
                is = request.getInputStream();
                fos = new FileOutputStream(new File(realPath + filename));
                IOUtils.copy(is, fos);
                response.setStatus(HttpServletResponse.SC_OK);
                writer.print("{success: true}");
            } catch (FileNotFoundException ex) {
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                writer.print("{success: false}");
                log(MultiContentServlet.class.getName() + "has thrown an exception: " + ex.getMessage());
            } catch (IOException ex) {
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                writer.print("{success: false}");
                log(MultiContentServlet.class.getName() + "has thrown an exception: " + ex.getMessage());
            } finally {
                try {
                    fos.close();
                    is.close();
                } catch (IOException ignored) {
                }
            }
 
            writer.flush();
            writer.close();
        }
    }
 
    private void processFormField(FileItem item) {
        // Process a regular form field
        if (item.isFormField()) {
            String name = item.getFieldName();
            String value = item.getString();
            log("name: " + name + " value: " + value);
        }
    }
 
    private String processUploadedFile(FileItem item) {
        // Process a file upload
        if (!item.isFormField()) {
            try {
                item.write(new File(realPath + item.getName()));
                return "{success:true}";
            } catch (Exception ex) {
                log(FileUploadServlet.class.getName() + " has thrown an exception: " + ex.getMessage());
            }
        }
        return "{success:false}";
    }
 
    /**
     * Returns a short description of the servlet.
     *
     * @return a String containing servlet description
     */
    @Override
    public String getServletInfo() {
        return "Handles file upload data from application/octet-stream, and multipart/form-data.";
    }
}

Sunday, December 18, 2011

RichFaces 4.1.0 Final on NetBeans 7.1 IDE

I created a video which demonstrates how to add RichFaces 4.1.0 Final as a component library in NetBeans 7.1. The video details how to add the required jars to create the library, demonstrates code completion, and finally deployment to GlassFish 3.1.1 for display.

In the video I take advantage of using my local Apache Maven repository which includes all of the dependencies required.

The list below includes references to the required libraries and versions. There is a link to the video as well.

  • sac-1.3.jar
  • guava-r08.jar
  • cssparser-0.9.5.jar
  • richfaces-core-api-4.1.0.Final.jar
  • richfaces-core-impl-4.1.0.Final.jar
  • richfaces-components-api-4.1.0.Final.jar
  • richfaces-components-ui-4.1.0.Final.jar
NetBeans 7.1 Library Manager

Friday, December 16, 2011

JAX-RS Tip of the Day: NetBeans Project Layout Recommendations

When developing a JAX-RS project using NetBeans, I have a couple of simple recommendations. This will hopefully save you some time up front.

  1. First create a JavaJava Class Library project.
    This project will be used to contain classes we expect to share between the service and the client.
  2. Create a second project Java WebWeb Application.
    This project will serve as our REST service application.
  3. Add the first project to the second project.
  4. (Optional) Create a JavaJava Application
    This final project will be used for as our client application.
  5. (Optional) Add the first project to the client project.

The most important part of the recommendation is to make sure that you have a separate library project. The shared code can later be used in multiple projects without refactoring later.

Here is an example project that meets the recommendations above. It was created with NetBeans 7.1 RC2.

JAX-RS Tip of the Day: Use GenericEntity<T> for Collections

A common requirement is to generate List<T> for a REST service. The JAX-RS client must retrieve this, but the question is how to use it. The easiest method to use a GenericType<T>.

As an example, we have a service which returns a List<Widget>;. The client application will need to wrap the returned List<Widget>. We accomplish this as shown below using GenericType<List<Widget>>(){}.



1
2
3
4
5
6
public List<widget> getXml_XML() throws UniformInterfaceException {
          WebResource resource = webResource;
          return resource.accept(MediaType.APPLICATION_XML).
                  get(new GenericType<List<widget>>() {
          });
      }


The GenericType<List<Widget>>(){} allows us to wrap the List<Widget>, and return the <code>Collection</code> object we are really interested in. So remember, if you need to wrap a <code>Collection</code> use GenericType<T>.

Thursday, December 15, 2011

Java Tip of the Day: Using ProcessBuilder to Make System Calls

A few years ago I posted an article on how to execute OS level system calls using the Runtime class. When I wrote that article I was using a methodology which has been around since the 1.0 days of Java. In Java 5 Standard Edition, and subsequently improved in Java 6 and Java 7, is a class called ProcessBuilder. This class is a significant improvement over using the Runtime class. It provides complete flexibilty around the system call in terms of environment including directories, input and output streams, and execution.

Here is the NetBeans 7.1 Apache Maven project: process-builder-example.zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
 * Copyright 2011 Blue Lotus Software, LLC.
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
 
/**
 * <p>
{@link java.lang.ProcessBuilder} Example Application to execute commands
 * from command line arguments.</p>
*
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public class App {
 
    private static void copy(InputStream in, OutputStream out) throws IOException {
        while (true) {
            int c = in.read();
            if (c == -1) {
                break;
            }
            out.write((char) c);
        }
    }
 
    public static void main(String[] args) throws IOException, InterruptedException {
 
        if (args.length == 0) {
            System.out.println("You must supply at least one argument.");
            return;
        }
 
        ProcessBuilder processBuilder = new ProcessBuilder(args);
        processBuilder.redirectErrorStream(true);
 
        Process process = processBuilder.start();
        copy(process.getInputStream(), System.out);
        process.waitFor();
        System.out.println("Exit Status : " + process.exitValue());
    }
}

When I execute it from NetBeans I get something like this:
[exec:exec]
total 24
drwxr-xr-x  12 jyeary  staff   408B Dec 15 08:19 .hg
-rw-r--r--   1 jyeary  staff    52B Dec 15 08:08 .hgignore
-rw-r--r--   1 jyeary  staff   1.7K Dec 15 07:59 nbactions.xml
-rw-r--r--   1 jyeary  staff   2.0K Dec 15 08:19 pom.xml
drwxr-xr-x   4 jyeary  staff   136B Dec 14 23:31 src
drwxr-xr-x   4 jyeary  staff   136B Dec 15 08:35 target
Exit Status : 0

Wednesday, December 14, 2011

Subversion to Mercurial Migration: Using --splicemap to Map Branches and Merges

I am in the process of migrating all of my existing code projects to Mercurial from Subversion. However, the process has some snags associated with it. The biggest snag is that Mercurial does not understand the branching and merging process which is common in most Subversion repositories.  If you convert a repository from Subversion to Mercurial the timeline indicates the branch, but does not associate it with the parent (default head). This results in "floating" heads (branches). One blogger I found called it a multi-headed Hydra.

Floating Heads (Branches)

What we really want is to show consistency, and progression. We want to show the branches and merges that are stored Subversion in Mercurial. We don't want to lose that vital historical information.

So we now know what the problem is, but how do we fix it. It turns out that the convert extension method has a very poorly (read not useful at all) documented switch called splicemap. This allows us to map specific revisions in parent-child relationships.

In the Rock Hymas blog, they give an example of the format for our splicemap for Subversion. It looks like the following:
svn:<uuid>/path/to/module@revnum
However, he ends short of what to do with this information, or how to obtain it. The article is good on explanation of the issue, but not on how to solve it. I will detail a solution and show you how I did it. I will also show how to use a splicemap "so that others may live"... oh wait that is the US Air Force Pararescue motto. I will show a working example to make a point.

Process

The first thing we will need is the UUID from Subversion. This can be obtained using the svnlook command.
apple1:~ root# svnlook uuid /usr/local/svnhome/
dbc3d074-2bd0-48a6-aa6f-e69526dc1c92
The UUID is absolutely vital. It will be different for every repository.

Next, we need a log of the activity for the project we are interested in mapping. We will use the svn log -v command to get the details. The results will look something like this.  I have shortened my output to just a couple of relevant versions which we will need.

------------------------------------------------------------------------
r417 | jyeary | 2011-12-11 06:28:21 -0500 (Sun, 11 Dec 2011) | 1 line
Changed paths:
   M /MailService/trunk/MailService/build.xml
   M /MailService/trunk/MailService/nbproject/build-impl.xml
   M /MailService/trunk/MailService/nbproject/genfiles.properties
   A /MailService/trunk/MailService/nbproject/jaxws-build.xml
   M /MailService/trunk/MailService/nbproject/project.properties 
  
Merged 3.0 tag to trunk, and updated NetBeans project files. 
------------------------------------------------------------------------
r137 | jyeary | 2008-09-05 19:15:25 -0400 (Fri, 05 Sep 2008) | 1 line
Changed paths:
   M /MailService/branches/3.0-DEV/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/MailServiceBean.java

Updated properties.
------------------------------------------------------------------------
r128 | jyeary | 2008-09-02 17:51:49 -0400 (Tue, 02 Sep 2008) | 1 line
Changed paths:
   M /MailService/trunk/MailService
   M /MailService/trunk/MailService/nbproject/project.properties
   M /MailService/trunk/MailService/src/conf/sun-ejb-jar.xml
   D /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/MailService.java
   M /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/MailServiceBean.java
   D /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/MailServiceRemote.java
   A /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/local (from /MailService/branches/2.0-DEV/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/local:124)
   R /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/local/MailServiceLocal.java (from /MailService/branches/2.0-DEV/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/local/MailServiceLocal.java:124)
   A /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/remote (from /MailService/branches/2.0-DEV/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/remote:124)
   R /MailService/trunk/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/remote/MailServiceRemote.java (from /MailService/branches/2.0-DEV/MailService/src/java/com/bluelotussoftware/mail/ee/ssb/remote/MailServiceRemote.java:124)

merged 2.0 changes into trunk.
------------------------------------------------------------------------

Next we determine how to splice it together. We want to ensure that the parents, and children match up.  The file format is: child parent with a space between the child and parent. We may also provide an additional parent which is separated by a comma such as child parent1,parent2

So the first splice in the splicemap for the 2.0-DEV branch looks like this:

svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@95 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57

Note: I am splicing the branch to the trunk (parent).

Using the hg convert --datesort --splicemap splicemap1 http://10.0.1.2/svn/MailService resulted in the following output:

assuming destination MailService-hg
initializing destination MailService-hg repository
svn: cannot probe remote repository, assume it could be a subversion repository. Use --source-type if you know better.
scanning source...
sorting...
converting...
21 creating structure
20 Moving project out of MailServiceApplication
19 Fixed javadoc
18 [Netbeans SVN client generated message: create a new folder for the copy]: '
spliced in ['svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57'] as parents of svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@95
17 2.0 development branch
16 1. Updated version numbers.
15 Added new sendMessage() method that allows for different MIME types.
14 1.Added new MimeMessage format to @Remote
13 1.Added new MimeMessage format to @Local
12 1. Added new MimeMessage format
11 Updated project properties
10 1. Created a new local package
9 1. Updated Javadocs
8 Updated project properties
7 merged 2.0 changes into trunk.
6 Adding new development branch
5 Adding trunk to 3.0 Development branch
4 Removed @Local interface implementation from bean.
3 Removed @Local interface.
2 Removed directory.
1 Updated properties.
0 Merged 3.0 tag to trunk, and updated NetBeans project files.
updating tags

More importantly, the result is what we are looking for. See the image below.

Branch
Alright, that solved the branching problem, but the more difficult issue is the merge. I was banging my head on my desk when I came across this Nabble post by Bruce Frederiksen which gave me the clue I needed to merge the branch back to the default head.

We need to tell Mercurial what the parent branches are for the merge from the perspective of the repository. In this case we want the revision at 57 as one parent from the branch above, and the other to be the revision we merge at on the default head. In this case revision 123. We also need to reverse the order (remember what I said about perspective... thanks Bruce). Our new splice looks like:

svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@95 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@127 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57,\
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@123

Note: I am splicing the trunk@127 to the trunk@57 and branch@123. This is the trick.

Note: The splices are wrapped to fit on the page, the actual file requires each complete splice to be on a new line.

The result when we run the hg convert command again results in merging  the branch back to the default head.

Branch-Merge
Finally, I make the remaining splices to complete the mapping which look like:

svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@95 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@127 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@57,\
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/2.0-DEV@123
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/3.0-DEV@129 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@128
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@417 svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/trunk@128,\
svn:dbc3d074-2bd0-48a6-aa6f-e69526dc1c92/MailService/branches/3.0-DEV@137

Note: The splices are wrapped to fit on the page, the actual file requires each complete splice to be on a new line.

This results in the final completed completed product.

Completed

Conclusion

I hope that this explains how to effectively use a splicemap to branch and merge your Subversion repository into Mercurial. I hope that this saves some other unfortunate developers from trying to figure out a really cool, but poorly documented feature of the convert extension.

Tuesday, December 13, 2011

Secure Mercurial in GlassFish using SSL

A friend of mine, Wade Chandler, contacted me the other day to ask about implementing security (SSL) for Mercurial on GlassFish. Wade is NetBeans Dream Team member, and all-around great guy. I told him I would figure it out, and tell him how to do it. I also decided that I would explain to everyone how to take advantage of this cool functionality using GlassFish 3.1.1 and NetBeans 7.1 RC 2. 

In  a previous blog entry, Mercurial on GlassFish 2.1 Using Multiple Repositories, I successfully deployed Mercurial repositories on GlassFish. This time I have given everything a Java EE 6 upgrade.

One of the great aspects of using GlassFish to provide SSL is that we don't require messy configuration in Python and Mercurial to implement SSL. GlassFish handles the security for us. The only thing that the end client users will want to do is add the server fingerprint to the .hgrc file in their home directory. This will remove any warnings about a missing server fingerprint, or cacerts file.

If you have used Mercurial on Apache HTTPD Server, along with cacerts, and client certificates... this will make you make the switch immediately.

Requirements:

You can create your own web project in NetBeans, but the Mercurial.zip is a self-contained Mercurial repository, and NetBeans project which can serve as the basis for your own server with very little modification. I would simply use my project as the basis for your own deployment.

Note: The requirements include repos.zip which is optional. The repos.zip contains sample mercurial repos which are required for the demo.

Procedures

Configure GlassFish

  1. Start the GlassFish administration console.
  2. Go to Configurationserver-configSecurityRealmsfile.
    Note: You may create your own realm, this is just for demonstration purposes.
  3. Click on Manage Users.
    GlassFish Security Configuration
  4. Click on New. Add a user called mercurial in a group called Mercurial Users with the password hg.
    Note: Please change the username, group, and password for production.
  5. The server configuration is complete.

NetBeans

  1. Using NetBeans open the Mercurial project from the requirements link above.
  2. There are four files in the project (simple huh?) which control the behavior of the Mercurial repository on GlassFish: hgweb.cgi, hgweb.config, web.xml, and glassfish-web.xml.
  3. The hgweb.cgi should not need to be modified under normal circumstances.
  4. Open the hgweb.config file. Make any changes that you would like to customize the application. Pay particular attention to the location of the repositories.
    There are two examples: (1) using the "/" along with a * on the end of the file path will list all repositories in the specified directory, and (2) a specific listing.

    Note: Please note that allow_push = * and push_ssl = false are set. This will allow push to the server and let GlassFish take care of the SSL.
  5. The web.xml file has been modified to add security constraints for the alpha, and beta example repositories.
    The alpha repository has security only on PUT, POST, and DELETE.
    The beta repository will not allow you to perform any actions without logging into the server.
    The gamma repository has no security constraints.

    Note: You will need to modify the security constraints to meet your requirements.

    Note: You secure the entire repository by adding:
    <url-pattern>/cgi-bin/hgweb.cgi/*</url-pattern>.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    <?xml version="1.0" encoding="UTF-8"?>
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="3.0">
        <servlet>
            <servlet-name>cgi</servlet-name>
            <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
            <init-param>
                <param-name>debug</param-name>
                <param-value>1</param-value>
            </init-param>
            <init-param>
                <param-name>cgiPathPrefix</param-name>
                <param-value>WEB-INF/cgi</param-value>
            </init-param>
            <load-on-startup>5</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>cgi</servlet-name>
            <url-pattern>/cgi-bin/*</url-pattern>
        </servlet-mapping>
        <session-config>
            <session-timeout>
                30
            </session-timeout>
        </session-config>
        <welcome-file-list>
            <welcome-file>cgi-bin/hgweb.cgi</welcome-file>
        </welcome-file-list>
        <security-constraint>
            <display-name>Alpha</display-name>
            <web-resource-collection>
                <web-resource-name>alpha</web-resource-name>
                <description/>
                <url-pattern>/cgi-bin/hgweb.cgi/alpha/*</url-pattern>
                <http-method>PUT</http-method>
                <http-method>POST</http-method>
                <http-method>DELETE</http-method>
            </web-resource-collection>
            <auth-constraint>
                <description/>
                <role-name>Mercurial Users</role-name>
            </auth-constraint>
        </security-constraint>
        <security-constraint>
            <display-name>Beta</display-name>
            <web-resource-collection>
                <web-resource-name>beta</web-resource-name>
                <description/>
                <url-pattern>/cgi-bin/hgweb.cgi/beta/*</url-pattern>
            </web-resource-collection>
            <auth-constraint>
                <description/>
                <role-name>Mercurial Users</role-name>
            </auth-constraint>
        </security-constraint>
        <login-config>
            <auth-method>BASIC</auth-method>
            <realm-name>file</realm-name>
        </login-config>
        <security-role>
            <description/>
            <role-name>Mercurial Users</role-name>
        </security-role>
    </web-app>
  6. The glassfish-web.xml is used to map the security role(s) to the file realm on the GlassFish server.
    1
    2
    3
    4
    <security-role-mapping>
      <role-name>Mercurial Users</role-name>
      <group-name>Mercurial Users</group-name>
    </security-role-mapping>
  7. Clean, build, and Run the application.
  8. When the application successfully deploys, go to the secure connection on https://localhost:8181/Mercurial. It may prompt you to accept the server certificate. Accept it.
  9. You should see something like the image below:
    Mercurial Repositories
  10. Click on the beta repository. It should prompt you to login.
  11. Congratulations you are running Mercurial on GlassFish securely.

Note: To secure the application completely, you should disable the non-secure http port listener on port 8080, or redirect requests to the secure port. 

Verification

It would be unwise to accept that the application is secure without verification.

I made this demonstration video to prove the connection is secure. The demonstration shows both insecure and secure connections to the repository. I use Wireshark to sniff the connection, and pull the repository from the command line using hg clone.


Conclusion

Mercurial is a great distributed source control system. Its ease of use is further enhanced by using GlassFish 3.1.1 to provide the front end to your repository. It also simplifies the deployment, and use models.

Friday, December 02, 2011

Java Tip of the Day: Generic JAXB Map<K, V> XmlAdapter

There are a couple of well-known issues with using Map<K,V> with JAXB. First, JAXB does not like interfaces. Second, it considers a Map<K,V> to be a bean so it does not handle the internal implementation. The JAXB JIRA Issue is here JAXB-223

There are a number of articles published about how to adapt a Map<K,V> to work in JAXB. However, all of them I have seen are very specific mappings like Map<String,String>, or Map<Integer,String>, etc. None of them address the more specific generic case Map<K,V>. This example handles the more general case using generics. All the other examples I looked at online generally follow the example code from the XmlAdapter javadoc. The code snippet does not encapsulate the values, and generally does not adhere to good coding practices.

The process is to create an adapter is as follows:
  1. Create an mapping entry for Map<K,V>. In our case, we will create one called: MapEntryType<K, V> which will map our generic types.
  2. Next we create a value holder for our Map<K,V> which contains a List<MapEntryType<K, V>> of our entries. The value holder looks like MapType<K, V>
  3. Finally, we create an adapter which extends XmlAdapter. In our case, it is very long since I am using Java 6. In Java 7, we can use the diamond <> operator to shorten it. The adapter is XmlGenericMapAdapter<K, V> extends XmlAdapter<MapType<K, V>, Map<K, V>>.
Note: Please note that the MapEntryType<K, V> has the @XmlAccessorType(XmlAccessType.PROPERTY) set to property and the getters are annotated as @XmlElement.

The NetBeans 7.1 RC1 Apache Maven project can be downloaded here: generic-jaxb-map-adapter.zip

MapEntryType<K, V>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.bluelotussoftware.jaxb.adapter;
 
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
 
/**
 *
 * @author John Yeary
 * @version 1.0
 */
@XmlAccessorType(XmlAccessType.PROPERTY)
public class MapEntryType<K, V> {
 
    private K key;
    private V value;
 
    public MapEntryType() {
    }
 
    public MapEntryType(Map.Entry<K, V> e) {
        key = e.getKey();
        value = e.getValue();
    }
 
    @XmlElement
    public K getKey() {
        return key;
    }
 
    public void setKey(K key) {
        this.key = key;
    }
 
    @XmlElement
    public V getValue() {
        return value;
    }
 
    public void setValue(V value) {
        this.value = value;
    }
}

MapType<K, V>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.bluelotussoftware.jaxb.adapter;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
 
/**
 *
 * @author John Yeary
 * @version 1.0
 */
public class MapType<K, V> {
 
    private List<MapEntryType<K, V>> entry = new ArrayList<MapEntryType<K, V>>();
 
    public MapType() {
    }
 
    public MapType(Map<K, V> map) {
        for (Map.Entry<K, V> e : map.entrySet()) {
            entry.add(new MapEntryType<K, V>(e));
        }
    }
 
    public List<MapEntryType<K, V>> getEntry() {
        return entry;
    }
 
    public void setEntry(List<MapEntryType<K, V>> entry) {
        this.entry = entry;
    }
}

XmlGenericMapAdapter<K, V> extends XmlAdapter<MapType<K, V>, Map<K, V>>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.bluelotussoftware.jaxb.adapter;
 
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.adapters.XmlAdapter;
 
/**
 *
 * @author John Yeary
 * @version 1.0
 */
public class XmlGenericMapAdapter<K, V> extends XmlAdapter<MapType<K, V>, Map<K, V>> {
 
    @Override
    public Map<K, V> unmarshal(MapType<K, V> v) throws Exception {
        HashMap<K, V> map = new HashMap<K, V>();
 
        for (MapEntryType<K, V> mapEntryType : v.getEntry()) {
            map.put(mapEntryType.getKey(), mapEntryType.getValue());
        }
        return map;
    }
 
    @Override
    public MapType marshal(Map<K, V> v) throws Exception {
        MapType<K, V> mapType = new MapType<K, V>();
 
        for (Map.Entry<K, V> entry : v.entrySet()) {
            MapEntryType<K, V> mapEntryType = new MapEntryType<K, V>();
            mapEntryType.setKey(entry.getKey());
            mapEntryType.setValue(entry.getValue());
            mapType.getEntry().add(mapEntryType);
        }
        return mapType;
    }
}

Here is the output from our adapter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mapvalueholder>
    <map>
        <entry>
            <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Beta</key>
            <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Beta</value>
        </entry>
        <entry>
            <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Alpha</key>
            <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Alpha</value>
        </entry>
        <entry>
            <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Delta</key>
            <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Delta</value>
        </entry>
        <entry>
            <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Epsilon</key>
            <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Epsilon</value>
        </entry>
        <entry>
            <key xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Gamma</key>
            <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Gamma</value>
        </entry>
    </map>
</mapValueHolder>

We take advantage of JAXB to provide automatic XML Schema definitions for our keys and values. The only problem with this approach is that it is very "chatty". It provides a schema definition for each key and value.

If you know that you are going to be using simple types, it is more appropriate to use a specific type rather than using generics. We would replace all of the <K, V> with <String, String>. This will remove all of the schema definitions and produce an output which is much cleaner. The String adapters are included in the source code for further review.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mapvalueholder>
    <map>
        <entry>
            <key>Beta</key>
            <value>Beta</value>
        </entry>
        <entry>
            <key>Alpha</key>
            <value>Alpha</value>
        </entry>
        <entry>
            <key>Delta</key>
            <value>Delta</value>
        </entry>
        <entry>
            <key>Epsilon</key>
            <value>Epsilon</value>
        </entry>
        <entry>
            <key>Gamma</key>
            <value>Gamma</value>
        </entry>
    </map>
</mapValueHolder>

Coding Tip of the Day: Using Map<?,?> Collections

I was working on some code in the last few days where I came across a protected Map map instance variable. I examined the code and javadocs which indicated that the intent was to provide a mapping of keys and values. However the actual implementation was setting the keys and values to the same value. This again is not really an issue. However, the usage of the Map was incorrect.

See if you can determine the issue from the code below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
 
private static MapValueHolder<String, String> me;
 
    public static void main(String[] args) {
 
        me = new MapValueHolderImpl<String, String>();
        Map<String, String> mx = new HashMap<String, String>();
        mx.put("A", "A");
        mx.put("B", "B");
        mx.put("C", "C");
        me.setMap(mx);
 
 
        for (String key : me.getMap().keySet()) {
            System.out.println("MapValueHolder Key --> " + key);
        }
 
        for (String key : me.getMap().keySet()) {
            System.out.println(key + " --> " + me.getMap().get(key));
        }
...
The MapValueHolder<K,V> looks like the following.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 * Copyright 2011 Bluelotus Software, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public abstract class MapValueHolder<K, V> {
 
    protected Map<K, V> map = new HashMap<K, V>();
 
    public MapValueHolder() {
    }
 
    public Map<K, V> getMap() {
        return Collections.unmodifiableMap(map);
    }
 
    public void setMap(Map<K, V> map) {
        this.map = new HashMap<K, V>(map);
    }
}
Here is the implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 * Copyright 2011 Bluelotus Software, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public class MapValueHolderImpl<K, V> extends MapValueHolder<K, V> {
}

As a general rule, you should use the key to fetch the value. Even if you expect that the values are the same. Since the instance variable is protected it can be modified in a sub-class, or any class in the same package. Since we code to interfaces, or abstractions (super classes). We can not be sure that the implementation class we are provided does what we expect. In this case, it does what we expect.
MapValueHolder Key --> A
MapValueHolder Key --> B
MapValueHolder Key --> C
A --> A
B --> B
C --> C

Here we continue using another implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
...
        me = new KeyValueHolder<String, String>();
        me.setMap(mx);
 
 
        for (String key : me.getMap().keySet()) {
            System.out.println("MapValueHolder Key --> " + key);
        }
 
        for (String key : me.getMap().keySet()) {
            System.out.println(key + " --> " + me.getMap().get(key));
        }
...
This time the result looks different.
MapValueHolder Key --> A
MapValueHolder Key --> B
MapValueHolder Key --> C
A --> null
B --> null
C --> null

This is because our MapValueHolder<K,V> is different.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 * Copyright 2011 Bluelotus Software, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public class KeyValueHolder<K, V> extends MapValueHolder<K, V> {
 
    public KeyValueHolder() {
    }
 
    @Override
    public void setMap(Map<K, V> map) {
        Map<K, V> mx = new HashMap<K, V>(map);
        for (K k : map.keySet()) {
            mx.put(k, null);
        }
        this.map = mx;
    }
}
Admittedly, the implementation above is a bit contrived. We really are getting is a Set<K>. In the next example, we get another result.
1
2
3
4
5
6
7
8
...
        me = new SingletonMapValueHolder<String, String>();
        me.setMap(mx);
 
        for (String key : me.getMap().keySet()) {
            System.out.println("MapValueHolder Key --> " + key);
        }
...
The result is considerably different.

MapValueHolder Key --> A
A --> A

Here is the implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 * Copyright 2011 Bluelotus Software, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
import java.util.Collections;
import java.util.Map;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public class SingletonMapValueHolder<K, V> extends MapValueHolder<K, V> {
 
    public SingletonMapValueHolder() {
    }
 
    @Override
    public void setMap(Map<K, V> map) {
        this.map = Collections.singletonMap(
                map.entrySet().iterator().next().getKey(),
                map.entrySet().iterator().next().getValue());
    }
}
Again, this is a contrived result where we are just returning the first key and value in a Map<K,V>. Here is another example which demonstrates unexpected results.
1
System.out.println(new Mod().MAP + " --> " + me.map.toString());
This results in this very strange result.

Map --> {}

Here I use reflection to modify the expected result.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
 * Copyright 2011 John Yeary <jyeary@bluelotussoftware.com>.
 * Copyright 2011 Bluelotus Software, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bluelotussoftware.example;
 
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
 
/**
 *
 * @author John Yeary <jyeary@bluelotussoftware.com>
 * @version 1.0
 */
public final class Mod {
 
    public static final String MAP = "Map";
 
    static {
        try {
            Field[] fx = App.class.newInstance().getClass().getDeclaredFields();
            Field f = fx[0];
            f.setAccessible(true);
            MapValueHolder<String, String> mvh = new MapValueHolderImpl<String, String>();
            f.set(null, mvh);
        } catch (Throwable ex) {
            Logger.getLogger(Mod.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
If you have followed me to this point, you will have learned a little bit about the Java Type system, reflection, abstraction, and Collections. Yet, you may be asking what is the tip? Here are some rules:
  1. Use the keys to fetch values.
  2. Check for null values. In my examples, I did not check for null values.
  3. Use the correct Java Collection.
The Java Collection Framework covers most common cases. Take the time to read about them. The code above was based on a couple of issues I discovered: (1) Using keys for values, and (2) not using the correct collection. In this case, they should have used a List<V>. If they wanted to use an ordered Map<K,V>, they should have used a LinkedHashmap<K,V>

The code for the examples are here: bad-mapping.implementation.zip

The code was developed using NetBeans 7.1 RC1.

Popular Posts