• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.62
today-context/src/main/java/infra/format/support/FormattingConversionService.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.format.support;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.lang.annotation.Annotation;
23
import java.util.Collections;
24
import java.util.Set;
25
import java.util.concurrent.ConcurrentHashMap;
26

27
import infra.context.expression.EmbeddedValueResolverAware;
28
import infra.core.DecoratingProxy;
29
import infra.core.GenericTypeResolver;
30
import infra.core.StringValueResolver;
31
import infra.core.TypeDescriptor;
32
import infra.core.conversion.ConditionalGenericConverter;
33
import infra.core.conversion.ConversionService;
34
import infra.core.conversion.GenericConverter;
35
import infra.core.conversion.support.GenericConversionService;
36
import infra.core.i18n.LocaleContextHolder;
37
import infra.format.AnnotationFormatterFactory;
38
import infra.format.Formatter;
39
import infra.format.FormatterRegistry;
40
import infra.format.Parser;
41
import infra.format.Printer;
42
import infra.util.ClassUtils;
43
import infra.util.StringUtils;
44

45
/**
46
 * A {@link ConversionService} implementation
47
 * designed to be configured as a {@link FormatterRegistry}.
48
 *
49
 * @author Keith Donald
50
 * @author Juergen Hoeller
51
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
52
 * @since 4.0
53
 */
54
public class FormattingConversionService extends GenericConversionService
2✔
55
        implements FormatterRegistry, EmbeddedValueResolverAware {
56

57
  @Nullable
58
  private StringValueResolver embeddedValueResolver;
59

60
  private final ConcurrentHashMap<ConverterKey, GenericConverter> cachedParsers
6✔
61
          = new ConcurrentHashMap<>(64);
62

63
  private final ConcurrentHashMap<ConverterKey, GenericConverter> cachedPrinters
7✔
64
          = new ConcurrentHashMap<>(64);
65

66
  @Override
67
  public void setEmbeddedValueResolver(@Nullable StringValueResolver resolver) {
68
    this.embeddedValueResolver = resolver;
3✔
69
  }
1✔
70

71
  @Override
72
  public void addPrinter(Printer<?> printer) {
73
    Class<?> fieldType = getFieldType(printer, Printer.class);
4✔
74
    addConverter(new PrinterConverter(fieldType, printer, this));
8✔
75
  }
1✔
76

77
  @Override
78
  public void addParser(Parser<?> parser) {
79
    Class<?> fieldType = getFieldType(parser, Parser.class);
4✔
80
    addConverter(new ParserConverter(fieldType, parser, this));
8✔
81
  }
1✔
82

83
  @Override
84
  public void addFormatter(Formatter<?> formatter) {
85
    addFormatterForFieldType(getFieldType(formatter), formatter);
5✔
86
  }
1✔
87

88
  @Override
89
  public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
90
    addConverter(new PrinterConverter(fieldType, formatter, this));
8✔
91
    addConverter(new ParserConverter(fieldType, formatter, this));
8✔
92
  }
1✔
93

94
  @Override
95
  public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
96
    addConverter(new PrinterConverter(fieldType, printer, this));
8✔
97
    addConverter(new ParserConverter(fieldType, parser, this));
8✔
98
  }
1✔
99

100
  @Override
101
  public <T extends Annotation> void addFormatterForFieldAnnotation(AnnotationFormatterFactory<T> factory) {
102
    Class<T> annotationType = getAnnotationType(factory);
3✔
103
    if (embeddedValueResolver != null && factory instanceof EmbeddedValueResolverAware) {
6!
104
      ((EmbeddedValueResolverAware) factory).setEmbeddedValueResolver(embeddedValueResolver);
5✔
105
    }
106
    Set<Class<?>> fieldTypes = factory.getFieldTypes();
3✔
107
    for (Class<?> fieldType : fieldTypes) {
10✔
108
      addConverter(new AnnotationPrinterConverter(annotationType, factory, fieldType));
9✔
109
      addConverter(new AnnotationParserConverter(annotationType, factory, fieldType));
9✔
110
    }
1✔
111
  }
1✔
112

113
  static Class<?> getFieldType(Formatter<?> formatter) {
114
    return getFieldType(formatter, Formatter.class);
4✔
115
  }
116

