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}