001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.format.support;
018
019import java.lang.annotation.Annotation;
020import java.util.Collections;
021import java.util.Map;
022import java.util.Set;
023import java.util.concurrent.ConcurrentHashMap;
024
025import org.springframework.context.EmbeddedValueResolverAware;
026import org.springframework.context.i18n.LocaleContextHolder;
027import org.springframework.core.DecoratingProxy;
028import org.springframework.core.GenericTypeResolver;
029import org.springframework.core.convert.ConversionService;
030import org.springframework.core.convert.TypeDescriptor;
031import org.springframework.core.convert.converter.ConditionalGenericConverter;
032import org.springframework.core.convert.converter.GenericConverter;
033import org.springframework.core.convert.support.GenericConversionService;
034import org.springframework.format.AnnotationFormatterFactory;
035import org.springframework.format.Formatter;
036import org.springframework.format.FormatterRegistry;
037import org.springframework.format.Parser;
038import org.springframework.format.Printer;
039import org.springframework.lang.Nullable;
040import org.springframework.util.Assert;
041import org.springframework.util.ClassUtils;
042import org.springframework.util.StringUtils;
043import org.springframework.util.StringValueResolver;
044
045/**
046 * A {@link org.springframework.core.convert.ConversionService} implementation
047 * designed to be configured as a {@link FormatterRegistry}.
048 *
049 * @author Keith Donald
050 * @author Juergen Hoeller
051 * @since 3.0
052 */
053public class FormattingConversionService extends GenericConversionService
054                implements FormatterRegistry, EmbeddedValueResolverAware {
055
056        @Nullable
057        private StringValueResolver embeddedValueResolver;
058
059        private final Map<AnnotationConverterKey, GenericConverter> cachedPrinters = new ConcurrentHashMap<>(64);
060
061        private final Map<AnnotationConverterKey, GenericConverter> cachedParsers = new ConcurrentHashMap<>(64);
062
063
064        @Override
065        public void setEmbeddedValueResolver(StringValueResolver resolver) {
066                this.embeddedValueResolver = resolver;
067        }
068
069
070        @Override
071        public void addPrinter(Printer<?> printer) {
072                Class<?> fieldType = getFieldType(printer, Printer.class);
073                addConverter(new PrinterConverter(fieldType, printer, this));
074        }
075
076        @Override
077        public void addParser(Parser<?> parser) {
078                Class<?> fieldType = getFieldType(parser, Parser.class);
079                addConverter(new ParserConverter(fieldType, parser, this));
080        }
081
082        @Override
083        public void addFormatter(Formatter<?> formatter) {
084                addFormatterForFieldType(getFieldType(formatter), formatter);
085        }
086
087        @Override
088        public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
089                addConverter(new PrinterConverter(fieldType, formatter, this));
090                addConverter(new ParserConverter(fieldType, formatter, this));
091        }
092
093        @Override
094        public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
095                addConverter(new PrinterConverter(fieldType, printer, this));
096                addConverter(new ParserConverter(fieldType, parser, this));
097        }
098
099        @Override
100        public void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory) {
101                Class<? extends Annotation> annotationType = getAnnotationType(annotationFormatterFactory);
102                if (this.embeddedValueResolver != null && annotationFormatterFactory instanceof EmbeddedValueResolverAware) {
103                        ((EmbeddedValueResolverAware) annotationFormatterFactory).setEmbeddedValueResolver(this.embeddedValueResolver);
104                }
105                Set<Class<?>> fieldTypes = annotationFormatterFactory.getFieldTypes();
106                for (Class<?> fieldType : fieldTypes) {
107                        addConverter(new AnnotationPrinterConverter(annotationType, annotationFormatterFactory, fieldType));
108                        addConverter(new AnnotationParserConverter(annotationType, annotationFormatterFactory, fieldType));
109                }
110        }
111
112
113        static Class<?> getFieldType(Formatter<?> formatter) {
114                return getFieldType(formatter, Formatter.class);
115        }
116
117        private static <T> Class<?> getFieldType(T instance, Class<T> genericInterface) {
118                Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(instance.getClass(), genericInterface);
119                if (fieldType == null && instance instanceof DecoratingProxy) {
120                        fieldType = GenericTypeResolver.resolveTypeArgument(
121                                        ((DecoratingProxy) instance).getDecoratedClass(), genericInterface);
122                }
123                Assert.notNull(fieldType, () -> "Unable to extract the parameterized field type from " +
124                                        ClassUtils.getShortName(genericInterface) + " [" + instance.getClass().getName() +
125                                        "]; does the class parameterize the <T> generic type?");
126                return fieldType;
127        }
128
129        @SuppressWarnings("unchecked")
130        static Class<? extends Annotation> getAnnotationType(AnnotationFormatterFactory<? extends Annotation> factory) {
131                Class<? extends Annotation> annotationType = (Class<? extends Annotation>)
132                                GenericTypeResolver.resolveTypeArgument(factory.getClass(), AnnotationFormatterFactory.class);
133                if (annotationType == null) {
134                        throw new IllegalArgumentException("Unable to extract parameterized Annotation type argument from " +
135                                        "AnnotationFormatterFactory [" + factory.getClass().getName() +
136                                        "]; does the factory parameterize the <A extends Annotation> generic type?");
137                }
138                return annotationType;
139        }
140
141
142        private static class PrinterConverter implements GenericConverter {
143
144                private final Class<?> fieldType;
145
146                private final TypeDescriptor printerObjectType;
147
148                @SuppressWarnings("rawtypes")
149                private final Printer printer;
150
151                private final ConversionService conversionService;
152
153                public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
154                        this.fieldType = fieldType;
155                        this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
156                        this.printer = printer;
157                        this.conversionService = conversionService;
158                }
159
160                @Override
161                public Set<ConvertiblePair> getConvertibleTypes() {
162                        return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
163                }
164
165                @Override
166                @SuppressWarnings("unchecked")
167                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
168                        if (!sourceType.isAssignableTo(this.printerObjectType)) {
169                                source = this.conversionService.convert(source, sourceType, this.printerObjectType);
170                        }
171                        if (source == null) {
172                                return "";
173                        }
174                        return this.printer.print(source, LocaleContextHolder.getLocale());
175                }
176
177                @Nullable
178                private Class<?> resolvePrinterObjectType(Printer<?> printer) {
179                        return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class);
180                }
181
182                @Override
183                public String toString() {
184                        return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer);
185                }
186        }
187
188
189        private static class ParserConverter implements GenericConverter {
190
191                private final Class<?> fieldType;
192
193                private final Parser<?> parser;
194
195                private final ConversionService conversionService;
196
197                public ParserConverter(Class<?> fieldType, Parser<?> parser, ConversionService conversionService) {
198                        this.fieldType = fieldType;
199                        this.parser = parser;
200                        this.conversionService = conversionService;
201                }
202
203                @Override
204                public Set<ConvertiblePair> getConvertibleTypes() {
205                        return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
206                }
207
208                @Override
209                @Nullable
210                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
211                        String text = (String) source;
212                        if (!StringUtils.hasText(text)) {
213                                return null;
214                        }
215                        Object result;
216                        try {
217                                result = this.parser.parse(text, LocaleContextHolder.getLocale());
218                        }
219                        catch (IllegalArgumentException ex) {
220                                throw ex;
221                        }
222                        catch (Throwable ex) {
223                                throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
224                        }
225                        TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
226                        if (!resultType.isAssignableTo(targetType)) {
227                                result = this.conversionService.convert(result, resultType, targetType);
228                        }
229                        return result;
230                }
231
232                @Override
233                public String toString() {
234                        return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser);
235                }
236        }
237
238
239        private class AnnotationPrinterConverter implements ConditionalGenericConverter {
240
241                private final Class<? extends Annotation> annotationType;
242
243                @SuppressWarnings("rawtypes")
244                private final AnnotationFormatterFactory annotationFormatterFactory;
245
246                private final Class<?> fieldType;
247
248                public AnnotationPrinterConverter(Class<? extends Annotation> annotationType,
249                                AnnotationFormatterFactory<?> annotationFormatterFactory, Class<?> fieldType) {
250
251                        this.annotationType = annotationType;
252                        this.annotationFormatterFactory = annotationFormatterFactory;
253                        this.fieldType = fieldType;
254                }
255
256                @Override
257                public Set<ConvertiblePair> getConvertibleTypes() {
258                        return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
259                }
260
261                @Override
262                public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
263                        return sourceType.hasAnnotation(this.annotationType);
264                }
265
266                @Override
267                @SuppressWarnings("unchecked")
268                @Nullable
269                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
270                        Annotation ann = sourceType.getAnnotation(this.annotationType);
271                        if (ann == null) {
272                                throw new IllegalStateException(
273                                                "Expected [" + this.annotationType.getName() + "] to be present on " + sourceType);
274                        }
275                        AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, sourceType.getObjectType());
276                        GenericConverter converter = cachedPrinters.get(converterKey);
277                        if (converter == null) {
278                                Printer<?> printer = this.annotationFormatterFactory.getPrinter(
279                                                converterKey.getAnnotation(), converterKey.getFieldType());
280                                converter = new PrinterConverter(this.fieldType, printer, FormattingConversionService.this);
281                                cachedPrinters.put(converterKey, converter);
282                        }
283                        return converter.convert(source, sourceType, targetType);
284                }
285
286                @Override
287                public String toString() {
288                        return ("@" + this.annotationType.getName() + " " + this.fieldType.getName() + " -> " +
289                                        String.class.getName() + ": " + this.annotationFormatterFactory);
290                }
291        }
292
293
294        private class AnnotationParserConverter implements ConditionalGenericConverter {
295
296                private final Class<? extends Annotation> annotationType;
297
298                @SuppressWarnings("rawtypes")
299                private final AnnotationFormatterFactory annotationFormatterFactory;
300
301                private final Class<?> fieldType;
302
303                public AnnotationParserConverter(Class<? extends Annotation> annotationType,
304                                AnnotationFormatterFactory<?> annotationFormatterFactory, Class<?> fieldType) {
305
306                        this.annotationType = annotationType;
307                        this.annotationFormatterFactory = annotationFormatterFactory;
308                        this.fieldType = fieldType;
309                }
310
311                @Override
312                public Set<ConvertiblePair> getConvertibleTypes() {
313                        return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
314                }
315
316                @Override
317                public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
318                        return targetType.hasAnnotation(this.annotationType);
319                }
320
321                @Override
322                @SuppressWarnings("unchecked")
323                @Nullable
324                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
325                        Annotation ann = targetType.getAnnotation(this.annotationType);
326                        if (ann == null) {
327                                throw new IllegalStateException(
328                                                "Expected [" + this.annotationType.getName() + "] to be present on " + targetType);
329                        }
330                        AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType());
331                        GenericConverter converter = cachedParsers.get(converterKey);
332                        if (converter == null) {
333                                Parser<?> parser = this.annotationFormatterFactory.getParser(
334                                                converterKey.getAnnotation(), converterKey.getFieldType());
335                                converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this);
336                                cachedParsers.put(converterKey, converter);
337                        }
338                        return converter.convert(source, sourceType, targetType);
339                }
340
341                @Override
342                public String toString() {
343                        return (String.class.getName() + " -> @" + this.annotationType.getName() + " " +
344                                        this.fieldType.getName() + ": " + this.annotationFormatterFactory);
345                }
346        }
347
348
349        private static class AnnotationConverterKey {
350
351                private final Annotation annotation;
352
353                private final Class<?> fieldType;
354
355                public AnnotationConverterKey(Annotation annotation, Class<?> fieldType) {
356                        this.annotation = annotation;
357                        this.fieldType = fieldType;
358                }
359
360                public Annotation getAnnotation() {
361                        return this.annotation;
362                }
363
364                public Class<?> getFieldType() {
365                        return this.fieldType;
366                }
367
368                @Override
369                public boolean equals(@Nullable Object other) {
370                        if (this == other) {
371                                return true;
372                        }
373                        if (!(other instanceof AnnotationConverterKey)) {
374                                return false;
375                        }
376                        AnnotationConverterKey otherKey = (AnnotationConverterKey) other;
377                        return (this.fieldType == otherKey.fieldType && this.annotation.equals(otherKey.annotation));
378                }
379
380                @Override
381                public int hashCode() {
382                        return (this.fieldType.hashCode() * 29 + this.annotation.hashCode());
383                }
384        }
385
386}