ajaxspringspring-mvctiles

NoClassDefFoundError while using AjaxUrlBasedViewResolver in Spring mvc 3


for some time i was developing web app using Spring framework and everything worked fine. However recently it turned out that i would have to introduce some ajax calls to my pages. After some googling i found out that Tiles2 gives nice support for resolving views while ajax call is recived, using parameter 'fragments=nameOfTile'. To achive that you have to use org.springframework.js.ajax.AjaxUrlBasedViewResolverwhich and also org.springframework.js.ajax.tiles2.AjaxTilesView.

Below is my configuration for view resolvers(NOTE THAT I USE TILES.3.0.1):

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions" value="WEB-INF/tiles-config.xml"/>
</bean>
<bean id="ajaxViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver" p:order="0">
        <property name="viewClass" value="org.springframework.js.ajax.tiles2.AjaxTilesView"/>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.tiles3.TilesViewResolver" p:order="1">
</bean>

As i mentioned before when i wasnt using this:

<bean id="ajaxViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver" p:order="0">
        <property name="viewClass" value="org.springframework.js.ajax.tiles2.AjaxTilesView"/>
</bean>

Everything was fine, but when i introduced ajaxViewResolver to my code. What ever page i request(normally and ajax calls) i get this response:

HTTP Status 500 - Handler processing failed; nested exception is java.lang.NoClassDefFoundError: org/apache/tiles/TilesApplicationContext

So my questions are:

thanks for help, K

EDIT #1: @Bar says: "Have you included the spring-webflow jars? springsource.org/spring-web-flow#download"

Well i am using maven as my dependency manager. Below you can see my pom.xml:

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>3.2.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
      </dependency>
    <dependency>
        <groupId>org.apache.tiles</groupId>
        <artifactId>tiles-jsp</artifactId>
        <version>3.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.5.6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.webflow</groupId>
        <artifactId>spring-webflow</artifactId>
        <version>2.3.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator-annotation-processor</artifactId>
      <version>4.1.0.Final</version>
    </dependency>

So it should be included.


