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