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}