001// -------------------------------------------------------------------------------- 002// Copyright 2002-2024 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017/* 018Copyright 2005-2006 Seth Fitzsimmons <seth@note.amherst.edu> 019 020Licensed under the Apache License, Version 2.0 (the "License"); 021you may not use this file except in compliance with the License. 022You may obtain a copy of the License at 023 024 http://www.apache.org/licenses/LICENSE-2.0 025 026Unless required by applicable law or agreed to in writing, software 027distributed under the License is distributed on an "AS IS" BASIS, 028WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 029See the License for the specific language governing permissions and 030limitations under the License. 031*/ 032package com.echothree.view.client.web.struts.sprout; 033 034import com.echothree.view.client.web.struts.sprout.annotation.SproutAction; 035import com.echothree.view.client.web.struts.sprout.annotation.SproutForm; 036import com.echothree.view.client.web.struts.sprout.annotation.SproutForward; 037import com.echothree.view.client.web.struts.sprout.annotation.SproutProperty; 038import io.github.classgraph.ClassGraph; 039import io.github.classgraph.ClassInfoList; 040import io.github.classgraph.ScanResult; 041import java.lang.annotation.Annotation; 042import java.lang.reflect.Constructor; 043import java.lang.reflect.InvocationTargetException; 044import java.lang.reflect.Method; 045import javax.servlet.ServletException; 046import org.apache.commons.beanutils.BeanMap; 047import org.apache.log4j.Logger; 048import org.apache.struts.action.ActionFormBean; 049import org.apache.struts.action.ActionForward; 050import org.apache.struts.action.ActionServlet; 051import org.apache.struts.action.PlugIn; 052import org.apache.struts.config.ActionConfig; 053import org.apache.struts.config.ForwardConfig; 054import org.apache.struts.config.ModuleConfig; 055 056/** 057 * <p>Finds Sprouts present in the classpath and registers them with 058 * Struts, using cues from annotations present to set specific properties.</p> 059 * 060 * <p>This needs to be configured as a plug-in in 061 * <code>struts-config.xml</code>.</p> 062 * 063 * <p>TODO create GlobalForward annotation and create global forwards based on that</p> 064 * 065 * @author Seth Fitzsimmons 066 */ 067public class SproutAutoLoaderPlugIn 068 extends SproutContextLoaderPlugIn { 069 070 private final static Logger log = Logger.getLogger(SproutAutoLoaderPlugIn.class); 071 072 private void loadForm(final Class bean) { 073 final Annotation[] annotations = bean.getAnnotations(); 074 075 for(int j = 0; j < annotations.length; j++) { 076 final Annotation a = annotations[j]; 077 final Class type = a.annotationType(); 078 079 if(type.equals(SproutForm.class)) { 080 final SproutForm form = (SproutForm) a; 081 final String actionFormName = form.name(); 082 final String actionFormType = bean.getName(); 083 084 if(log.isDebugEnabled()) { 085 log.debug("ActionForm " + actionFormName + " -> " + actionFormType); 086 } 087 088 getModuleConfig().addFormBeanConfig(new ActionFormBean(actionFormName, actionFormType)); 089 } 090 } 091 } 092 093 private void loadAction(final Class bean) { 094 final Annotation[] annotations = bean.getAnnotations(); 095 096 for(int i = 0; i < annotations.length; i++) { 097 final Annotation a = annotations[i]; 098 final Class type = a.annotationType(); 099 100 if(type.equals(SproutAction.class)) { 101 final SproutAction form = (SproutAction) a; 102 final String path = form.path(); 103 final Class<ActionConfig> mappingClass = form.mappingClass(); 104 final String scope = form.scope(); 105 final String name = form.name(); 106 final String parameter = form.parameter(); 107 final boolean validate = form.validate(); 108 final String input = form.input(); 109 final SproutProperty[] properties = form.properties(); 110 final SproutForward[] forwards = form.forwards(); 111 ActionConfig actionConfig = null; 112 113 try { 114 Constructor<ActionConfig> constructor = mappingClass.getDeclaredConstructor(); 115 116 actionConfig = constructor.newInstance(); 117 } catch (NoSuchMethodException nsme) { 118 log.error("Failed to create a new instance of " + mappingClass + ", " + nsme.getMessage()); 119 } catch (InstantiationException ie) { 120 log.error("Failed to create a new instance of " + mappingClass + ", " + ie.getMessage()); 121 } catch (IllegalAccessException iae) { 122 log.error("Failed to create a new instance of " + mappingClass + ", " + iae.getMessage()); 123 } catch (InvocationTargetException ite) { 124 log.error("Failed to create a new instance of " + mappingClass + ", " + ite.getMessage()); 125 } 126 127 if(actionConfig != null) { 128 actionConfig.setPath(path); 129 actionConfig.setType(bean.getName()); 130 actionConfig.setScope(scope); 131 actionConfig.setValidate(validate); 132 133 if(name.length() > 0) { 134 actionConfig.setName(name); 135 } 136 137 if(parameter.length() > 0) { 138 actionConfig.setParameter(parameter); 139 } 140 141 if(input.length() > 0) { 142 actionConfig.setInput(input); 143 } 144 145 if(properties != null && properties.length > 0) { 146 var beanMap = new BeanMap(actionConfig); 147 148 for(int j = 0; j < properties.length; j++) { 149 beanMap.put(properties[j].property(), properties[j].value()); 150 } 151 } 152 153 if(forwards != null && forwards.length > 0) { 154 for(int j = 0; j < forwards.length; j++) { 155 String fcModule = forwards[j].module(); 156 157 actionConfig.addForwardConfig(makeForward(forwards[j].name(), forwards[j].path(), forwards[j].redirect(), 158 fcModule.length() == 0? null: fcModule)); 159 } 160 } 161 } 162 163 if(log.isDebugEnabled()) { 164 log.debug("Action " + path + " -> " + bean.getName()); 165 } 166 167 getModuleConfig().addActionConfig(actionConfig); 168 } 169 } 170 } 171 172 private void loadAnnotatedActionsAndForms() { 173 try(ScanResult scanResult = new ClassGraph() 174 .enableAnnotationInfo() 175 .scan()) { 176 final ClassInfoList actionClasses = scanResult 177 .getClassesWithAnnotation(SproutAction.class.getName()); 178 final ClassInfoList formClasses = scanResult 179 .getClassesWithAnnotation(SproutForm.class.getName()); 180 181 for(var actionClass : actionClasses) { 182 Class clazz = actionClass.loadClass(); 183 184 loadAction(clazz); 185 } 186 187 for(var formClass : formClasses) { 188 Class clazz = formClass.loadClass(); 189 190 loadForm(clazz); 191 } 192 } 193 } 194 195 /** 196 * Extends SproutContextLoaderPlugIn's initialization callback to add 197 * Struts registration of Sprouts. 198 */ 199 @Override 200 public void onInit() throws ServletException { 201 loadAnnotatedActionsAndForms(); 202 } 203 204 /** 205 * Helper method for creating ActionForwards. 206 * 207 * @param name Forward name. 208 * @param path Registered path. 209 * @return ForwardConfig. 210 */ 211 private ForwardConfig makeForward(final String name, final String path) { 212 return makeForward(name, path, false, null); 213 } 214 215 /** 216 * Helper method for creating ActionForwards. 217 * 218 * @param name Forward name. 219 * @param path Registered path. 220 * @param redirect Whether this should be an HTTP redirect. 221 * @return ActionForward. 222 */ 223 private ActionForward makeForward(final String name, final String path, final boolean redirect, final String module) { 224 final ActionForward actionForward = new ActionForward(); 225 226 actionForward.setName(name); 227 actionForward.setPath(path); 228 actionForward.setRedirect(redirect); 229 actionForward.setModule(module); 230 231 return actionForward; 232 } 233 234 /** 235 * Finds the method in the target class which corresponds to a registered 236 * pathname. 237 * 238 * @param name Action portion of pathname. 239 * @param clazz Target class. 240 * @return Corresponding method. 241 * @throws NoSuchMethodException when corresponding method cannot be found. 242 */ 243 private Method findMethod(final String name, final Class clazz) throws NoSuchMethodException { 244 final Method[] methods = clazz.getMethods(); 245 246 for(int i = 0; i < methods.length; i++) { 247 String methodName = methods[i].getName(); 248 249 if(methodName.equals("publick")) 250 methodName = "public"; 251 252 if(methodName.equalsIgnoreCase(name.replaceAll("_([a-z])", "$1"))) 253 return methods[i]; 254 } 255 256 throw new NoSuchMethodException(name); 257 } 258 259}