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.
01 02 03 04 05 06 07 08 09 10 11 | <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:
1 2 3 4 5 6 7 8 | '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.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 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 | 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(…).
01 02 03 04 05 06 07 08 09 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 | 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
1 2 3 | <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.