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}