Passing complex objects in URL parameters

Imagine you would like to pass primitive data types, complex Java objects like
java.util.Data, java.lang.List, generic classes, arrays and everything what you want via URL parameters in order to preset default values on any web page after the page was loaded. Common task? Yeah, but available solutions are mostly restricted to encoding / decoding of java.lang.String. The approach I will show doesn’t have any limits on the data types. Only one limit is the limit of the URL size. URLs longer than 2083 characters may not work properly in old IE versions. Modern Firefox, Opera, and Safari can handle at least 80000 characters in URL.

Passing any type of objects via URL parameters is possible if we serialize objects to JSON and deserialize them on the server-side. An encoded JSON string has a valid
 
format and this is a way to go! Well, but there is a problem. A serialization / deserialization to / from the JSON format works fine if the object is a non-generic type. However, if the object is of a generic type, then the Generic type information gets lost because of Java Type Erasure. What to do in this case? The solution I want to demonstrate is not restricted to JSF, but as I’m developing Java / Web front-ends, I’m rotating in this circle… So, let’s start. First, we need a proper converter to receive URL parameters in JSON format and converts them back to Java.
PrimeFaces Extensions provides one – JsonConverter.java. How it works? The following example shows how the JsonConverter can be applied to f:viewParam to convert a list of Strings in the JSON format to a List in Java.

<f:metadata>
    <f:viewParam name='subscriptions' value='#{subscriptionController.subscriptions}'>
        <pe:convertJson type='java.util.List<java.lang.String>' />
    </f:viewParam>
</f:metadata>

<h:selectManyCheckbox value='#{subscriptionController.subscriptions}'>
    <f:selectItem id='item1' itemLabel='News' itemValue='1' />
    <f:selectItem id='item2' itemLabel='Sports' itemValue='2' />
    <f:selectItem id='item3' itemLabel='Music' itemValue='3' />
</h:selectManyCheckbox>

The JsonConverter has one optional attribute type. We don’t need to provide a data type information for primitives such as boolean or int. But generally, the type information is a necessary attribute. It specifies a data type of the value object. Any primitive type, array, non generic or generic type is supported. The type consists of fully qualified class names (except primitives). Examples:

'long[]'
'java.lang.String'
'java.util.Date'
'java.util.Collection<java.lang.Integer>'
'java.util.Map<java.lang.String, com.prime.FooPair<java.lang.Integer, java.util.Date>>'
'com.prime.FooNonGenericClass'
'com.prime.FooGenericClass<java.lang.String, java.lang.Integer>'
'com.prime.FooGenericClass<int[], com.prime.FooGenericClass<com.prime.FooNonGenericClass, java.lang.Boolean>>'

The string in the type is parsed at runtime. The code for the JsonConverter is available here (for readers who are interested in details). The JsonConverter is based on three other classes: ParameterizedTypeImpl.java,
GsonConverter.java and DateTypeAdapter.java. The last one is a special adapter for dates because java.util.Date should be converted to milliseconds as long and back to the java.util.Date. So far so good. But how to prepare the values as URL parameters on the Java side? I will show an utility class which can be used for that. Read comments please, they are self-explained.

import org.apache.log4j.Logger;
import org.primefaces.extensions.converter.JsonConverter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

/**
 * Builder for request parameters.
 */
public class RequestParameterBuilder {

    private Logger LOG = Logger.getLogger(RequestParameterBuilder.class);

    private StringBuilder buffer;
    private String originalUrl;
    private JsonConverter jsonConverter;
    private String encoding;
    private boolean added;

