View Javadoc
1   /*
2    * The MIT License
3    * Copyright © 2004-2014 Fabrizio Giustina
4    * Copyright © 2022-2022 Web-Legacy
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in
14   * all copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22   * THE SOFTWARE.
23   */
24  package net.sf.maventaglib;
25  
26  import java.lang.reflect.Array;
27  import java.lang.reflect.Method;
28  import java.text.MessageFormat;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Locale;
32  
33  import org.apache.commons.beanutils.PropertyUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.doxia.sink.Sink;
36  import org.apache.maven.plugin.logging.Log;
37  
38  import net.sf.maventaglib.checker.ELFunction;
39  import net.sf.maventaglib.checker.Tag;
40  import net.sf.maventaglib.checker.TagAttribute;
41  import net.sf.maventaglib.checker.Tld;
42  
43  
44  /**
45   * Validates tag handler classes fount in tlds.
46   * @author Fabrizio Giustina
47   * @version $Revision $ ($Author $)
48   */
49  public class ValidateRenderer extends AbstractMavenTaglibReportRenderer
50  {
51  
52      private static final int ICO_SUCCESS = 0;
53  
54      private static final int ICO_INFO = 1;
55  
56      private static final int ICO_WARNING = 2;
57  
58      private static final int ICO_ERROR = 3;
59  
60      private static final String IMAGE_ERROR_SRC = Messages.getString("Validate.image.error"); //$NON-NLS-1$
61  
62      private static final String IMAGE_WARNING_SRC = Messages.getString("Validate.image.warning"); //$NON-NLS-1$
63  
64      private static final String IMAGE_INFO_SRC = Messages.getString("Validate.image.info"); //$NON-NLS-1$
65  
66      private static final String IMAGE_SUCCESS_SRC = Messages.getString("Validate.image.success"); //$NON-NLS-1$
67  
68      /**
69       * list of Tld to check.
70       */
71      private Tld[] tlds;
72  
73      private Log log;
74  
75      private ClassLoader projectClassLoader;
76  
77      /**
78       * javax.servlet.jsp.tagext.TagSupport class loaded using the project classloader.
79       */
80      private Class<?> tagSupportClass;
81  
82      /**
83       * javax.servlet.jsp.tagext.TagExtraInfo class loaded using the project classloader.
84       */
85      private Class<?> tagExtraInfoClass;
86  
87      /**
88       * javax.servlet.jsp.tagext.SimpleTag class loaded using the project classloader.
89       */
90      private Class<?> simpleTagClass;
91  
92      /**
93       * Class-Constructor
94       *
95       * @param sink the sink to use.
96       * @param locale the wanted locale to return the report's description, could be <code>null</code>.
97       * @param tlds list of TLDs to check.
98       * @param log the logger that has been injected into this mojo.
99       * @param projectClassLoader ClassLoader for all compile-classpaths
100      */
101     public ValidateRenderer(Sink sink, Locale locale, Tld[] tlds, Log log, ClassLoader projectClassLoader)
102     {
103         super(sink, locale);
104         this.tlds = tlds;
105         this.log = log;
106         this.projectClassLoader = projectClassLoader;
107 
108         try
109         {
110             tagSupportClass = Class.forName("javax.servlet.jsp.tagext.TagSupport", true, this.projectClassLoader); //$NON-NLS-1$
111         }
112         catch (ClassNotFoundException e)
113         {
114             log.error(Messages.getString("Validate.error.unabletoload.TagSupport")); //$NON-NLS-1$
115         }
116         try
117         {
118             tagExtraInfoClass = Class.forName("javax.servlet.jsp.tagext.TagExtraInfo", true, this.projectClassLoader); //$NON-NLS-1$
119         }
120         catch (ClassNotFoundException e)
121         {
122             log.error(Messages.getString("Validate.error.unabletoload.TagExtraInfo")); //$NON-NLS-1$
123         }
124         try
125         {
126             simpleTagClass = Class.forName("javax.servlet.jsp.tagext.SimpleTag", true, this.projectClassLoader); //$NON-NLS-1$
127         }
128         catch (ClassNotFoundException e)
129         {
130             log.debug(Messages.getString("Validate.error.unabletoload.SimpleTag")); //$NON-NLS-1$
131         }
132 
133     }
134 
135     /**
136      * @see org.apache.maven.reporting.AbstractMavenReportRenderer#getTitle()
137      */
138     @Override
139     public String getTitle()
140     {
141         return getMessageString("Validate.title"); //$NON-NLS-1$
142     }
143 
144     /**
145      * Check the given tld. Assure that:
146      * <ul>
147      * <li>Any tag class is loadable</li>
148      * <li>the tag class has a setter for any of the declared attribute</li>
149      * <li>the type declared in the dtd for an attribute (if any) matches the type accepted by the getter</li>
150      * </ul>
151      * @see org.apache.maven.reporting.AbstractMavenReportRenderer#renderBody()
152      */
153     @Override
154     protected void renderBody()
155     {
156         sink.body();
157         startSection(getMessageString("Validate.h1")); //$NON-NLS-1$
158         paragraph(getMessageString("Validate.into1")); //$NON-NLS-1$
159         paragraph(getMessageString("Validate.intro2")); //$NON-NLS-1$
160 
161         sink.list();
162         for (Tld tld : tlds)
163         {
164 
165             sink.listItem();
166             sink.link("#" + tld.getFilename()); //$NON-NLS-1$
167             sink.text(MessageFormat.format(getMessageString("Validate.listitem.tld"), //$NON-NLS-1$
168                 StringUtils.defaultIfEmpty(tld.getName(), tld.getShortname()), tld.getFilename() ));
169             sink.link_();
170             sink.text(getMessageString("Validate.listitem.uri") + tld.getUri()); //$NON-NLS-1$
171 
172             sink.listItem_();
173         }
174         sink.list_();
175 
176         endSection();
177 
178         for (Tld tld : tlds)
179         {
180             checkTld(tld);
181         }
182 
183         sink.body_();
184     }
185 
186     /**
187      * Checks a single tld and returns validation results.
188      * @param tld Tld
189      */
190     private void checkTld(Tld tld)
191     {
192         // new section for each tld
193         sink.anchor(tld.getFilename());
194         sink.anchor_();
195         startSection(StringUtils.defaultIfEmpty(tld.getName(), tld.getShortname()) + " " + tld.getFilename()); //$NON-NLS-1$
196 
197         doTags(tld.getTags(), tld.getShortname());
198         doFunctions(tld.getFunctions(), tld.getShortname());
199 
200         endSection();
201     }
202 
203     /**
204      * @param tld
205      */
206     private void doTags(Tag[] tags, String shortname)
207     {
208         if (tags != null && tags.length > 0)
209         {
210             for (Tag tldItem : tags)
211             {
212                 checkTag(shortname, tldItem);
213             }
214         }
215     }
216 
217     private void doFunctions(ELFunction[] tags, String shortname)
218     {
219         if (tags != null && tags.length > 0)
220         {
221 
222             startSection("EL functions");
223 
224             startTable();
225 
226             tableHeader(new String[]{
227                 getMessageString("Validate.header.validated"), "function", getMessageString("Validate.header.class"), getMessageString("Validate.header.signature") }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
228 
229             for (ELFunction tldItem : tags)
230             {
231                 checkFunction(shortname, tldItem);
232             }
233 
234             endTable();
235 
236             endSection();
237         }
238     }
239 
240     /**
241      * @param shortname
242      * @param tldItem
243      */
244     private void checkFunction(String prefix, ELFunction tag)
245     {
246 
247         String className = tag.getFunctionClass();
248 
249         boolean found = true;
250 
251         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
252         Thread.currentThread().setContextClassLoader(this.projectClassLoader);
253 
254         try
255         {
256             Class<?> functionClass = Class.forName(className, true, this.projectClassLoader);
257 
258             String fullSignature = tag.getFunctionSignature();
259             String paramsString = tag.getParameters();
260             String returnvalue = null;
261 
262             String methodName = StringUtils.trim(StringUtils.substringBefore(fullSignature, "("));
263             if (StringUtils.contains(methodName, " "))
264             {
265                 returnvalue = StringUtils.substringBefore(methodName, " ");
266                 methodName = StringUtils.substringAfter(methodName, " ");
267             }
268 
269             String[] params = StringUtils.split(paramsString, ",");
270 
271             List<Class<?>> parClasses = new ArrayList<>(params.length);
272 
273             for (String stringClass : params)
274             {
275                 parClasses.add(Class.forName(StringUtils.trim(stringClass), true, this.projectClassLoader));
276             }
277 
278             Method method = functionClass.getMethod(methodName, parClasses.toArray(new Class<?>[0]));
279 
280             Class< ? > returnType = method.getReturnType();
281 
282             if (!(returnvalue == null || returnType.getCanonicalName().equals(returnvalue)))
283             {
284                 found = false;
285             }
286         }
287         catch (Throwable e)
288         {
289             found = false;
290         }
291 
292         Thread.currentThread().setContextClassLoader(currentClassLoader);
293 
294         sink.tableRow();
295 
296         sink.tableCell();
297         figure(found ? ICO_SUCCESS : ICO_ERROR);
298         sink.tableCell_();
299 
300         tableCell(prefix + ":" + tag.getName() + "()");
301         tableCell(className);
302         tableCell(tag.getFunctionSignature());
303 
304         sink.tableRow_();
305 
306     }
307 
308     /**
309      * Checks a single tag and returns validation results.
310      * @param tag Tag
311      */
312     private void checkTag(String prefix, Tag tag)
313     {
314 
315         // new subsection for each tag
316         startSection("<" + prefix + ":" + tag.getName() + ">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
317 
318         String className = tag.getTagClass();
319 
320         startTable();
321 
322         tableHeader(new String[]{
323             getMessageString("Validate.header.found"), getMessageString("Validate.header.loadable"), getMessageString("Validate.header.extends"), getMessageString("Validate.header.class") }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
324 
325         boolean found = true;
326         boolean loadable = true;
327         boolean extend = true;
328 
329         Object tagObject = null;
330         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
331         Thread.currentThread().setContextClassLoader(this.projectClassLoader);
332 
333         try
334         {
335             Class<?> tagClass = Class.forName(className, true, this.projectClassLoader);
336 
337             // extend only true, if tagClass derives from TagSupport or derives from SimpleTag
338             extend = tagSupportClass.isAssignableFrom(tagClass)
339                 || simpleTagClass != null && simpleTagClass.isAssignableFrom(tagClass);
340 
341             try
342             {
343                 tagObject = tagClass.getDeclaredConstructor().newInstance();
344             }
345             catch (Throwable e)
346             {
347                 loadable = false;
348             }
349 
350         }
351         catch (ClassNotFoundException e)
352         {
353             found = false;
354             loadable = false;
355             extend = false;
356         }
357         catch (NoClassDefFoundError e)
358         {
359             found = false;
360             loadable = false;
361             extend = false;
362         }
363 
364         Thread.currentThread().setContextClassLoader(currentClassLoader);
365 
366         TagAttribute[] attributes = tag.getAttributes();
367 
368         sink.tableRow();
369 
370         sink.tableCell();
371         figure(found ? ICO_SUCCESS : ICO_ERROR);
372         sink.tableCell_();
373 
374         sink.tableCell();
375         figure(loadable ? ICO_SUCCESS : ICO_ERROR);
376         sink.tableCell_();
377 
378         sink.tableCell();
379         figure(extend ? ICO_SUCCESS : ICO_ERROR);
380         sink.tableCell_();
381 
382         tableCell(className);
383 
384         sink.tableRow_();
385 
386         if (tag.getTeiClass() != null)
387         {
388             checkTeiClass(tag.getTeiClass());
389         }
390 
391         endTable();
392 
393         if (tagObject != null && attributes.length > 0)
394         {
395 
396             startTable();
397             tableHeader(new String[]{
398                 StringUtils.EMPTY,
399                 getMessageString("Validate.header.attributename"), getMessageString("Validate.header.tlddeclares"), getMessageString("Validate.header.tagdeclares") }); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
400 
401             for (TagAttribute attribute : attributes)
402             {
403                 checkAttribute(tagObject, attribute);
404             }
405 
406             endTable();
407         }
408         endSection();
409     }
410 
411     /**
412      * Check a declared TagExtraInfo class.
413      * @param className TEI class name
414      */
415     private void checkTeiClass(String className)
416     {
417 
418         boolean found = true;
419         boolean loadable = true;
420         boolean extend = true;
421 
422         Class<?> teiClass = null;
423         try
424         {
425             teiClass = Class.forName(className, true, this.projectClassLoader);
426 
427             if (tagExtraInfoClass == null || !tagExtraInfoClass.isAssignableFrom(teiClass))
428             {
429                 extend = false;
430             }
431 
432             try
433             {
434                 teiClass.getDeclaredConstructor().newInstance();
435             }
436             catch (Throwable e)
437             {
438                 loadable = false;
439             }
440         }
441         catch (ClassNotFoundException e)
442         {
443             found = false;
444             loadable = false;
445             extend = false;
446         }
447 
448         catch (NoClassDefFoundError e)
449         {
450             found = false;
451             loadable = false;
452             extend = false;
453         }
454 
455         sink.tableRow();
456 
457         sink.tableCell();
458         figure(found ? ICO_SUCCESS : ICO_ERROR);
459         sink.tableCell_();
460 
461         sink.tableCell();
462         figure(loadable ? ICO_SUCCESS : ICO_ERROR);
463         sink.tableCell_();
464 
465         sink.tableCell();
466         figure(extend ? ICO_SUCCESS : ICO_ERROR);
467         sink.tableCell_();
468 
469         sink.tableCell();
470         sink.text(className);
471         sink.tableCell_();
472 
473         sink.tableRow_();
474     }
475 
476     /**
477      * Checks a single attribute and returns validation results.
478      * @param tag tag handler instance
479      * @param attribute TagAttribute
480      */
481     private void checkAttribute(Object tag, TagAttribute attribute)
482     {
483 
484         String tldType = attribute.getType();
485         String tldName = attribute.getName();
486         Class<?> tagType = null;
487         String tagTypeName = null;
488 
489         List<ValidationError> validationErrors = new ArrayList<>(3);
490 
491         if (!PropertyUtils.isWriteable(tag, tldName))
492         {
493             validationErrors.add(new ValidationError(ValidationError.LEVEL_ERROR,
494                 getMessageString("Validate.error.setternotfound"))); //$NON-NLS-1$
495         }
496 
497         // don't check if setter is missing
498         if (validationErrors.isEmpty())
499         {
500 
501             try
502             {
503                 tagType = PropertyUtils.getPropertyType(tag, tldName);
504             }
505             catch (Throwable e)
506             {
507                 // should never happen, since we already checked the writable property
508                 log.warn(e);
509             }
510             tagTypeName = tagType == null ? StringUtils.EMPTY : tagType.getName();
511 
512             if (tldType != null && tagType != null)
513             {
514                 Class<?> tldTypeClass = getClassFromName(tldType);
515 
516                 if (!tagType.isAssignableFrom(tldTypeClass))
517                 {
518 
519                     validationErrors.add(new ValidationError(ValidationError.LEVEL_ERROR, MessageFormat.format(
520                         getMessageString("Validate.error.attributetypemismatch"), //$NON-NLS-1$
521                         tldType, tagType.getName() )));
522                 }
523             }
524         }
525 
526         // don't check if we already know type is different
527         if (validationErrors.isEmpty())
528         {
529 
530             if (tldType != null && !tldType.equals(tagType.getName()))
531             {
532                 validationErrors.add(new ValidationError(ValidationError.LEVEL_WARNING, MessageFormat.format(
533                     getMessageString("Validate.error.attributetypeinexactmatch"), //$NON-NLS-1$
534                     tldType, tagType.getName() )));
535             }
536             else if (tldType == null && !String.class.equals(tagType))
537             {
538                 validationErrors.add(new ValidationError(ValidationError.LEVEL_INFO,
539                     getMessageString("Validate.error.attributetype"))); //$NON-NLS-1$
540             }
541         }
542 
543         sink.tableRow();
544 
545         sink.tableCell();
546 
547         int figure = ICO_SUCCESS;
548 
549         for (ValidationError error : validationErrors)
550         {
551             if (error.getLevel() == ValidationError.LEVEL_ERROR)
552             {
553                 figure = ICO_ERROR;
554             }
555             else if (figure == ICO_SUCCESS) // warning
556             {
557                 figure = ICO_WARNING;
558             }
559 
560         }
561 
562         figure(figure);
563         sink.tableCell_();
564 
565         sink.tableCell();
566         sink.text(tldName);
567 
568         for (ValidationError error : validationErrors)
569         {
570             sink.lineBreak();
571             if (error.getLevel() == ValidationError.LEVEL_ERROR)
572             {
573                 sink.bold();
574             }
575             sink.text(error.getText());
576             if (error.getLevel() == ValidationError.LEVEL_ERROR)
577             {
578                 sink.bold_();
579 
580             }
581         }
582 
583         sink.tableCell_();
584 
585         sink.tableCell();
586         if (tldType != null)
587         {
588             sink.text(StringUtils.substringAfter(tldType, "java.lang.")); //$NON-NLS-1$
589         }
590         sink.tableCell_();
591 
592         tableCell(StringUtils.substringAfter(tagTypeName, "java.lang.")); //$NON-NLS-1$
593 
594         sink.tableRow_();
595 
596     }
597 
598     private void figure(int type)
599     {
600         String text;
601         String src;
602 
603         switch (type)
604         {
605             case ICO_ERROR :
606                 text = getMessageString("Validate.level.error"); //$NON-NLS-1$
607                 src = IMAGE_ERROR_SRC;
608                 break;
609             case ICO_WARNING :
610                 text = getMessageString("Validate.level.warning"); //$NON-NLS-1$
611                 src = IMAGE_WARNING_SRC;
612                 break;
613             case ICO_INFO :
614                 text = getMessageString("Validate.level.info"); //$NON-NLS-1$
615                 src = IMAGE_INFO_SRC;
616                 break;
617             default :
618                 text = getMessageString("Validate.level.success"); //$NON-NLS-1$
619                 src = IMAGE_SUCCESS_SRC;
620                 break;
621         }
622 
623         sink.figure();
624         sink.figureGraphics(src);
625         sink.figureCaption();
626         sink.text(text);
627         sink.figureCaption_();
628         sink.figure_();
629     }
630 
631     /**
632      * returns a class from its name, handling primitives.
633      * @param className clss name
634      * @return Class istantiated using Class.forName or the matching primitive.
635      */
636     private Class<?> getClassFromName(String className)
637     {
638 
639         Class<?> tldTypeClass = tryGettingPrimitiveClass(className);
640 
641         if (tldTypeClass == null)
642         {
643             // not a primitive type
644             try
645             {
646                 if (isArrayClassName(className))
647                 {
648                     tldTypeClass = getArrayClass(className);
649                 }
650                 else
651                 {
652                     tldTypeClass = Class.forName(className, true, this.projectClassLoader);
653                 }
654             }
655             catch (ClassNotFoundException e)
656             {
657                 log.error(MessageFormat.format(Messages.getString("Validate.error.unabletofindclass"), //$NON-NLS-1$
658                     className ));
659             }
660         }
661         return tldTypeClass;
662     }
663 
664     private Class<?> tryGettingPrimitiveClass(String className)
665     {
666         if ("int".equals(className)) //$NON-NLS-1$
667         {
668             return int.class;
669         }
670         if ("long".equals(className)) //$NON-NLS-1$
671         {
672             return long.class;
673         }
674         if ("double".equals(className)) //$NON-NLS-1$
675         {
676             return double.class;
677         }
678         if ("boolean".equals(className)) //$NON-NLS-1$
679         {
680             return boolean.class;
681         }
682         if ("char".equals(className)) //$NON-NLS-1$
683         {
684             return char.class;
685         }
686         if ("byte".equals(className)) //$NON-NLS-1$
687         {
688             return byte.class;
689         }
690 
691         return null;
692     }
693 
694     private boolean isArrayClassName(String className)
695     {
696         return className.endsWith("[]");
697     }
698 
699     private Class<?> getArrayClass(String className) throws ClassNotFoundException
700     {
701         String elementClassName = StringUtils.replace(className, "[]", "");
702         Class<?> elementClass = tryGettingPrimitiveClass(elementClassName);
703         if (elementClass == null)
704         {
705             elementClass = Class.forName(elementClassName);
706         }
707         return Array.newInstance(elementClass, 0).getClass();
708     }
709 
710     static class ValidationError
711     {
712 
713         public static final int LEVEL_INFO = 1;
714 
715         public static final int LEVEL_WARNING = 2;
716 
717         public static final int LEVEL_ERROR = 3;
718 
719         private int level;
720 
721         private String text;
722 
723         /**
724          * @param level
725          * @param text
726          */
727         public ValidationError(int level, String text)
728         {
729             this.level = level;
730             this.text = text;
731         }
732 
733         /**
734          * Getter for <code>level</code>.
735          * @return Returns the level.
736          */
737         public int getLevel()
738         {
739             return this.level;
740         }
741 
742         /**
743          * Setter for <code>level</code>.
744          * @param level The level to set.
745          */
746         public void setLevel(int level)
747         {
748             this.level = level;
749         }
750 
751         /**
752          * Getter for <code>text</code>.
753          * @return Returns the text.
754          */
755         public String getText()
756         {
757             return this.text;
758         }
759 
760         /**
761          * Setter for <code>text</code>.
762          * @param text The text to set.
763          */
764         public void setText(String text)
765         {
766             this.text = text;
767         }
768     }
769 
770 }