How to Integrate Struts Conventions with Tiles such that the benefit of conventions is maintained

不羁岁月 提交于 2019-12-04 11:26:55

Here are the steps needed:

  • Create custom tiles result which dynamically builds a "location" string (the location string is the value passed to tiles) which takes into account the namespace, actionName.
  • Create a package which uses this result (named "tiles") and have conventions use that as it's parent package
  • Implement and register a "com.opensymphony.xwork2.UnknownHandler", this step is the most critical as this handler is called when the result can't be resolved
  • Tiles definition(s) which make use of "location" passed in from the first step

The above steps require the following in struts.xml

<struts>
   <constant name="struts.convention.default.parent.package" value="tiles-package"/>
   <bean type="com.opensymphony.xwork2.UnknownHandler" name="tilesUnknownHandler" class="com.kenmcwilliams.tiles.result.TilesUnknownHandler"/>

   <package  name="tiles-package" extends="convention-default">
      <result-types>
         <result-type default="true" name="tiles" class="com.kenmcwilliams.tiles.result.TilesResult"/>
      </result-types>
   </package>   
</struts>

Custom result-type implementation:

package com.kenmcwilliams.tiles.result;

import com.opensymphony.xwork2.ActionInvocation;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.ServletDispatcherResult;
import org.apache.tiles.TilesContainer;
import org.apache.tiles.access.TilesAccess;
import org.apache.tiles.request.ApplicationContext;
import org.apache.tiles.request.servlet.ServletRequest;
import org.apache.tiles.request.servlet.ServletUtil;

public class TilesResult extends ServletDispatcherResult {

    private static final Logger log = Logger.getLogger(TilesResult.class.getName());

    public TilesResult() {
        super();
    }

    public TilesResult(String location) {
        super(location);
    }

    @Override
    public void doExecute(String location, ActionInvocation invocation) throws Exception {
        //location = "test.definition"; //for test
        log.log(Level.INFO, "TilesResult doExecute() location: {0}", location);
        //Start simple conventions
        //
        if (/** tiles && **/location == null) {
            String namespace = invocation.getProxy().getNamespace();
            String actionName = invocation.getProxy().getActionName();
            location = namespace + "#" + actionName + ".jsp"; //Warning forcing extension
            log.log(Level.INFO, "TilesResult namespace: {0}", namespace);
            log.log(Level.INFO, "TilesResult actionName: {0}", actionName);
            log.log(Level.INFO, "TilesResult location: {0}", location);
        }
        //End simple conventions
        setLocation(location);
        ServletContext context = ServletActionContext.getServletContext();
        ApplicationContext applicationContext = ServletUtil.getApplicationContext(context);
        TilesContainer container = TilesAccess.getContainer(applicationContext);
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpServletResponse response = ServletActionContext.getResponse();
        ServletRequest servletRequest = new ServletRequest(applicationContext, request, response);
        container.render(location, servletRequest);
    }
}

TilesUnknownHandler Implementation:

package com.kenmcwilliams.tiles.result;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.XWorkException;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig.Builder;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import flexjson.JSONSerializer;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import org.apache.commons.lang.StringUtils;
import org.apache.struts2.convention.ConventionUnknownHandler;

public class TilesUnknownHandler extends ConventionUnknownHandler {

    private static final Logger log = Logger.getLogger(TilesUnknownHandler.class.getName());
    private static final String conventionBase = "/WEB-INF/content";

    @Inject
    public TilesUnknownHandler(Configuration configuration, ObjectFactory objectFactory,
            ServletContext servletContext, Container container,
            @Inject("struts.convention.default.parent.package") String defaultParentPackageName,
            @Inject("struts.convention.redirect.to.slash") String redirectToSlash,
            @Inject("struts.convention.action.name.separator") String nameSeparator) {
        super(configuration, objectFactory, servletContext, container, defaultParentPackageName,
                redirectToSlash, nameSeparator);
        log.info("Constructed TilesUnknownHandler");
    }