117
  private static <T> Class<?> getFieldType(T instance, Class<T> genericInterface) {
118
    Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(instance.getClass(), genericInterface);
5✔
119
    if (fieldType == null && instance instanceof DecoratingProxy decoratingProxy) {
8✔
120
      fieldType = GenericTypeResolver.resolveTypeArgument(
3✔
121
              decoratingProxy.getDecoratedClass(), genericInterface);
2✔
122
    }
123
    if (fieldType == null) {
2✔
124
      throw new IllegalArgumentException(
8✔
125
              "Unable to extract the parameterized field type from %s [%s]; does the class parameterize the <T> generic type?"
126
                      .formatted(ClassUtils.getShortName(genericInterface), instance.getClass().getName()));
11✔
127
    }
128
    return fieldType;
2✔
129
  }
130

131
  static <T extends Annotation> Class<T> getAnnotationType(AnnotationFormatterFactory<T> factory) {
132
    Class<T> annotationType = GenericTypeResolver.resolveTypeArgument(factory.getClass(), AnnotationFormatterFactory.class);
5✔
133
    if (annotationType == null) {
2!
134
      throw new IllegalArgumentException(
×
135
              "Unable to extract parameterized Annotation type argument from AnnotationFormatterFactory [%s]; does the factory parameterize the <A extends Annotation> generic type?"
136
                      .formatted(factory.getClass().getName()));
×
137
    }
138
    return annotationType;
2✔
139
  }
140

141
  private static Annotation getAnnotation(TypeDescriptor sourceType, Class<? extends Annotation> annotationType) {
142
    Annotation ann = sourceType.getAnnotation(annotationType);
4✔
143
    if (ann == null) {
2!
144
      throw new IllegalStateException(
×
145
              "Expected [%s] to be present on %s".formatted(annotationType.getName(), sourceType));
×
146
    }
147
    return ann;
2✔
148
  }
149

150
  private static final class PrinterConverter implements GenericConverter {
151

152
    private final Class<?> fieldType;
153

154
    private final TypeDescriptor printerObjectType;
155

156
    @SuppressWarnings("rawtypes")
157
    private final Printer printer;
158

159
    private final ConversionService conversionService;
160

161
    public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
2✔
162
      this.fieldType = fieldType;
3✔
163
      this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
6✔
164
      this.printer = printer;
3✔
165
      this.conversionService = conversionService;
3✔
166
    }
1✔
167

168
    @Override
169
    public Set<ConvertiblePair> getConvertibleTypes() {
170
      return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
8✔
171
    }
172

173
    @Override
174
    @SuppressWarnings("unchecked")
175
    public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
176
      if (!sourceType.isAssignableTo(this.printerObjectType)) {
5✔
177
        source = this.conversionService.convert(source, sourceType, this.printerObjectType);
8✔
178
      }
179
      if (source == null) {
2✔
180
        return "";
2✔
181
      }
182
      return this.printer.print(source, LocaleContextHolder.getLocale());
6✔
183
    }
184

185
    @Nullable
186
    private Class<?> resolvePrinterObjectType(Printer<?> printer) {
187
      return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class);
5✔
188
    }
189

190
    @Override
191
    public String toString() {
192
      return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer);
×
193
    }
194
  }
195

196
  private record ParserConverter(Class<?> fieldType, Parser<?> parser, ConversionService conversionService)
12✔
197
          implements GenericConverter {
198

199
    @Override
200
    public Set<ConvertiblePair> getConvertibleTypes() {
201
      return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
8✔
202
    }
203

204
    @Override
205
    @Nullable
206
    public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
207
      String text = (String) source;
3✔
208
      if (StringUtils.isBlank(text)) {
3✔
209
        return null;
2✔
210
      }
211
      Object result;
212
      try {
213
        result = this.parser.parse(text, LocaleContextHolder.getLocale());
6✔
214
      }
215
      catch (IllegalArgumentException ex) {
×
216
        throw ex;
×
217
      }
218
      catch (Throwable ex) {
1✔
219
        throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
7✔
220
      }
1✔
221
      TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
4✔
222
      if (!resultType.isAssignableTo(targetType)) {
4✔
223
        result = this.conversionService.convert(result, resultType, targetType);
7✔
224
      }
225
      return result;
2✔
226
    }
227

228
    @Override
229
    public String toString() {
230
      return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser);
×
231
    }
232
  }
233

