001// -------------------------------------------------------------------------------- 002// Copyright 2002-2025 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 io.github.classgraph.ClassGraph; 037import java.lang.reflect.InvocationTargetException; 038import java.lang.reflect.Method; 039import javax.servlet.ServletException; 040import org.apache.commons.beanutils.BeanMap; 041import org.apache.log4j.Logger; 042import org.apache.struts.action.ActionFormBean; 043import org.apache.struts.action.ActionForward; 044import org.apache.struts.config.ActionConfig; 045import org.apache.struts.config.ForwardConfig; 046 047/** 048 * <p>Finds Sprouts present in the classpath and registers them with 049 * Struts, using cues from annotations present to set specific properties.</p> 050 * 051 * <p>This needs to be configured as a plug-in in 052 * <code>struts-config.xml</code>.</p> 053 * 054 * <p>TODO create GlobalForward annotation and create global forwards based on that</p> 055 * 056 * @author Seth Fitzsimmons 057 */ 058public class SproutAutoLoaderPlugIn 059 extends SproutContextLoaderPlugIn { 060 061 private final static Logger log = Logger.getLogger(SproutAutoLoaderPlugIn.class); 062 063 private void loadForm(final Class bean) { 064 final var annotations = bean.getAnnotations(); 065 066 for(var j = 0; j < annotations.length; j++) { 067 final var a = annotations[j]; 068 final Class type = a.annotationType(); 069 070 if(type.equals(SproutForm.class)) { 071 final var form = (SproutForm) a; 072 final var actionFormName = form.name(); 073 final var actionFormType = bean.getName(); 074 075 if(log.isDebugEnabled()) { 076 log.debug("ActionForm " + actionFormName + " -> " + actionFormType); 077 } 078 079 getModuleConfig().addFormBeanConfig(new ActionFormBean(actionFormName, actionFormType)); 080 } 081 } 082 } 083 084 private void loadAction(final Class bean) { 085 final var annotations = bean.getAnnotations(); 086 087 for(var i = 0; i < annotations.length; i++) { 088 final var a = annotations[i]; 089 final Class type = a.annotationType(); 090 091 if(type.equals(SproutAction.class)) { 092 final var form = (SproutAction) a; 093 final var path = form.path(); 094 final Class<ActionConfig> mappingClass = form.mappingClass(); 095 final var scope = form.scope(); 096 final var name = form.name(); 097 final var parameter = form.parameter(); 098 final var validate = form.validate(); 099 final var input = form.input(); 100 final var properties = form.properties(); 101 final var forwards = form.forwards(); 102 ActionConfig actionConfig = null; 103 104 try { 105 var constructor = mappingClass.getDeclaredConstructor(); 106 107 actionConfig = constructor.newInstance(); 108 } catch (NoSuchMethodException nsme) { 109 log.error("Failed to create a new instance of " + mappingClass + ", " + nsme.getMessage()); 110 } catch (InstantiationException ie) { 111 log.error("Failed to create a new instance of " + mappingClass + ", " + ie.getMessage()); 112 } catch (IllegalAccessException iae) { 113 log.error("Failed to create a new instance of " + mappingClass + ", " + iae.getMessage()); 114 } catch (InvocationTargetException ite) { 115 log.error("Failed to create a new instance of " + mappingClass + ", " + ite.getMessage()); 116 } 117 118 if(actionConfig != null) { 119 actionConfig.setPath(path); 120 actionConfig.setType(bean.getName()); 121 actionConfig.setScope(scope); 122 actionConfig.setValidate(validate); 123 124 if(name.length() > 0) { 125 actionConfig.setName(name); 126 } 127 128 if(parameter.length() > 0) { 129 actionConfig.setParameter(parameter); 130 } 131 132 if(input.length() > 0) { 133 actionConfig.setInput(input); 134 } 135 136 if(properties != null && properties.length > 0) { 137 var beanMap = new BeanMap(actionConfig); 138 139 for(var j = 0; j < properties.length; j++) { 140 beanMap.put(properties[j].property(), properties[j].value()); 141 } 142 } 143 144 if(forwards != null && forwards.length > 0) { 145 for(var j = 0; j < forwards.length; j++) { 146 var fcModule = forwards[j].module(); 147 148 actionConfig.addForwardConfig(makeForward(forwards[j].name(), forwards[j].path(), forwards[j].redirect(), 149 fcModule.length() == 0? null: fcModule)); 150 } 151 } 152 } 153 154 if(log.isDebugEnabled()) { 155 log.debug("Action " + path + " -> " + bean.getName()); 156 } 157 158 getModuleConfig().addActionConfig(actionConfig); 159 } 160 } 161 } 162 163 private void loadAnnotatedActionsAndForms() { 164 try(var scanResult = new ClassGraph() 165 .enableAnnotationInfo() 166 .scan()) { 167 final var actionClasses = scanResult 168 .getClassesWithAnnotation(SproutAction.class.getName()); 169 final var formClasses = scanResult 170 .getClassesWithAnnotation(SproutForm.class.getName()); 171 172 for(var actionClass : actionClasses) { 173 Class clazz = actionClass.loadClass(); 174 175 loadAction(clazz); 176 } 177 178 for(var formClass : formClasses) { 179 Class clazz = formClass.loadClass(); 180 181 loadForm(clazz); 182 } 183 } 184 } 185 186 /** 187 * Extends SproutContextLoaderPlugIn's initialization callback to add 188 * Struts registration of Sprouts. 189 */ 190 @Override 191 public void onInit() throws ServletException { 192 loadAnnotatedActionsAndForms(); 193 } 194 195 /** 196 * Helper method for creating ActionForwards. 197 * 198 * @param name Forward name. 199 * @param path Registered path. 200 * @return ForwardConfig. 201 */ 202 private ForwardConfig makeForward(final String name, final String path) { 203 return makeForward(name, path, false, null); 204 } 205 206 /** 207 * Helper method for creating ActionForwards. 208 * 209 * @param name Forward name. 210 * @param path Registered path. 211 * @param redirect Whether this should be an HTTP redirect. 212 * @return ActionForward. 213 */ 214 private ActionForward makeForward(final String name, final String path, final boolean redirect, final String module) { 215 final var actionForward = new ActionForward(); 216 217 actionForward.setName(name); 218 actionForward.setPath(path); 219 actionForward.setRedirect(redirect); 220 actionForward.setModule(module); 221 222 return actionForward; 223 } 224 225 /** 226 * Finds the method in the target class which corresponds to a registered 227 * pathname. 228 * 229 * @param name Action portion of pathname. 230 * @param clazz Target class. 231 * @return Corresponding method. 232 * @throws NoSuchMethodException when corresponding method cannot be found. 233 */ 234 private Method findMethod(final String name, final Class clazz) throws NoSuchMethodException { 235 final var methods = clazz.getMethods(); 236 237 for(var i = 0; i < methods.length; i++) { 238 var methodName = methods[i].getName(); 239 240 if(methodName.equals("publick")) 241 methodName = "public"; 242 243 if(methodName.equalsIgnoreCase(name.replaceAll("_([a-z])", "$1"))) 244 return methods[i]; 245 } 246 247 throw new NoSuchMethodException(name); 248 } 249 250}