View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.net.URI;
25  import java.util.Arrays;
26  import java.util.List;
27  import java.util.regex.Pattern;
28  
29  import org.apache.commons.beanutils.ConversionException;
30  import org.apache.commons.beanutils.ConvertUtilsBean;
31  import org.apache.commons.beanutils.Converter;
32  import org.junit.jupiter.api.Test;
33  
34  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
35  import com.puppycrawl.tools.checkstyle.api.Scope;
36  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
37  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
38  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
39  
40  public class AbstractAutomaticBeanTest {
41      @Test
42      public void testConfigureNoSuchAttribute() {
43          final TestBean testBean = new TestBean();
44          final DefaultConfiguration conf = new DefaultConfiguration("testConf");
45          conf.addProperty("NonExistent", "doesn't matter");
46          try {
47              testBean.configure(conf);
48              assertWithMessage("Exception is expected")
49                      .fail();
50          }
51          catch (CheckstyleException exc) {
52              assertWithMessage("Exceptions cause should be null")
53                      .that(exc)
54                      .hasCauseThat()
55                      .isNull();
56              assertWithMessage("Invalid exception message")
57                      .that(exc)
58                      .hasMessageThat()
59                      .isEqualTo("Property 'NonExistent' does not exist,"
60                              + " please check the documentation");
61          }
62      }
63  
64      @Test
65      public void testConfigureNoSuchAttribute2() {
66          final TestBean testBean = new TestBean();
67          final DefaultConfiguration conf = new DefaultConfiguration("testConf");
68          conf.addProperty("privateField", "doesn't matter");
69          try {
70              testBean.configure(conf);
71              assertWithMessage("Exception is expected")
72                      .fail();
73          }
74          catch (CheckstyleException exc) {
75              assertWithMessage("Exceptions cause should be null")
76                      .that(exc)
77                      .hasCauseThat()
78                      .isNull();
79              assertWithMessage("Invalid exception message")
80                      .that(exc)
81                      .hasMessageThat()
82                      .isEqualTo("Property 'privateField' does not exist,"
83                              + " please check the documentation");
84          }
85      }
86  
87      @Test
88      public void testSetupChildFromBaseClass() throws CheckstyleException {
89          final TestBean testBean = new TestBean();
90          testBean.configure(new DefaultConfiguration("bean config"));
91          testBean.setupChild(null);
92          try {
93              testBean.setupChild(new DefaultConfiguration("dummy"));
94              assertWithMessage("Exception is expected")
95                      .fail();
96          }
97          catch (CheckstyleException exc) {
98              final String expectedMessage = "dummy is not allowed as a child in bean config. "
99                      + "Please review 'Parent Module' section for this Check"
100                     + " in web documentation if Check is standard.";
101             assertWithMessage("Invalid exception message")
102                     .that(exc)
103                     .hasMessageThat()
104                     .isEqualTo(expectedMessage);
105         }
106     }
107 
108     @Test
109     public void testSetupInvalidChildFromBaseClass() {
110         final TestBean testBean = new TestBean();
111         final DefaultConfiguration parentConf = new DefaultConfiguration("parentConf");
112         final DefaultConfiguration childConf = new DefaultConfiguration("childConf");
113         TestUtil.setInternalState(testBean, "configuration", parentConf);
114 
115         try {
116             testBean.setupChild(childConf);
117             assertWithMessage("expecting checkstyle exception")
118                     .fail();
119         }
120         catch (CheckstyleException exc) {
121             assertWithMessage("Invalid exception message")
122                     .that(exc)
123                     .hasMessageThat()
124                     .isEqualTo("childConf is not allowed as a "
125                             + "child in parentConf. Please review 'Parent Module' section "
126                             + "for this Check in web documentation if Check is standard.");
127         }
128     }
129 
130     @Test
131     public void testContextualizeInvocationTargetException() {
132         final TestBean testBean = new TestBean();
133         final DefaultContext context = new DefaultContext();
134         context.add("exceptionalMethod", 123.0f);
135         try {
136             testBean.contextualize(context);
137             assertWithMessage("InvocationTargetException is expected")
138                     .fail();
139         }
140         catch (CheckstyleException exc) {
141             final String expected = "Cannot set property 'exceptionalMethod' to '123.0'";
142             assertWithMessage("Invalid exception cause, should be: ReflectiveOperationException")
143                     .that(exc)
144                     .hasCauseThat()
145                     .isInstanceOf(ReflectiveOperationException.class);
146             assertWithMessage("Invalid exception message, should start with: " + expected)
147                     .that(exc)
148                     .hasMessageThat()
149                     .isEqualTo(expected);
150         }
151     }
152 
153     @Test
154     public void testContextualizeConversionException() {
155         final TestBean testBean = new TestBean();
156         final DefaultContext context = new DefaultContext();
157         context.add("val", "some string");
158         try {
159             testBean.contextualize(context);
160             assertWithMessage("InvocationTargetException is expected")
161                     .fail();
162         }
163         catch (CheckstyleException exc) {
164             final String expected = "illegal value ";
165             assertWithMessage("Invalid exception cause, should be: ConversionException")
166                     .that(exc)
167                     .hasCauseThat()
168                     .isInstanceOf(ConversionException.class);
169             assertWithMessage("Invalid exception message, should start with: " + expected)
170                     .that(exc)
171                     .hasMessageThat()
172                     .startsWith(expected);
173         }
174     }
175 
176     @Test
177     public void testTestBean() {
178         final TestBean testBean = new TestBean();
179         testBean.setVal(0);
180         testBean.setWrong("wrongVal");
181         testBean.assignPrivateFieldSecretly(null);
182         try {
183             testBean.setExceptionalMethod("someValue");
184             assertWithMessage("exception expected")
185                     .fail();
186         }
187         catch (IllegalStateException exc) {
188             assertWithMessage("Invalid exception message")
189                     .that(exc)
190                     .hasMessageThat()
191                     .isEqualTo("null,wrongVal,0,someValue");
192         }
193     }
194 
195     @Test
196     public void testRegisterIntegralTypes() throws Exception {
197         final ConvertUtilsBeanStub convertUtilsBean = new ConvertUtilsBeanStub();
198         TestUtil.invokeStaticMethod(AbstractAutomaticBean.class,
199                 "registerIntegralTypes", convertUtilsBean);
200         assertWithMessage("Number of converters registered differs from expected")
201                 .that(convertUtilsBean.getRegisterCount())
202                 .isEqualTo(81);
203     }
204 
205     @Test
206     public void testBeanConverters() throws Exception {
207         final ConverterBean bean = new ConverterBean();
208 
209         // methods are not seen as used by reflection
210         bean.setStrings("BAD");
211         bean.setPattern(null);
212         bean.setSeverityLevel(null);
213         bean.setScope(null);
214         bean.setUri(null);
215         bean.setAccessModifiers(AccessModifierOption.PACKAGE);
216 
217         final DefaultConfiguration config = new DefaultConfiguration("bean");
218         config.addProperty("strings", "a, b, c");
219         config.addProperty("pattern", ".*");
220         config.addProperty("severityLevel", "error");
221         config.addProperty("scope", "public");
222         config.addProperty("uri", "http://github.com");
223         config.addProperty("accessModifiers", "public, private");
224         bean.configure(config);
225 
226         final String message = "invalid result";
227         assertWithMessage(message)
228                 .that(bean.strings)
229                 .asList()
230                 .containsExactly("a", "b", "c")
231                 .inOrder();
232         assertWithMessage(message)
233                 .that(bean.pattern.pattern())
234                 .isEqualTo(".*");
235         assertWithMessage(message)
236                 .that(bean.severityLevel)
237                 .isEqualTo(SeverityLevel.ERROR);
238         assertWithMessage(message)
239                 .that(bean.scope)
240                 .isEqualTo(Scope.PUBLIC);
241         assertWithMessage(message)
242                 .that(bean.uri)
243                 .isEqualTo(new URI("http://github.com"));
244         assertWithMessage(message)
245                 .that(bean.accessModifiers)
246                 .asList()
247                 .containsExactly(AccessModifierOption.PUBLIC, AccessModifierOption.PRIVATE)
248                 .inOrder();
249     }
250 
251     @Test
252     public void testBeanConvertersUri2() throws Exception {
253         final ConverterBean bean = new ConverterBean();
254         final DefaultConfiguration config = new DefaultConfiguration("bean");
255         config.addProperty("uri", "");
256         bean.configure(config);
257 
258         assertWithMessage("invalid result")
259                 .that(bean.uri)
260                 .isNull();
261     }
262 
263     @Test
264     public void testBeanConvertersUri3() {
265         final ConverterBean bean = new ConverterBean();
266         final DefaultConfiguration config = new DefaultConfiguration("bean");
267         config.addProperty("uri", "BAD");
268 
269         try {
270             bean.configure(config);
271             assertWithMessage("Exception is expected")
272                     .fail();
273         }
274         catch (CheckstyleException exc) {
275             assertWithMessage("Error message is not expected")
276                     .that(exc)
277                     .hasMessageThat()
278                     .isEqualTo("illegal value 'BAD' for property 'uri'");
279         }
280     }
281 
282     @Test
283     public void testBeanConverterPatternArray() throws Exception {
284         final ConverterBean bean = new ConverterBean();
285         final DefaultConfiguration config = new DefaultConfiguration("bean");
286         final String patternString = "^a*$  , ^b*$ , ^c*$ ";
287         final List<String> expectedPatternStrings = Arrays.asList("^a*$", "^b*$", "^c*$");
288         config.addProperty("patterns", patternString);
289         bean.configure(config);
290 
291         final List<String> actualPatternStrings = Arrays.stream(bean.patterns)
292                 .map(Pattern::pattern)
293                 .toList();
294 
295         assertWithMessage("invalid size of result")
296                 .that(bean.patterns)
297                 .hasLength(3);
298         assertWithMessage("invalid result")
299                 .that(actualPatternStrings)
300                 .containsExactlyElementsIn(expectedPatternStrings);
301     }
302 
303     @Test
304     public void testBeanConverterPatternArraySingleElement() throws Exception {
305         final ConverterBean bean = new ConverterBean();
306         final DefaultConfiguration config = new DefaultConfiguration("bean");
307         final String patternString = "^a*$";
308         final List<String> expectedPatternStrings = List.of("^a*$");
309         config.addProperty("patterns", patternString);
310         bean.configure(config);
311 
312         final List<String> actualPatternStrings = Arrays.stream(bean.patterns)
313                 .map(Pattern::pattern)
314                 .toList();
315 
316         assertWithMessage("invalid size of result")
317                 .that(bean.patterns)
318                 .hasLength(1);
319         assertWithMessage("invalid result")
320                 .that(actualPatternStrings)
321                 .containsExactlyElementsIn(expectedPatternStrings);
322     }
323 
324     @Test
325     public void testBeanConverterPatternArrayEmptyString() throws Exception {
326         final ConverterBean bean = new ConverterBean();
327         final DefaultConfiguration config = new DefaultConfiguration("bean");
328         config.addProperty("patterns", "");
329         bean.configure(config);
330 
331         assertWithMessage("invalid size of result")
332                 .that(bean.patterns)
333                 .hasLength(0);
334     }
335 
336     private static final class ConvertUtilsBeanStub extends ConvertUtilsBean {
337 
338         private int registerCount;
339 
340         @Override
341         public void register(Converter converter, Class<?> clazz) {
342             super.register(converter, clazz);
343             if (converter != null) {
344                 registerCount++;
345             }
346         }
347 
348         public int getRegisterCount() {
349             return registerCount;
350         }
351 
352     }
353 
354     public static final class TestBean extends AbstractAutomaticBean {
355 
356         private String privateField;
357 
358         private String wrong;
359 
360         private int val;
361 
362         public void setWrong(String wrong) {
363             this.wrong = wrong;
364         }
365 
366         public void setVal(int val) {
367             this.val = val;
368         }
369 
370         public void assignPrivateFieldSecretly(String input) {
371             privateField = input;
372         }
373 
374         public void setExceptionalMethod(String value) {
375             throw new IllegalStateException(privateField + "," + wrong + "," + val + "," + value);
376         }
377 
378         @Override
379         protected void finishLocalSetup() {
380             // No code by default
381         }
382 
383     }
384 
385     /**
386      * This class has to be public for reflection to access the methods.
387      */
388     public static class ConverterBean extends AbstractAutomaticBean {
389 
390         private String[] strings;
391         private Pattern pattern;
392         private SeverityLevel severityLevel;
393         private Scope scope;
394         private URI uri;
395         private AccessModifierOption[] accessModifiers;
396         private Pattern[] patterns;
397 
398         /**
399          * Setter for strings.
400          *
401          * @param strings strings.
402          */
403         public void setStrings(String... strings) {
404             this.strings = Arrays.copyOf(strings, strings.length);
405         }
406 
407         /**
408          * Setter for pattern.
409          *
410          * @param pattern pattern.
411          */
412         public void setPattern(Pattern pattern) {
413             this.pattern = pattern;
414         }
415 
416         /**
417          * Setter for severity level.
418          *
419          * @param severityLevel severity level.
420          */
421         public void setSeverityLevel(SeverityLevel severityLevel) {
422             this.severityLevel = severityLevel;
423         }
424 
425         /**
426          * Setter for scope.
427          *
428          * @param scope scope.
429          */
430         public void setScope(Scope scope) {
431             this.scope = scope;
432         }
433 
434         /**
435          * Setter for uri.
436          *
437          * @param uri uri.
438          */
439         public void setUri(URI uri) {
440             this.uri = uri;
441         }
442 
443         /**
444          * Setter for access modifiers.
445          *
446          * @param accessModifiers access modifiers.
447          */
448         public void setAccessModifiers(AccessModifierOption... accessModifiers) {
449             this.accessModifiers = Arrays.copyOf(accessModifiers,
450                     accessModifiers.length);
451         }
452 
453         /**
454          * Setter for patterns.
455          *
456          * @param patterns patterns
457          */
458         public void setPatterns(Pattern... patterns) {
459             this.patterns = Arrays.copyOf(patterns, patterns.length);
460         }
461 
462         @Override
463         protected void finishLocalSetup() {
464             // no code
465         }
466 
467     }
468 
469 }