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