234
  private final class AnnotationPrinterConverter implements ConditionalGenericConverter {
235

236
    private final Class<? extends Annotation> annotationType;
237

238
    @SuppressWarnings("rawtypes")
239
    private final AnnotationFormatterFactory formatterFactory;
240

241
    private final Class<?> fieldType;
242

243
    public AnnotationPrinterConverter(
244
            Class<? extends Annotation> annotationType,
245
            AnnotationFormatterFactory<?> formatterFactory, Class<?> fieldType) {
5✔
246

247
      this.fieldType = fieldType;
3✔
248
      this.annotationType = annotationType;
3✔
249
      this.formatterFactory = formatterFactory;
3✔
250
    }
1✔
251

252
    @Override
253
    public Set<ConvertiblePair> getConvertibleTypes() {
254
      return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
8✔
255
    }
256

257
    @Override
258
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
259
      return sourceType.hasAnnotation(this.annotationType);
5✔
260
    }
261

262
    @Override
263
    @SuppressWarnings("unchecked")
264
    @Nullable
265
    public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
266
      Annotation ann = getAnnotation(sourceType, annotationType);
5✔
267
      ConverterKey key = new ConverterKey(ann, sourceType.getObjectType());
7✔
268
      GenericConverter converter = cachedPrinters.get(key);
7✔
269
      if (converter == null) {
2✔
270
        Printer<?> printer = formatterFactory.getPrinter(key.annotation, key.fieldType);
8✔
271
        converter = new PrinterConverter(this.fieldType, printer, FormattingConversionService.this);
9✔
272
        cachedPrinters.put(key, converter);
7✔
273
      }
274
      return converter.convert(source, sourceType, targetType);
6✔
275
    }
276

277
    @Override
278
    public String toString() {
279
      return ("@" + this.annotationType.getName() + " " + this.fieldType.getName() + " -> " +
×
280
              String.class.getName() + ": " + this.formatterFactory);
×
281
    }
282
  }
283

284
  private final class AnnotationParserConverter implements ConditionalGenericConverter {
285

286
    private final Class<? extends Annotation> annotationType;
287

288
    @SuppressWarnings("rawtypes")
289
    private final AnnotationFormatterFactory formatterFactory;
290

291
    private final Class<?> fieldType;
292

293
    public AnnotationParserConverter(
294
            Class<? extends Annotation> annotationType,
295
            AnnotationFormatterFactory<?> formatterFactory, Class<?> fieldType) {
5✔
296
      this.fieldType = fieldType;
3✔
297
      this.annotationType = annotationType;
3✔
298
      this.formatterFactory = formatterFactory;
3✔
299
    }
1✔
300

301
    @Override
302
    public Set<ConvertiblePair> getConvertibleTypes() {
303
      return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
8✔
304
    }
305

306
    @Override
307
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
308
      return targetType.hasAnnotation(this.annotationType);
5✔
309
    }
310

311
    @Override
312
    @SuppressWarnings("unchecked")
313
    @Nullable
314
    public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
315
      Annotation ann = getAnnotation(targetType, annotationType);
5✔
316
      ConverterKey key = new ConverterKey(ann, targetType.getObjectType());
7✔
317
      GenericConverter converter = cachedParsers.get(key);
7✔
318
      if (converter == null) {
2✔
319
        Parser<?> parser = formatterFactory.getParser(key.annotation, key.fieldType);
8✔
320
        converter = new ParserConverter(fieldType, parser, FormattingConversionService.this);
9✔
321
        cachedParsers.put(key, converter);
7✔
322
      }
323
      return converter.convert(source, sourceType, targetType);
6✔
324
    }
325

326
    @Override
327
    public String toString() {
328
      return (String.class.getName() + " -> @" + this.annotationType.getName() + " " +
×
329
              this.fieldType.getName() + ": " + this.formatterFactory);
×
330
    }
331
  }
332

333
  private static final class ConverterKey {
334
    public final Annotation annotation;
335
    public final Class<?> fieldType;
336

337
    private ConverterKey(Annotation annotation, Class<?> fieldType) {
2✔
338
      this.annotation = annotation;
3✔
339
      this.fieldType = fieldType;
3✔
340
    }
1✔
341

342
    @Override
343
    public boolean equals(@Nullable Object other) {
344
      if (this == other) {
3!
345
        return true;
×
346
      }
347
      if (!(other instanceof ConverterKey otherKey)) {
7!
348
        return false;
×
349
      }
350
      return (this.fieldType == otherKey.fieldType && this.annotation.equals(otherKey.annotation));
14!
351
    }
352

353
    @Override
354
    public int hashCode() {
355
      return (this.fieldType.hashCode() * 29 + this.annotation.hashCode());
10✔
356
    }
357
  }
358

359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc