001    /*
002     * Licensed under the Apache License, Version 2.0 (the "License");
003     * you may not use this file except in compliance with the License.
004     * You may obtain a copy of the License at
005     *
006     * http://www.apache.org/licenses/LICENSE-2.0
007     *
008     * Unless required by applicable law or agreed to in writing, software
009     * distributed under the License is distributed on an "AS IS" BASIS,
010     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011     * See the License for the specific language governing permissions and
012     * limitations under the License.
013     * 
014     * See the NOTICE file distributed with this work for additional
015     * information regarding copyright ownership.
016     */
017    
018    package com.osbcp.apbs.annotations;
019    
020    import java.util.ArrayList;
021    import java.util.List;
022    import java.util.Set;
023    
024    import javax.annotation.processing.AbstractProcessor;
025    import javax.annotation.processing.RoundEnvironment;
026    import javax.annotation.processing.SupportedAnnotationTypes;
027    import javax.lang.model.element.Element;
028    import javax.lang.model.element.ElementKind;
029    import javax.lang.model.element.Modifier;
030    import javax.lang.model.element.TypeElement;
031    import javax.lang.model.type.ExecutableType;
032    import javax.lang.model.type.TypeMirror;
033    import javax.lang.model.type.TypeVisitor;
034    import javax.lang.model.util.SimpleTypeVisitor6;
035    import javax.tools.Diagnostic.Kind;
036    
037    /**
038     * The Annotation Processing Tool logic for the @IsAjaxPostBackEvent annotation.
039     * 
040     * @see @IsAjaxPostBackEvent
041     * @see <a href="http://download.oracle.com/javase/6/docs/api/javax/annotation/processing/AbstractProcessor.html">AbstractProcessor</a>
042     * @author <a href="mailto:christoffer@christoffer.me">Christoffer Pettersson</a>
043     */
044    
045    @SupportedAnnotationTypes("com.osbcp.apbs.annotations.IsAjaxPostBackEvent")
046    public class AjaxPostBackAnnotationProcessor extends AbstractProcessor {
047    
048            /**
049             * Used to toggle debug mode.
050             */
051    
052            public static boolean DEBUG = false;
053    
054            /**
055             * AbstractProcessor.process
056             * 
057             * @see <a href="http://download.oracle.com/javase/6/docs/api/javax/annotation/processing/AbstractProcessor.html#process%28java.util.Set,%20javax.annotation.processing.RoundEnvironment%29">AbstractProcessor.process</a>
058             * @param annotations The annotation types requested to be processed.
059             * @param roundEnvironment Environment for information about the current and prior round.
060             * @return Whether or not the set of annotations are claimed by this processor.
061             */
062    
063            @Override
064            public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
065    
066                    try {
067    
068                            StringBuilder debug = new StringBuilder();
069    
070                            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(IsAjaxPostBackEvent.class);
071    
072                            for (final Element classElement : elements) {
073    
074                                    /*
075                                     * Check the parent class 
076                                     */
077    
078                                    if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
079                                            error(classElement, debug, "The event '" + classElement.getSimpleName() + "' has to be public.");
080                                            return true;
081                                    }
082    
083                                    /*
084                                     * Constructor 
085                                     */
086    
087                                    List<Element> constructors = getEnclosedElements(debug, classElement, ElementKind.CONSTRUCTOR);
088                                    debug.append("found " + constructors.size() + " constructors\n");
089    
090                                    if (constructors.isEmpty()) {
091                                            error(classElement, debug, "The event '" + classElement.getSimpleName() + "' needs to have an constructor.");
092                                    }
093    
094                                    if (constructors.size() > 1) {
095                                            error(classElement, debug, "The event '" + classElement.getSimpleName() + "' may only have one constructor.");
096                                    }
097    
098                                    Element elementConstructor = constructors.get(0);
099    
100                                    if (!elementConstructor.getModifiers().contains(Modifier.PUBLIC)) {
101                                            error(classElement, debug, "The event '" + classElement.getSimpleName() + "' has to have a public constructor.");
102                                    }
103    
104                                    /*
105                                     * Get constructor data 
106                                     */
107    
108                                    MethodData constructorData = getMethodData(elementConstructor);
109                                    debug.append("constructor-data-return-type:'" + constructorData.getReturnType() + "'\n");
110                                    for (TypeMirror paraElemtn : constructorData.getParameterTypes()) {
111                                            debug.append("constructor-data-parameter:" + paraElemtn + "\n");
112                                    }
113                                    for (Modifier modifier : elementConstructor.getModifiers()) {
114                                            debug.append("constructor-modifier:" + modifier.name() + "\n");
115                                    }
116    
117                                    /*
118                                     * Check for illegal constructor parameter types
119                                     */
120    
121                                    Object illegalParameterType = getIllegalConstructorParameterType(constructorData.getParameterTypes());
122                                    if (illegalParameterType != null) {
123                                            error(classElement, debug, "The constructor parameter type '" + illegalParameterType.toString() + "' is illegal .");
124                                            return true;
125                                    }
126    
127                                    /*
128                                     * Check the call method
129                                     * But only if the class is not abstract
130                                     */
131    
132                                    if (!classElement.getModifiers().contains(Modifier.ABSTRACT)) {
133    
134                                            List<Element> methods = getEnclosedElements(debug, classElement, ElementKind.METHOD);
135                                            List<Element> methodsCall = new ArrayList<Element>();
136    
137                                            for (Element method : methods) {
138                                                    debug.append("method:" + method.getSimpleName() + ":" + method.getKind() + "\n");
139                                                    if ("call".equals(method.getSimpleName().toString())) {
140                                                            methodsCall.add(method);
141                                                    }
142                                            }
143    
144                                            if (methodsCall.isEmpty()) {
145                                                    error(classElement, debug, "The event '" + classElement.getSimpleName() + "' needs to have a public and static method namned 'call'.");
146                                            }
147    
148                                            if (methodsCall.size() > 1) {
149                                                    error(classElement, debug, "The event '" + classElement.getSimpleName() + "' may only have method called 'call'.");
150                                            }
151    
152                                            Element elementCallMethod = methodsCall.get(0);
153    
154                                            if (!elementCallMethod.getModifiers().contains(Modifier.PUBLIC) && !elementCallMethod.getModifiers().contains(Modifier.STATIC)) {
155                                                    error(classElement, debug, "The 'call' method has to be both public and static.");
156                                            }
157    
158                                            /*
159                                             * Get method data
160                                             */
161    
162                                            MethodData methodData = getMethodData(elementCallMethod);
163                                            debug.append("method-data-return-type1:'" + methodData.getReturnType() + "'\n");
164                                            debug.append("method-data-return-type2:'" + methodData.getReturnType().toString() + "'\n");
165                                            for (TypeMirror paraElemtn : methodData.getParameterTypes()) {
166                                                    debug.append("method-data-parameter1:" + paraElemtn + "\n");
167                                            }
168    
169                                            /*
170                                             * Check the return type
171                                             */
172    
173                                            if (!"com.osbcp.apbs.AjaxPostBackCaller".equals(methodData.getReturnType().toString())) {
174                                                    error(classElement, debug, "The 'call' method has to return the 'com.osbcp.apbs.AjaxPostBackCaller' type.");
175                                            }
176    
177                                            /* 
178                                             * Check the parameters
179                                             */
180    
181                                            if (constructorData.getParameterTypes().size() != methodData.getParameterTypes().size()) {
182                                                    error(classElement, debug, getErrorMessageHasToHaveParameters(elementCallMethod, constructorData.getParameterTypes()) + " Incorrect number of parameters.");
183                                                    return true;
184                                            }
185    
186                                            // Check the parameters
187                                            for (int i = 0; i < methodData.getParameterTypes().size(); i++) {
188    
189                                                    String methodParameterType = methodData.getParameterTypes().get(i).toString();
190                                                    String constructorParameterType = constructorData.getParameterTypes().get(i).toString();
191    
192                                                    boolean isSameParameterType = methodParameterType.equals(constructorParameterType);
193                                                    boolean isJavaScriptType = "com.osbcp.jjsw.JavaScript".equals(methodParameterType);
194    
195                                                    debug.append("checking-parameter-" + i + ":" + constructorParameterType + "==" + methodParameterType + "?\n");
196                                                    debug.append("isSameParameterType=" + isSameParameterType + "\n");
197                                                    debug.append("isJavaScriptType=" + isJavaScriptType + "\n");
198    
199                                                    if (!isSameParameterType && !isJavaScriptType) {
200                                                            error(classElement, debug, getErrorMessageHasToHaveParameters(elementCallMethod, constructorData.getParameterTypes()));
201                                                            return true;
202                                                    }
203    
204                                            }
205    
206                                    }
207    
208                            }
209    
210                    } catch (final Throwable e) {
211                            processingEnv.getMessager().printMessage(Kind.ERROR, e.toString());
212                    }
213    
214                    return true;
215    
216            }
217    
218            /**
219             * Checks a list of parameter types for any illegal types used for a constructor.
220             * 
221             * @param parameterTypes The parameter types for a constructor.
222             * @return Any illegal parameter type.
223             */
224    
225            private Object getIllegalConstructorParameterType(final List<? extends TypeMirror> parameterTypes) {
226    
227                    List<String> legalTypes = new ArrayList<String>();
228                    legalTypes.add("java.lang.String");
229                    legalTypes.add("java.lang.Integer");
230                    legalTypes.add("int");
231                    legalTypes.add("java.lang.Byte");
232                    legalTypes.add("byte");
233                    legalTypes.add("java.lang.Short");
234                    legalTypes.add("short");
235                    legalTypes.add("java.lang.Long");
236                    legalTypes.add("long");
237                    legalTypes.add("java.lang.Float");
238                    legalTypes.add("float");
239                    legalTypes.add("java.lang.Double");
240                    legalTypes.add("double");
241                    legalTypes.add("java.lang.Boolean");
242                    legalTypes.add("boolean");
243                    legalTypes.add("java.lang.Character");
244                    legalTypes.add("character");
245    
246                    for (Object parameter : parameterTypes) {
247    
248                            if (!legalTypes.contains(parameter.toString())) {
249                                    return parameter;
250                            }
251    
252                    }
253    
254                    // No illegal type
255                    return null;
256    
257            }
258    
259            /**
260             * Get a fancy error message for incorrect parameters.
261             * 
262             * @param elementCallMethod The method element.
263             * @param list List of method parameter types.
264             * @return A fancy error message for incorrect parameters.
265             */
266            private String getErrorMessageHasToHaveParameters(final Element elementCallMethod, final List<? extends TypeMirror> list) {
267                    if (list.isEmpty()) {
268                            return "The method '" + elementCallMethod.getSimpleName() + "' should not have any parameters.";
269                    } else {
270                            return "The method '" + elementCallMethod.getSimpleName() + "' has to have the parameters '" + StringUtil.toString(list) + "' or switch any parameter to a 'com.osbcp.jjsw.JavaScript' type.";
271                    }
272            }
273    
274            /**
275             * Displays the error message, uses the DEBUG variable to determine if the debug message should be displayed as well.
276             * 
277             * @param element The element which caused the error.
278             * @param debug The Debug string.
279             * @param message The error message.
280             */
281    
282            private void error(final Element element, final StringBuilder debug, final String message) {
283    
284                    if (DEBUG) {
285                            processingEnv.getMessager().printMessage(Kind.ERROR, debug.toString() + "\n\n" + message, element);
286                    } else {
287                            processingEnv.getMessager().printMessage(Kind.ERROR, message, element);
288                    }
289    
290            }
291    
292            /**
293             * Returns method data of a method element.
294             * 
295             * @param elementMethod The method element to check. 
296             * @return Method data of a method element.
297             */
298    
299            private MethodData getMethodData(final Element elementMethod) {
300    
301                    MethodData data = new MethodData();
302    
303                    TypeMirror mirror = elementMethod.asType();
304                    mirror.accept(visitor(data), null);
305    
306                    return data;
307    
308            }
309    
310            /**
311             * Method visitor
312             * @param data Method data that should be populated with data.
313             * @return A visitor
314             */
315    
316            private TypeVisitor<Boolean, Void> visitor(final MethodData data) {
317    
318                    return new SimpleTypeVisitor6<Boolean, Void>() {
319    
320                            public Boolean visitExecutable(ExecutableType t, Void v) {
321                                    data.setReturnType(t.getReturnType());
322                                    data.setParameterTypes(t.getParameterTypes());
323                                    return true;
324                            }
325    
326                    };
327    
328            }
329    
330            /**
331             * Returns a list of enclosed elements of an element.
332             * 
333             * @param debug The debug string.
334             * @param element The parent element. 
335             * @param elementKind The element kind.
336             * @return A list of enclosed elements of an element.
337             */
338    
339            private List<Element> getEnclosedElements(final StringBuilder debug, final Element element, final ElementKind elementKind) {
340    
341                    List<Element> list = new ArrayList<Element>();
342    
343                    for (Element enclosedElement : element.getEnclosedElements()) {
344    
345                            if (enclosedElement.getKind() == elementKind) {
346                                    debug.append("adding " + elementKind + ":" + enclosedElement.getSimpleName() + ":" + enclosedElement.getKind() + "\n");
347                                    list.add(enclosedElement);
348                            }
349    
350                    }
351    
352                    return list;
353    
354            }
355    
356    }