    @Override
    public ActionConfig handleUnknownAction(String namespace, String actionName)
            throws XWorkException {
        ActionConfig actionConfig;
        log.info("TilesUnknownHandler: before handleUnknownAction");
        ActionConfig handleUnknownAction = super.handleUnknownAction(namespace, actionName);

        log.info("TilesUnknownHandler: after handleUnknownAction, returning with:");
        log.log(Level.INFO, "...ActionConfig value: {0}", (new JSONSerializer().serialize(handleUnknownAction)));
        log.log(Level.INFO, "Modifying handleUnknowAction result handler");

        Map<String, ResultConfig> results = handleUnknownAction.getResults();
        ResultConfig resultConfig = results.get("success");
        Builder builder = new ResultConfig.Builder("com.opensymphony.xwork2.config.entities.ResultConfig", "com.kenmcwilliams.tiles.result.TilesResult");
        Map<String, String> params = resultConfig.getParams();

        String tilesResultString = null;
        String location = params.get("location");
        if (location != null && !location.isEmpty()) {
            int length = conventionBase.length();

            if(StringUtils.startsWith(location, conventionBase)){
                String subString = location.substring(length); //chop off "/WEB-INF/content"
                int count = StringUtils.countMatches(subString, "/");//TODO: maybe check for "//", although I don't know why it would be in the string
                if (count == 1){//empty namespace
                    tilesResultString = subString.replaceFirst("/", "#"); //TODO: because I am doing a straight replacement of the last element the else can probably be removed
                }else{ //replace the last slash between the namespace and the file with "#"
                    int lastIndex = subString.lastIndexOf("/");
                    //subString.substring(lastIndex, lastIndex);
                    String nameSpace = subString.substring(0, lastIndex);
                    String file = subString.substring(lastIndex + 1);
                    tilesResultString = nameSpace + "#" + file;
                }
            }
        }

        Map<String, String> myParams = new LinkedHashMap<String, String>();
        myParams.put("location", tilesResultString);

        builder.addParams(myParams);
        ResultConfig build = builder.build();
        Map<String, ResultConfig> myMap = new LinkedHashMap<String, ResultConfig>();
        myMap.put("success", build);
        log.log(Level.INFO, "\n\n...results: {0}\n\n", (new JSONSerializer().serialize(results)));
        actionConfig = new ActionConfig.Builder(handleUnknownAction).addResultConfigs(myMap).build();
        //className("com.kenmcwilliams.tiles.result.TilesResult")
        return actionConfig;
    }

    @Override
    public Result handleUnknownResult(ActionContext actionContext, String actionName,
            ActionConfig actionConfig, String resultCode) throws XWorkException {
        log.info("TilesUnknownHandler: before handleUnknownResult");
        Result handleUnknownResult = super.handleUnknownResult(actionContext, actionName, actionConfig, resultCode);
        log.info("TilesUnknownHandler: after handleUnknownResult, returning with:");
        log.log(Level.INFO, "...Result value: {0}", (new JSONSerializer().serialize(handleUnknownResult)));
        return handleUnknownResult;
    }
}

An example of how to use our "location" string which is in the form of: NameSpace + "#" + ActionName + ".jsp", note this definition <definition name="REGEXP:(.*)#(.*)" extends="default"> in the following:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN" "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
    <definition name="default" template="/WEB-INF/template/template.jsp">
        <put-list-attribute name="cssList" cascade="true">
            <add-attribute value="/style/cssreset-min.css" />
            <add-attribute value="/style/cssfonts-min.css" />
            <add-attribute value="/style/cssbase-min.css" />  
            <add-attribute value="/style/grids-min.css" />
            <add-attribute value="/script/jquery-ui-1.8.24.custom/css/ui-lightness/jquery-ui-1.8.24.custom.css" />
            <add-attribute value="/style/style.css" />
        </put-list-attribute>    
        <put-list-attribute name="jsList" cascade="true">
            <add-attribute value="/script/jquery/1.8.1/jquery.min.js" />
            <add-attribute value="/script/jquery-ui-1.8.24.custom/js/jquery-ui-1.8.24.custom.min.js" />
            <add-attribute value="/script/jquery.sort.js" />
            <add-attribute value="/script/custom/jquery-serialize.js" />
        </put-list-attribute>   
        <put-attribute name="title" value="defaults-name" cascade="true"  type="string"/>
        <put-attribute name="head" value="/WEB-INF/template/head.jsp"/>
        <put-attribute name="header" value="/WEB-INF/template/header.jsp"/>
        <put-attribute name="body" value="/WEB-INF/template/body.jsp"/>
        <put-attribute name="footer" value="/WEB-INF/template/footer.jsp"/>
    </definition>

    <definition name="REGEXP:(.*)#(.*)"  extends="default">
        <put-attribute name="title" cascade="true" expression="OGNL:@com.opensymphony.xwork2.ActionContext@getContext().name"/>
        <put-attribute name="body" value="/WEB-INF/content{1}/{2}"/>
    </definition>

</tiles-definitions>

With this in place you can create JSP's under /WEB-INF/content/someplace/my-action.jsp

Just as you would with conventions AND tiles will decorate it appropriately as well if you create an action class called com.myapp.action.someplace.MyAction without any result type this code will execute and the /WEB-INF/content/someplace/my-action.jsp result would still be rendered.

There you have it conventions + tiles with no more annotations (well for the normal case).

NOTES:

  • This answer certainly isn't perfect but it does provide a working example of the strategy which can be applied to other view technologies (sitemesh, others).
  • Currently you can see the ".jsp" is being appended in the tiles result NOT in the tiles definitions this is inflexible. The specific extension should be specified within tiles, that is the body attribute within the definition should append the specific view type (.jsp, .fml, .vm) because you should know best at that time.
  • It is important to note that definitions are tried in the order they are given,so you can override the normal case REGEXP:(.*)#(.*) by placing definitions between the default and REGEXP:(.*)#(.*) definitions. For instance a definition called authenticated\(.*) can be placed between these two definitions. After all if you couldn't do this and all pages had to be tiled the same we really wouldn't be using tiles!
  • Just so you know when using tiles3 (the struts2 tiles3 plugin) you can use all three types of view technologies (jsp, freemarker, velocity) to compose one tile. It works. You are probably going to use one view technology consistently but it's nice to know it is possible.
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!