    /**
     * Creates a builder instance by the current request URL.
     */
    public RequestParameterBuilder() {
        this(((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getRequestURL()
            .toString());
    }

    /**
     * Creates a builder instance by the given URL.
     *
     * @param url URL
     */
    public RequestParameterBuilder(String url) {
        buffer = new StringBuilder(url);
        originalUrl = url;
        jsonConverter = new JsonConverter();

        encoding = FacesContext.getCurrentInstance().getExternalContext().getRequestCharacterEncoding();
        if (encoding == null) {
            encoding = 'UTF-8';
        }
    }

    /**
     * Adds a request parameter to the URL without specifying a data type of the given parameter value.
     * Parameter's value is converted to JSON notation when adding. Furthermore, it will be encoded
     * according to the acquired encoding.
     *
     * @param name name of the request parameter
     * @param value value of the request parameter
     * @return RequestParameterBuilder updated this instance which can be reused
     */
    public RequestParameterBuilder paramJson(String name, Object value) throws UnsupportedEncodingException {
        return paramJson(name, value, null);
    }

    /**
     * Adds a request parameter to the URL with specifying a data type of the given parameter value. Data type is sometimes
     * required, especially for Java generic types, because type information is erased at runtime and the conversion to JSON
     * will not work properly. Parameter's value is converted to JSON notation when adding. Furthermore, it will be encoded
     * according to the acquired encoding.
     *
     * @param name name of the request parameter
     * @param value value of the request parameter
     * @param type data type of the value object. Any primitive type, array, non generic or generic type is supported.
     *             Data type is sometimes required to convert a value to a JSON representation. All data types should be
     *             fully qualified.
     * @return RequestParameterBuilder updated this instance which can be reused
     */
    public RequestParameterBuilder paramJson(String name, Object value, String type)
        throws UnsupportedEncodingException {
        jsonConverter.setType(type);

        String jsonValue;
        if (value == null) {
            jsonValue = 'null';
        } else {
            jsonValue = jsonConverter.getAsString(null, null, value);
        }

        if (added || originalUrl.contains('?')) {
            buffer.append('&');
        } else {
            buffer.append('?');
        }

        buffer.append(name);
        buffer.append('=');
        buffer.append(URLEncoder.encode(jsonValue, encoding));

        // set a flag that at least one request parameter was added
        added = true;

        return this;
    }

    /**
     * Adds a request parameter to the URL. This is a convenient method for primitive, plain data types.
     * Parameter's value will not be converted to JSON notation when adding. It will be only encoded
     * according to the acquired encoding. Note: null values will not be added.
     *
     * @param name name of the request parameter
     * @param value value of the request parameter
     * @return RequestParameterBuilder updated this instance which can be reused
     */
    public RequestParameterBuilder param(String name, Object value) throws UnsupportedEncodingException {
        if (value == null) {
            return this;
        }

        if (added || originalUrl.contains('?')) {
            buffer.append('&');
        } else {
            buffer.append('?');
        }

        buffer.append(name);
        buffer.append('=');
        buffer.append(URLEncoder.encode(value.toString(), encoding));

        // set a flag that at least one request parameter was added
        added = true;

        return this;
    }

    /**
     * Builds the end result.
     *
     * @return String end result
     */
    public String build() {
        String url = buffer.toString();

        if (url.length() > 2083) {
            LOG.error('URL ' + url + ' is longer than 2083 chars (' + buffer.length() +
                '). It may not work properly in old IE versions.');
        }

        return url;
    }

    /**
     * Resets the internal state in order to be reused.
     *
     * @return RequestParameterBuilder reseted builder
     */
    public RequestParameterBuilder reset() {
        buffer = new StringBuilder(originalUrl);
        jsonConverter.setType(null);
        added = false;

        return this;
    }
}

A typically bean using the RequestParameterBuilder provides a parametrized URL by calling either paramJson(…) or param(…).

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

/**
 * UrlParameterProvider bean.
 */
@ManagedBean
@SessionScoped
public class UrlParameterProvider implements Serializable {

    private String parametrizedUrl;

    @PostConstruct
    protected void initialize() {
        RequestParameterBuilder rpBuilder = new RequestParameterBuilder('/views/examples/params.jsf');
        try {
            List<String> subscriptions = new ArrayList<String>();
            tableBlockEntries.add('2');
            tableBlockEntries.add('3');

            // add the list to URL parameters with conversion to JSON
            rpBuilder.paramJson('subscriptions', subscriptions, 'java.util.List<java.lang.String>');

            // add int values to URL parameters without conversion to JSON (just for example)
            rpBuilder.param('min', 20);
            rpBuilder.param('max', 80);   
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        parametrizedUrl = rpBuilder.build();
    }

    public String getParametrizedUrl() {
        return parametrizedUrl;
    }
}

Using in XHTML – example with h:outputLink

<h:outputLink value='#{urlParameterProvider.parametrizedUrl}'>
    Parametrized URL
</h:outputLink>

Once the user clicks on the link and lands on the target page with the relative path/views/examples/params.jsf, he / she will see a pre-checked h:selectManyCheckbox. The real world is more complicated. In the fact I’ve written a lot of custom converters having JsonConverter inside. So that instead of <pe:convertJson type=’…’ /> are custom converters attached. That subject is going beyond of this post.
 

Reference: Passing complex objects in URL parameters from our JCG partner Oleg Varaksin at the Thoughts on software development blog.

Related Whitepaper:

Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expressions

Get ready to program in a whole new way!

Functional Programming in Java will help you quickly get on top of the new, essential Java 8 language features and the functional style that will change and improve your code. This short, targeted book will help you make the paradigm shift from the old imperative way to a less error-prone, more elegant, and concise coding style that’s also a breeze to parallelize. You’ll explore the syntax and semantics of lambda expressions, method and constructor references, and functional interfaces. You’ll design and write applications better using the new standards in Java 8 and the JDK.

Get it Now!  

Leave a Reply


eight × 7 =



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

15,153 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books
Get tutored by the Geeks! JCG Academy is a fact... Join Now
Hello. Add your message here.