Browse Source

ConversionService detects generic type declaration on target class behind proxy as well

Issue: SPR-14822
pull/1215/head
Juergen Hoeller 10 years ago
parent
commit
f7d740fa69
  1. 9
      spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java
  2. 120
      spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java
  3. 4
      spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java
  4. 6
      spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java
  5. 31
      spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java

9
spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java

@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
@ -97,9 +98,13 @@ public class FormattingConversionService extends GenericConversionService
static Class<?> getFieldType(Formatter<?> formatter) { static Class<?> getFieldType(Formatter<?> formatter) {
Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class);
if (fieldType == null && formatter instanceof DecoratingProxy) {
fieldType = GenericTypeResolver.resolveTypeArgument(
((DecoratingProxy) formatter).getDecoratedClass(), Formatter.class);
}
if (fieldType == null) { if (fieldType == null) {
throw new IllegalArgumentException("Unable to extract parameterized field type argument from Formatter [" + throw new IllegalArgumentException("Unable to extract the parameterized field type from Formatter [" +
formatter.getClass().getName() + "]; does the formatter parameterize the <T> generic type?"); formatter.getClass().getName() + "]; does the class parameterize the <T> generic type?");
} }
return fieldType; return fieldType;
} }

120
spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java

@ -32,7 +32,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.BeanUtils; import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -45,6 +45,7 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.Printer; import org.springframework.format.Printer;
@ -81,7 +82,7 @@ public class FormattingConversionServiceTests {
@Test @Test
public void testFormatFieldForTypeWithFormatter() throws ParseException { public void formatFieldForTypeWithFormatter() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
String formatted = formattingService.convert(3, String.class); String formatted = formattingService.convert(3, String.class);
assertEquals("3", formatted); assertEquals("3", formatted);
@ -90,7 +91,7 @@ public class FormattingConversionServiceTests {
} }
@Test @Test
public void testFormatFieldForTypeWithPrinterParserWithCoercion() throws ParseException { public void formatFieldForTypeWithPrinterParserWithCoercion() throws ParseException {
formattingService.addConverter(new Converter<DateTime, LocalDate>() { formattingService.addConverter(new Converter<DateTime, LocalDate>() {
@Override @Override
public LocalDate convert(DateTime source) { public LocalDate convert(DateTime source) {
@ -107,7 +108,7 @@ public class FormattingConversionServiceTests {
@Test @Test
@SuppressWarnings("resource") @SuppressWarnings("resource")
public void testFormatFieldForValueInjection() { public void formatFieldForValueInjection() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.registerBeanDefinition("valueBean", new RootBeanDefinition(ValueBean.class)); ac.registerBeanDefinition("valueBean", new RootBeanDefinition(ValueBean.class));
ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class)); ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class));
@ -118,7 +119,7 @@ public class FormattingConversionServiceTests {
@Test @Test
@SuppressWarnings("resource") @SuppressWarnings("resource")
public void testFormatFieldForValueInjectionUsingMetaAnnotations() { public void formatFieldForValueInjectionUsingMetaAnnotations() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
RootBeanDefinition bd = new RootBeanDefinition(MetaValueBean.class); RootBeanDefinition bd = new RootBeanDefinition(MetaValueBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
@ -140,20 +141,20 @@ public class FormattingConversionServiceTests {
} }
@Test @Test
public void testFormatFieldForAnnotation() throws Exception { public void formatFieldForAnnotation() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
doTestFormatFieldForAnnotation(Model.class, false); doTestFormatFieldForAnnotation(Model.class, false);
} }
@Test @Test
public void testFormatFieldForAnnotationWithDirectFieldAccess() throws Exception { public void formatFieldForAnnotationWithDirectFieldAccess() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
doTestFormatFieldForAnnotation(Model.class, true); doTestFormatFieldForAnnotation(Model.class, true);
} }
@Test @Test
@SuppressWarnings("resource") @SuppressWarnings("resource")
public void testFormatFieldForAnnotationWithPlaceholders() throws Exception { public void formatFieldForAnnotationWithPlaceholders() throws Exception {
GenericApplicationContext context = new GenericApplicationContext(); GenericApplicationContext context = new GenericApplicationContext();
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
Properties props = new Properties(); Properties props = new Properties();
@ -169,7 +170,7 @@ public class FormattingConversionServiceTests {
@Test @Test
@SuppressWarnings("resource") @SuppressWarnings("resource")
public void testFormatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception { public void formatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception {
GenericApplicationContext context = new GenericApplicationContext(); GenericApplicationContext context = new GenericApplicationContext();
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
Properties props = new Properties(); Properties props = new Properties();
@ -239,61 +240,61 @@ public class FormattingConversionServiceTests {
} }
@Test @Test
public void testPrintNull() throws ParseException { public void printNull() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertEquals("", formattingService.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class))); assertEquals("", formattingService.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
} }
@Test @Test
public void testParseNull() throws ParseException { public void parseNull() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService assertNull(formattingService
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); .convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test @Test
public void testParseEmptyString() throws ParseException { public void parseEmptyString() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test @Test
public void testParseBlankString() throws ParseException { public void parseBlankString() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService.convert(" ", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); assertNull(formattingService.convert(" ", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test(expected = ConversionFailedException.class) @Test(expected = ConversionFailedException.class)
public void testParseParserReturnsNull() throws ParseException { public void parseParserReturnsNull() throws ParseException {
formattingService.addFormatterForFieldType(Integer.class, new NullReturningFormatter()); formattingService.addFormatterForFieldType(Integer.class, new NullReturningFormatter());
assertNull(formattingService.convert("1", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); assertNull(formattingService.convert("1", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test(expected = ConversionFailedException.class) @Test(expected = ConversionFailedException.class)
public void testParseNullPrimitiveProperty() throws ParseException { public void parseNullPrimitiveProperty() throws ParseException {
formattingService.addFormatterForFieldType(Integer.class, new NumberStyleFormatter()); formattingService.addFormatterForFieldType(Integer.class, new NumberStyleFormatter());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class))); assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class)));
} }
@Test @Test
public void testPrintNullDefault() throws ParseException { public void printNullDefault() throws ParseException {
assertEquals(null, formattingService assertEquals(null, formattingService
.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class))); .convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
} }
@Test @Test
public void testParseNullDefault() throws ParseException { public void parseNullDefault() throws ParseException {
assertNull(formattingService assertNull(formattingService
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); .convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test @Test
public void testParseEmptyStringDefault() throws ParseException { public void parseEmptyStringDefault() throws ParseException {
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
} }
@Test @Test
public void testFormatFieldForAnnotationWithSubclassAsFieldType() throws Exception { public void formatFieldForAnnotationWithSubclassAsFieldType() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory() { formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory() {
@Override @Override
public Printer<?> getPrinter(org.springframework.format.annotation.DateTimeFormat annotation, Class<?> fieldType) { public Printer<?> getPrinter(org.springframework.format.annotation.DateTimeFormat annotation, Class<?> fieldType) {
@ -319,7 +320,7 @@ public class FormattingConversionServiceTests {
} }
@Test @Test
public void testRegisterDefaultValueViaFormatter() { public void registerDefaultValueViaFormatter() {
registerDefaultValue(Date.class, new Date()); registerDefaultValue(Date.class, new Date());
} }
@ -340,6 +341,45 @@ public class FormattingConversionServiceTests {
}); });
} }
@Test
public void introspectedFormatter() throws ParseException {
formattingService.addFormatter(new NumberStyleFormatter());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void proxiedFormatter() throws ParseException {
Formatter<?> formatter = new NumberStyleFormatter();
formattingService.addFormatter((Formatter<?>) new ProxyFactory(formatter).getProxy());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void introspectedConverter() {
formattingService.addConverter(new IntegerConverter());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void proxiedConverter() {
Converter<?, ?> converter = new IntegerConverter();
formattingService.addConverter((Converter<?, ?>) new ProxyFactory(converter).getProxy());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void introspectedConverterFactory() {
formattingService.addConverterFactory(new IntegerConverterFactory());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void proxiedConverterFactory() {
ConverterFactory<?, ?> converterFactory = new IntegerConverterFactory();
formattingService.addConverterFactory((ConverterFactory<?, ?>) new ProxyFactory(converterFactory).getProxy());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
public static class ValueBean { public static class ValueBean {
@ -348,6 +388,7 @@ public class FormattingConversionServiceTests {
public Date date; public Date date;
} }
public static class MetaValueBean { public static class MetaValueBean {
@MyDateAnn @MyDateAnn
@ -357,18 +398,21 @@ public class FormattingConversionServiceTests {
public Double number; public Double number;
} }
@Value("${myDate}") @Value("${myDate}")
@org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy") @org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy")
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public static @interface MyDateAnn { public @interface MyDateAnn {
} }
@Value("${myNumber}") @Value("${myNumber}")
@NumberFormat(style = NumberFormat.Style.PERCENT) @NumberFormat(style = NumberFormat.Style.PERCENT)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public static @interface MyNumberAnn { public @interface MyNumberAnn {
} }
public static class Model { public static class Model {
@org.springframework.format.annotation.DateTimeFormat(style="S-") @org.springframework.format.annotation.DateTimeFormat(style="S-")
@ -386,6 +430,7 @@ public class FormattingConversionServiceTests {
} }
} }
public static class ModelWithPlaceholders { public static class ModelWithPlaceholders {
@org.springframework.format.annotation.DateTimeFormat(style="${dateStyle}") @org.springframework.format.annotation.DateTimeFormat(style="${dateStyle}")
@ -403,11 +448,13 @@ public class FormattingConversionServiceTests {
} }
} }
@org.springframework.format.annotation.DateTimeFormat(pattern="${datePattern}") @org.springframework.format.annotation.DateTimeFormat(pattern="${datePattern}")
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public static @interface MyDatePattern { public @interface MyDatePattern {
} }
public static class NullReturningFormatter implements Formatter<Integer> { public static class NullReturningFormatter implements Formatter<Integer> {
@Override @Override
@ -419,17 +466,42 @@ public class FormattingConversionServiceTests {
public Integer parse(String text, Locale locale) throws ParseException { public Integer parse(String text, Locale locale) throws ParseException {
return null; return null;
} }
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")
public static class MyDate extends Date { public static class MyDate extends Date {
} }
private static class ModelWithSubclassField { private static class ModelWithSubclassField {
@org.springframework.format.annotation.DateTimeFormat(style = "S-") @org.springframework.format.annotation.DateTimeFormat(style = "S-")
public MyDate date; public MyDate date;
} }
private static class IntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.parseInt(source);
}
}
private static class IntegerConverterFactory implements ConverterFactory<String, Number> {
@Override
@SuppressWarnings("unchecked")
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
if (Integer.class == targetType) {
return (Converter<String, T>) new IntegerConverter();
}
else {
throw new IllegalStateException();
}
}
}
} }

4
spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,7 +34,7 @@ public interface ConverterFactory<S, R> {
* Get the converter to convert from S to target type T, where T is also an instance of R. * Get the converter to convert from S to target type T, where T is also an instance of R.
* @param <T> the target type * @param <T> the target type
* @param targetType the target type to convert to * @param targetType the target type to convert to
* @return A converter from S to T * @return a converter from S to T
*/ */
<T extends R> Converter<S, T> getConverter(Class<T> targetType); <T extends R> Converter<S, T> getConverter(Class<T> targetType);

6
spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java

@ -49,12 +49,12 @@ public interface ConverterRegistry {
/** /**
* Add a ranged converter factory to this registry. * Add a ranged converter factory to this registry.
* The convertible source/target type pair is derived from the ConverterFactory's parameterized types. * The convertible source/target type pair is derived from the ConverterFactory's parameterized types.
* @throws IllegalArgumentException if the parameterized types could not be resolved. * @throws IllegalArgumentException if the parameterized types could not be resolved
*/ */
void addConverterFactory(ConverterFactory<?, ?> converterFactory); void addConverterFactory(ConverterFactory<?, ?> factory);
/** /**
* Remove any converters from sourceType to targetType. * Remove any converters from {@code sourceType} to {@code targetType}.
* @param sourceType the source type * @param sourceType the source type
* @param targetType the target type * @param targetType the target type
*/ */

31
spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionFailedException;
@ -83,9 +84,14 @@ public class GenericConversionService implements ConfigurableConversionService {
@Override @Override
public void addConverter(Converter<?, ?> converter) { public void addConverter(Converter<?, ?> converter) {
ResolvableType[] typeInfo = getRequiredTypeInfo(converter, Converter.class); ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " + if (typeInfo == null && converter instanceof DecoratingProxy) {
"<T> which your Converter<S, T> converts between; declare these generic types."); typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class);
}
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1])); addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
} }
@ -102,11 +108,16 @@ public class GenericConversionService implements ConfigurableConversionService {
} }
@Override @Override
public void addConverterFactory(ConverterFactory<?, ?> converterFactory) { public void addConverterFactory(ConverterFactory<?, ?> factory) {
ResolvableType[] typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class); ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
Assert.notNull(typeInfo, "Unable to the determine source type <S> and target range type R which your " + if (typeInfo == null && factory instanceof DecoratingProxy) {
"ConverterFactory<S, R> converts between; declare these generic types."); typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
addConverter(new ConverterFactoryAdapter(converterFactory, }
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterFactoryAdapter(factory,
new ConvertiblePair(typeInfo[0].resolve(), typeInfo[1].resolve()))); new ConvertiblePair(typeInfo[0].resolve(), typeInfo[1].resolve())));
} }
@ -271,8 +282,8 @@ public class GenericConversionService implements ConfigurableConversionService {
// Internal helpers // Internal helpers
private ResolvableType[] getRequiredTypeInfo(Object converter, Class<?> genericIfc) { private ResolvableType[] getRequiredTypeInfo(Class<?> converterClass, Class<?> genericIfc) {
ResolvableType resolvableType = ResolvableType.forClass(converter.getClass()).as(genericIfc); ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc);
ResolvableType[] generics = resolvableType.getGenerics(); ResolvableType[] generics = resolvableType.getGenerics();
if (generics.length < 2) { if (generics.length < 2) {
return null; return null;

Loading…
Cancel
Save