Mapping namespace-less query parameters using Spring MVC in Liferay

Posted by under Java , , ,

When you work with portlet applications, all request parameters must be namespaced otherwise you won’t have access to their values using Portlet API in Liferay. However, in some cases your portlet has to process URL which was not constructed using Portlet API. In this post I will show you how to map un-namespaced query parameters declaratively using Spring MVC Portlet framework.

Imagine you have a single portlet deployed on your portal page and that portlet displays content of the article whose ID is passed as a query param, for instance:

https://news.portal.my/?id=123

That’s pretty standard requirement. URL like that is clear and easily bookmarkable. However, since parameter id is not namespaced, you portlet won’t have access to it through Portlet API. So the following code prints null into log:

@Controller
public class NewsController {

    private static final Logger LOG = Logger.getLogger(NewsController.class);

    @RenderMapping
    public void render(@RequestParam(value = "id", required = false) Integer id) {
        LOG.debug(id);
    }
}

In portal, namespace is unique for each portlet and it’s used as a prefix for all portlet’s parameters. This mechanism helps to avoid naming collisions when several portlets are deployed on the same page. For that reason you have to put portlet’s namespace into URL to make the prior code work:

https://news.portal.my/?p_p_id=news_WAR_portal&p_p_lifecycle=0&p_p_state=normal&p_p_mode=view&p_p_col_id=column-2&p_p_col_count=1&_news_WAR_portal_id=123

Now the URL is no longer short and clear. You would have to use Liferay’s friendly URL feature to simplify that although it would require additional XML configuration. However you have also a second option. You can access id parameter using Liferay API.

@Controller
public class NewsController {

    private static final Logger LOG = Logger.getLogger(NewsController.class);

    @RenderMapping
    public void view(RenderRequest req) {
        HttpServletRequest liferayServletReq = PortalUtil.getHttpServletRequest(req);
        String paramId = PortalUtil.getOriginalServletRequest(liferayServletReq).getParameter("id");

        LOG.debug(paramId);
    }
}

That worked fine, log revealed correct article number 123. But we can do better. We can wrap the above code into special WebArgumentResolver implementation and create additional annotation similar to @RequestParam to map un-namespaced query parameters to controller’s method arguments. Let’s call it @QueryParam. Using this approach we can utilise great annotation-based programming model in Spring.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface QueryParam {

    String value() default "";

    boolean required() default true;

    String defaultValue() default ValueConstants.DEFAULT_NONE;
}

Notice that the code is except annotation name exactly the same as for @RequestParam and it works almost equally. Now the WebArgumentResolver implementation:

public class QueryParamResolver implements WebArgumentResolver {

    @Override
    public Object resolveArgument(MethodParameter param, NativeWebRequest request) throws Exception {
        Assert.isInstanceOf(PortletRequest.class, request.getNativeRequest(),
                "You can use @QueryParam only in application running within a portlet container!");
        if (!param.hasParameterAnnotation(QueryParam.class)) {
            return UNRESOLVED;
        }

        return mapQueryParamToObject(param, (PortletRequest) request.getNativeRequest());
    }

    private Object mapQueryParamToObject(MethodParameter param, PortletRequest portletReq) {
        QueryParam queryAnnot = param.getParameterAnnotation(QueryParam.class);
        String queryParamName = queryAnnot.value();
        String queryParamValue = getServletRequest(portletReq).getParameter(queryParamName);
        if (queryParamValue == null) {
            Class<?> paramType = param.getParameterType();
            if (queryAnnot.required()) {
                throw new IllegalStateException("Missing parameter '" + queryParamName + "' of type ["
                        + paramType.getName() + "]");
            }
            if (boolean.class.equals(paramType)) {
                return Boolean.FALSE;
            }
            if (paramType.isPrimitive()) {
                throw new IllegalStateException(
                        "Optional "
                                + paramType
                                + " parameter '"
                                + queryParamName
                                + "' is not present but cannot be translated into a null value due to being declared as a "
                                + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
            }
        }

        WebDataBinder binder = new WebRequestDataBinder(null, queryParamName);
        return binder.convertIfNecessary(queryParamValue, param.getParameterType(), param);
    }

    private HttpServletRequest getServletRequest(PortletRequest request) {
        return PortalUtil.getOriginalServletRequest(PortalUtil.getHttpServletRequest(request));
    }
}

Only one step left. In order to map un-namespaced query parameter to render method parameter marked with @QueryParam annotation, we need to register resolver in our spring context.

    <bean id="annotationMethodHandlerAdapter" class="org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="customArgumentResolver">
            <bean class="org.exitcode.liferay.QueryParamResolver" />
        </property>
    </bean>

Finally, we can place our new annotation into controller:

@Controller
public class NewsController {

    private static final Logger LOG = Logger.getLogger(NewsController.class);

    @RenderMapping
    public void render(@QueryParam(value = "id", required = false) Integer id) {
        LOG.debug(id);
    }
}

Using declarative approach our code is now much more readable thanks to clear separation between the parts that describe the mapping (@QueryParam) and how the mapping is actually performed (QueryParamResolver).

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.