View Javadoc
1   package gboat2.base.plugin.struts.dispatcher;
2   
3   import gboat2.base.bridge.GboatAppConstants;
4   import gboat2.base.bridge.GboatAppContext;
5   import gboat2.base.bridge.debug.DefaultDebugHook;
6   import gboat2.base.bridge.exception.DefaultGboatNestedException;
7   import gboat2.base.bridge.util.BundleUtil;
8   
9   import java.io.File;
10  import java.io.IOException;
11  import java.net.URL;
12  
13  import javax.servlet.http.HttpServletRequest;
14  import javax.servlet.http.HttpServletResponse;
15  
16  import org.apache.commons.io.FileUtils;
17  import org.apache.commons.lang3.StringUtils;
18  import org.apache.struts2.ServletActionContext;
19  import org.apache.struts2.dispatcher.ServletDispatcherResult;
20  import org.apache.struts2.dispatcher.StrutsResultSupport;
21  import org.osgi.framework.Bundle;
22  import org.osgi.framework.FrameworkUtil;
23  import org.springframework.util.Assert;
24  
25  import com.opensymphony.xwork2.ActionInvocation;
26  import com.opensymphony.xwork2.inject.Container;
27  import com.opensymphony.xwork2.inject.Inject;
28  import com.opensymphony.xwork2.util.TextParseUtil;
29  import com.opensymphony.xwork2.util.logging.Logger;
30  import com.opensymphony.xwork2.util.logging.LoggerFactory;
31  
32  /**
33   * <p>
34   * 实现对 OSGI Bundle Jar 包中的 JSP 页面的请 求,在 struts-plugin.xml 配置文件中添加如下配置:
35   * <pre>
36   *  <code>
37   *  &lt;constant name="struts.convention.relative.result.types" value="dispatcher,velocity,freemarker<b>,osgiDispatcher</b>" /&gt;
38   *  &lt;!-- 在运行时通过“GboatDispatcherResult” 解压 OSGI Bundle Jar 中的的 JSP 文件时,JSP 文件的存放目录 --&gt;
39   *  &lt;constant name="struts.gboat2.osgi.resource.outdir" value="/WEB-INF/bundles-resource" /&gt;
40   *  &lt;!-- type="osgiDispatcher" 的 Result 拒绝请求的后缀,多值之前使用英文逗号分隔 --&gt;
41   *  &lt;constant name="struts.gboat2.osgi.resource.denysuffixs" value=".class,.properties" /&gt;
42   *  
43   *  &lt;package name="gboat2-default"&gt;
44   *      &lt;result-types&gt;
45   *          <b>&lt;result-type name="osgiDispatcher" class="gboat2.base.plugin.struts.dispatcher.GboatDispatcherResult"/&gt;</b>
46   *      &lt;/result-types&gt;
47   *  &lt;/package&gt;
48   *  </code>
49   * </pre>
50   * </p>
51   * <b>示例:</b>
52   * <pre>
53   * &lt;result name="success" type="osgiDispatcher"&gt;
54   *   &lt;param name="location"&gt;foo.jsp&lt;/param&gt;
55   * &lt;/result&gt;
56   * </pre>
57   *
58   * This result follows the same rules from {@link StrutsResultSupport}.
59   *
60   * @see javax.servlet.RequestDispatcher
61   * @see javax.servlet.ServletDispatcherResult
62   */
63  public class GboatDispatcherResult extends ServletDispatcherResult {
64      public static final String STRUTS_GBOAT2_OSGI_RESOURCE_OUTDIR = "struts.gboat2.osgi.resource.outdir";
65      public static final String STRUTS_GBOAT2_OSGI_RESOURCE_DENYSUFFIXS = "struts.gboat2.osgi.resource.denysuffixs";
66  
67      protected Container container;
68      protected static String osgiResourceOutDir;
69      protected String[] osgiResourceDenySuffixs;
70      
71      private static final long serialVersionUID = 1L;
72  
73      private static final Logger LOG = LoggerFactory.getLogger(GboatDispatcherResult.class);
74      
75      public GboatDispatcherResult() {
76          super();
77      }
78  
79      public GboatDispatcherResult(String location) {
80          super(location);
81      }
82  
83      /**
84       * 完成对 OSGI Bundle 中的资源文件(如:JSP)的访问,实现步骤如下
85       * <ol>
86       * <li>判断 Struts 配置文件中 {@value #STRUTS_GBOAT2_OSGI_RESOURCE_OUTDIR}
87       * 指定的目录下是否存在该文件,如果存在,则直接调用父类的方法进行处理</li>
88       * <li>如果上一步的结果为不存在,则判断当前执行的 Action 对应的 Bundle 中是否存在该文件,如果存在,则将 Bundle Jar
89       * 中的该文件复制一份到 Struts 配置文件中 {@value #STRUTS_GBOAT2_OSGI_RESOURCE_OUTDIR}
90       * 指定的目录下</li>
91       * <li>调用父类的方法进行处理</li>
92       * </ol>
93       * 
94       * @param finalLocation the location to dispatch to.
95       * @param invocation the execution state of the action
96       * @throws Exception if an error occurs. If the dispatch fails the error
97       *             will go back via the HTTP request.
98       */
99      @Override
100     public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
101         HttpServletRequest request = ServletActionContext.getRequest();
102         HttpServletResponse response = ServletActionContext.getResponse();
103         // 如果匹配拒绝访问的后缀,返回 403 错误
104         if(StringUtils.endsWithAny(finalLocation, osgiResourceDenySuffixs)) {
105             response.sendError(HttpServletResponse.SC_FORBIDDEN, "result '" + finalLocation + "' is forbidden");
106             return;
107         }
108         
109         Object includeServletPath = request.getAttribute(GboatAppConstants.INCLUDE_SERVLET_PATH_KEY);
110         if(includeServletPath == null) {
111         /*
112          * 往 request 中设置一个 key 为“javax.servlet.include.servlet_path”的 attribute,
113          * 在调用 org.apache.struts2.components.Include.getContextRelativePath(request, relativePath) 方法的时候需要用到。
114          * 注:使用 <s:include>、<g2:include> 和 <g2:page extend="..."> 标签时均会调用到 Include 类的 getContextRelativePath方法
115          */
116             request.setAttribute(GboatAppConstants.INCLUDE_SERVLET_PATH_KEY, finalLocation);
117         }
118         
119         Object action = invocation.getAction();
120         Bundle bundle = FrameworkUtil.getBundle(action.getClass()); // 根据 Action 获取到对应的 Bundle
121         Assert.notNull(bundle, "没有任何 Bundle 中包含请求的 Action[" + action + "] -> [" + request.getRequestURL() + "]");
122 //        ConventionsService conventionsService = container.getInstance(ConventionsService.class,
123 //                    container.getInstance(String.class, ConventionConstants.CONVENTION_CONVENTIONS_SERVICE));
124 //        finalLocation = (conventionsService.determineResultPath(action.getClass()) + "/" + finalLocation).replaceAll("/+", "/");
125         String targetLocation = processFinalLocation(finalLocation, bundle);
126         extractBundleJsp(finalLocation, bundle); // 从 Bundle 中复制文件到 WebRoot 下
127         
128         super.doExecute(targetLocation, invocation);
129     }
130     
131     /**
132      * 获取 Bundle 中资源文件存放路径的前缀,如:/WEB-INF/bundles-resource/gboat2.web/
133      * @param bundle OSGI Bundle
134      * @return 以“/”结尾的路径前缀
135      */
136     public static String getBundleResourcePrefix(Bundle bundle) {
137         String prefix = StringUtils.defaultString(osgiResourceOutDir) // Struts2 配置文件中设置的保存位置
138                 + "/" + bundle.getSymbolicName() // Bundle 的 SymbolicName
139                 + "/";
140         return prefix.replace('\\', '/').replaceAll("/+", "/");
141     }
142     
143     /**
144      * 对要转发的路径进行加工处理,最终交给 Web 容器(如 Tomcat) 进行处理的路径即该方法的返回值
145      * @param finalLocation the location to dispatch to.
146      * @param bundle 当前调用的 Action 所在的 Bundle 实例
147      * @return 处理后的 finalLocation
148      */
149     public static String processFinalLocation(String finalLocation, Bundle bundle) {
150         finalLocation = finalLocation.replace('\\', '/').replaceAll("/+", "/");
151         String prefix = getBundleResourcePrefix(bundle);
152         // 最终访问的 Bundle.jar 中的路径
153         return finalLocation.startsWith(prefix) ? finalLocation : (prefix + StringUtils.removeStart(finalLocation, "/"));
154     }
155     
156     /**
157      * 将 Bundle 中的 JSP 文件解压到 WebRoot 目录下
158      * @param page JSP 文件路径,如:/gboat2.web/content/test.jsp、/content/test.jsp、test.jsp
159      * @return 解压后的 JSP 文件路径(Web 容器可以直接访问的路径)
160      */
161     public static String extractBundleJsp(String page) {
162         String innerPath = StringUtils.removeStart(page, osgiResourceOutDir);
163         Bundle bundle = BundleUtil.getBundleForRequestJsp(innerPath, ServletActionContext.getContext().getActionInvocation());
164    
165        if(bundle == null) {
166            throw new DefaultGboatNestedException("无法根据 JSP 文件路径[" + page + "] 找到匹配的 Bundle");
167        } else {
168            String symbolicName = bundle.getSymbolicName();
169            if(innerPath.startsWith(symbolicName)) {
170                innerPath = innerPath.substring(symbolicName.length());
171            } else if(innerPath.startsWith("/" + symbolicName)) {
172                innerPath = innerPath.substring(symbolicName.length() + 1);
173            }
174            return extractBundleJsp(innerPath, bundle);
175        }
176     }
177     
178     /**
179      * 将 Bundle 中的 JSP 文件解压到常量 {@value #STRUTS_GBOAT2_OSGI_RESOURCE_OUTDIR} 指定的目录下
180      * @param finalLocation JSP 文件路径,如: /content/foo.jsp
181      * @param bundle JSP 文件所在 Bundle
182      * @return 解压后的 JSP 文件路径(Web 容器可以直接访问的路径)
183      */
184     public static String extractBundleJsp(String finalLocation, Bundle bundle){
185         String symbolicName =  bundle.getSymbolicName();
186         String targetLocation = processFinalLocation(finalLocation, bundle);
187         // 最终由 Web 容器(如 Tomcat)处理的目标文件
188         File targetFile = new File(GboatAppContext.getWebRootPath(), targetLocation);
189         
190         DefaultDebugHook debugHook = DefaultDebugHook.getInstance();
191         boolean isDevMode = debugHook.isBundleDebugEnabled(symbolicName); // 是否为调试模式
192         if (isDevMode || (!targetFile.exists())) { // 开启了调试模式,或者目标文件不存在时,才进行相应操作
193             File targetDir = targetFile.getParentFile();
194             if(!targetDir.exists() && !targetDir.mkdirs()) // 目标文件的父目录不存在,且创建失败
195                 throw new DefaultGboatNestedException("无法创建输出目录 [" + targetDir.getAbsolutePath() + "]");
196             
197             try {
198                 if(isDevMode) {
199                     File sourceFile = new File(debugHook.getSourceFilePath(symbolicName, finalLocation));
200                     if (!sourceFile.exists() || sourceFile.isDirectory()) {
201                         // 调试模式下源代码文件不存在,或是一个目录
202                         throw new DefaultGboatNestedException("Bundle[" + symbolicName + "] 开启了调试模式,在其源代码目录下并未发现文件 '"
203                                                 + finalLocation + "' -> [" + sourceFile.getAbsolutePath() + "]");
204                     }
205                     
206                    if(!targetFile.exists() || sourceFile.lastModified() > targetFile.lastModified()) {
207                        FileUtils.copyFile(sourceFile, targetFile, true); // 复制文件
208                    }
209                 } else {
210                     URL url = bundle.getResource(finalLocation);
211                     if (url == null) {
212                         LOG.error("Bundle[#0] 中没有找到资源文件 [#1]", symbolicName, finalLocation);
213                     } else {
214                         FileUtils.copyURLToFile(url, targetFile);
215                     }
216                 }
217             } catch (IOException e) {
218                 throw new DefaultGboatNestedException("从 Bundle[" + symbolicName + "] 中复制文件 [" + finalLocation + "] 到 WebRoot 下发生错误!", e);
219             }
220         }
221         return targetLocation;
222     }
223     
224     @Inject
225     public void setContainer(Container container) {
226         this.container = container;
227     }
228 
229     @Inject(STRUTS_GBOAT2_OSGI_RESOURCE_OUTDIR)
230     public void setOsgiResourceOutDir(String osgiResourceOutDir) {
231         GboatDispatcherResult.osgiResourceOutDir = osgiResourceOutDir;
232     }
233 
234     @Inject(STRUTS_GBOAT2_OSGI_RESOURCE_DENYSUFFIXS)
235     public void setOsgiResourceDenySuffixs(String osgiResourceDenySuffixs) {
236         if(StringUtils.isNotBlank(osgiResourceDenySuffixs)){
237             this.osgiResourceDenySuffixs = TextParseUtil.commaDelimitedStringToSet(osgiResourceDenySuffixs).toArray(new String[0]);
238         }
239     }
240 }