Solution

  • Ok so I have detected the problem. Since Tails 3 the abstraction above RequestContext has changed and org.springframework.js.ajax.tiles2.AjaxTilesView was using the old one. That's why it wasn't working, it was referencing non exsisting class.

    After some experiments and searching through tails 3 javadocs i've managed to rewrite this(AjaxTilesView) class and adopt it to work in tails 3 enviroment. I've done few tests and for now it works fine. The only problem is that u have to specify header in ur ajax request or additional parameter which will inform that this is actually ajax call. Below you can see rewritten AjaxTilesView class, example config and example of jquery call.

    AjaxTilesView.Java :

    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Set;
    
    import javax.servlet.ServletContext;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.tiles.Attribute;
    import org.apache.tiles.AttributeContext;
    import org.apache.tiles.Definition;
    import org.apache.tiles.access.TilesAccess;
    import org.apache.tiles.impl.BasicTilesContainer;
    import org.apache.tiles.request.ApplicationContext;
    import org.apache.tiles.request.Request;
    import org.apache.tiles.request.jsp.JspRequest;
    import org.apache.tiles.request.servlet.ServletRequest;
    import org.apache.tiles.request.servlet.ServletUtil;
    import org.springframework.js.ajax.AjaxHandler;
    import org.springframework.js.ajax.SpringJavascriptAjaxHandler;
    import org.springframework.util.Assert;
    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.support.JstlUtils;
    import org.springframework.web.servlet.support.RequestContext;
    import org.springframework.web.servlet.view.tiles3.TilesView;
    
    public class AjaxTilesView extends TilesView {
    
        private static final String FRAGMENTS_PARAM = "fragments";
    
        private ApplicationContext applicationContext;
    
        private AjaxHandler ajaxHandler = new SpringJavascriptAjaxHandler();
    
        public void afterPropertiesSet() throws Exception {
            super.afterPropertiesSet();
        }
    
        public AjaxHandler getAjaxHandler() {
            return ajaxHandler;
        }
    
        public void setAjaxHandler(AjaxHandler ajaxHandler) {
            this.ajaxHandler = ajaxHandler;
        }
    
        protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)
                throws Exception {
    
    
            ServletContext servletContext = getServletContext();
            if (ajaxHandler.isAjaxRequest(request, response)) {
                String[] fragmentsToRender = getRenderFragments(model, request, response);
                if (fragmentsToRender.length == 0) {
                    logger.warn("An Ajax request was detected, but no fragments were specified to be re-rendered.  "
                            + "Falling back to full page render.  This can cause unpredictable results when processing "
                            + "the ajax response on the client.");
                    super.renderMergedOutputModel(model, request, response);
                    return;
                }
    
                this.applicationContext = ServletUtil.getApplicationContext(getServletContext());
                BasicTilesContainer container = (BasicTilesContainer) TilesAccess.getContainer(this.applicationContext);
                if (container == null) {
                    throw new ServletException("Tiles container is not initialized. "
                            + "Have you added a TilesConfigurer to your web application context?");
                }
    
                exposeModelAsRequestAttributes(model, request);
                JstlUtils.exposeLocalizationContext(new RequestContext(request, servletContext));
                Request tilesRequestContext =  new ServletRequest(this.applicationContext, request, response);
                Definition compositeDefinition = container.getDefinitionsFactory().getDefinition(getUrl(),
                        tilesRequestContext);
                Map flattenedAttributeMap = new HashMap();
                flattenAttributeMap(container, tilesRequestContext, flattenedAttributeMap, compositeDefinition, request,
                        response);
                addRuntimeAttributes(container, flattenedAttributeMap, request, response);
                if (fragmentsToRender.length > 1) {
                    request.setAttribute(JspRequest.FORCE_INCLUDE_ATTRIBUTE_NAME, true);
                }
    
                for (int i = 0; i < fragmentsToRender.length; i++) {
                    Attribute attributeToRender = (Attribute) flattenedAttributeMap.get(fragmentsToRender[i]);
    
                    if (attributeToRender == null) {
                        throw new ServletException("No tiles attribute with a name of '" + fragmentsToRender[i]
                                + "' could be found for the current view: " + this);
                    } else {
                        container.startContext(tilesRequestContext).inheritCascadedAttributes(compositeDefinition);
                        container.render(attributeToRender, tilesRequestContext);
                        container.endContext(tilesRequestContext);
                    }
                }
            } else {
                super.renderMergedOutputModel(model, request, response);
            }
        }
    
        protected String[] getRenderFragments(Map model, HttpServletRequest request, HttpServletResponse response) {
            String attrName = request.getParameter(FRAGMENTS_PARAM);
            String[] renderFragments = StringUtils.commaDelimitedListToStringArray(attrName);
            return StringUtils.trimArrayElements(renderFragments);
        }
    
        /**
         * <p>
         * Iterate over all attributes in the given Tiles definition. Every attribute value that represents a template (i.e.
         * start with "/") or is a nested definition is added to a Map. The method class itself recursively to traverse
         * nested definitions.
         * </p>
         * 
         * @param container the TilesContainer
         * @param requestContext the TilesRequestContext
         * @param resultMap the output Map where attributes of interest are added to.
         * @param compositeDefinition the definition to search for attributes of interest.
         * @param request the servlet request
         * @param response the servlet response
         */
        protected void flattenAttributeMap(BasicTilesContainer container, Request requestContext,
                Map resultMap, Definition compositeDefinition, HttpServletRequest request, HttpServletResponse response) {
            Set<String> locAttr = compositeDefinition.getLocalAttributeNames();
            Set<String> cascAttr = compositeDefinition.getCascadedAttributeNames();
    
    
            for (String s : locAttr) {
                String attributeName = s;
                Attribute attribute = compositeDefinition.getAttribute(attributeName);
                if (attribute.getValue() == null || !(attribute.getValue() instanceof String)) {
                    continue;
                }
                String value = attribute.getValue().toString();
                if (value.startsWith("/")) {
                    resultMap.put(attributeName, attribute);
                } else if (container.isValidDefinition(value, new ServletRequest(this.applicationContext, request, response))) {
                    resultMap.put(attributeName, attribute);
                    Definition nestedDefinition = container.getDefinitionsFactory().getDefinition(value, requestContext);
                    Assert.isTrue(nestedDefinition != compositeDefinition, "Circular nested definition: " + value);
                    flattenAttributeMap(container, requestContext, resultMap, nestedDefinition, request, response);
                }
            }
    
            if(cascAttr == null)
                return;
    
            for (String s : cascAttr) {
                String attributeName = s;
                System.out.println(s);
                Attribute attribute = compositeDefinition.getAttribute(attributeName);
                if (attribute.getValue() == null || !(attribute.getValue() instanceof String)) {
                    continue;
                }
                String value = attribute.getValue().toString();
                if (value.startsWith("/")) {
                    resultMap.put(attributeName, attribute);
                } else if (container.isValidDefinition(value, new ServletRequest(this.applicationContext, request, response))) {
                    resultMap.put(attributeName, attribute);
                    Definition nestedDefinition = container.getDefinitionsFactory().getDefinition(value, requestContext);
                    Assert.isTrue(nestedDefinition != compositeDefinition, "Circular nested definition: " + value);
                    flattenAttributeMap(container, requestContext, resultMap, nestedDefinition, request, response);
                }
            }
    
    
        }
    
        /**
         * <p>
         * Iterate over dynamically added Tiles attributes (see "Runtime Composition" in the Tiles documentation) and add
         * them to the output Map passed as input.
         * </p>
         * 
         * @param container the Tiles container
         * @param resultMap the output Map where attributes of interest are added to.
         * @param request the Servlet request
         * @param response the Servlet response
         */
        protected void addRuntimeAttributes(BasicTilesContainer container, Map resultMap, HttpServletRequest request,
                HttpServletResponse response) {
            AttributeContext attributeContext = container.getAttributeContext(new ServletRequest(this.applicationContext, request, response));
            Set attributeNames = new HashSet();
            if (attributeContext.getLocalAttributeNames() != null) {
                attributeNames.addAll(attributeContext.getLocalAttributeNames());
            }
            if (attributeContext.getCascadedAttributeNames() != null) {
                attributeNames.addAll(attributeContext.getCascadedAttributeNames());
            }
            Iterator iterator = attributeNames.iterator();
            while (iterator.hasNext()) {
                String name = (String) iterator.next();
                Attribute attr = attributeContext.getAttribute(name);
                resultMap.put(name, attr);
            }
        }
    }
    

    jQuery:

    $('div[id="form"]').on("click",function(){
        $.ajax({
            type:"GET",
            beforeSend: function (request)
            {
                request.setRequestHeader("Accept", "text/html;type=ajax");
            },
            url: "directlink?fragments=form",
            processData: false,
            success: function(msg) {
                $('div[id="form"]').append(msg);
                }
        });
    });
    

    dispatcher-config.xml:

    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
        <property name="definitions" value="WEB-INF/tiles-config.xml"/>
    </bean>
    <bean id="viewResolver" class="org.springframework.web.servlet.view.tiles3.TilesViewResolver" p:order="1"/>
    <bean id="ajaxViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver" p:order="0">
        <property name="viewClass" value="com.springframework.web.views.AjaxTilesView"/>
    </bean>
    

    If any dont like to add header to their jquery, as i mentioned before you can use parameter ajaxSource the value isnt important, but it has to have text. so example url would look like:

     'myurl?fragments=someTail&ajaxSource=on'
     'myurl?fragments=someTail&ajaxSource=placeholdertext'