1 /*
2  * Copyright 2008-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.repository.support;
17
18 import java.util.Collections;
19 import java.util.Optional;
20 import java.util.Set;
21
22 import javax.annotation.Nonnull;
23
24 import org.springframework.context.ApplicationContext;
25 import org.springframework.context.ApplicationContextAware;
26 import org.springframework.core.convert.ConversionService;
27 import org.springframework.core.convert.TypeDescriptor;
28 import org.springframework.core.convert.converter.ConditionalGenericConverter;
29 import org.springframework.core.convert.converter.ConverterRegistry;
30 import org.springframework.data.repository.CrudRepository;
31 import org.springframework.data.repository.core.EntityInformation;
32 import org.springframework.data.repository.core.RepositoryInformation;
33 import org.springframework.data.util.Lazy;
34 import org.springframework.lang.Nullable;
35 import org.springframework.util.Assert;
36 import org.springframework.util.StringUtils;
37
38 /**
39  * {@link org.springframework.core.convert.converter.Converter} to convert arbitrary input into domain classes managed
40  * by Spring Data {@link CrudRepository}s. The implementation uses a {@link ConversionService} in turn to convert the
41  * source type into the domain class' id type which is then converted into a domain class object by using a
42  * {@link CrudRepository}.
43  *
44  * @author Oliver Gierke
45  * @author Thomas Darimont
46  */

47 public class DomainClassConverter<T extends ConversionService & ConverterRegistry>
48         implements ConditionalGenericConverter, ApplicationContextAware {
49
50     private final T conversionService;
51     private Lazy<Repositories> repositories = Lazy.of(Repositories.NONE);
52     private Optional<ToEntityConverter> toEntityConverter = Optional.empty();
53     private Optional<ToIdConverter> toIdConverter = Optional.empty();
54
55     /**
56      * Creates a new {@link DomainClassConverter} for the given {@link ConversionService}.
57      *
58      * @param conversionService must not be {@literal null}.
59      */

60     public DomainClassConverter(T conversionService) {
61
62         Assert.notNull(conversionService, "ConversionService must not be null!");
63
64         this.conversionService = conversionService;
65         this.conversionService.addConverter(this);
66     }
67
68     /*
69      * (non-Javadoc)
70      * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
71      */

72     @Nonnull
73     @Override
74     public Set<ConvertiblePair> getConvertibleTypes() {
75         return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
76     }
77
78     /*
79      * (non-Javadoc)
80      * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
81      */

82     @Nullable
83     @Override
84     public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
85         return getConverter(targetType).map(it -> it.convert(source, sourceType, targetType)).orElse(null);
86     }
87
88     /*
89      * (non-Javadoc)
90      * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
91      */

92     @Override
93     public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
94         return getConverter(targetType).map(it -> it.matches(sourceType, targetType)).orElse(false);
95     }
96
97     /**
98      * @param targetType
99      * @return
100      */

101     private Optional<? extends ConditionalGenericConverter> getConverter(TypeDescriptor targetType) {
102         return repositories.get().hasRepositoryFor(targetType.getType()) ? toEntityConverter : toIdConverter;
103     }
104
105     /*
106      * (non-Javadoc)
107      * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
108      */

109     public void setApplicationContext(ApplicationContext context) {
110
111         this.repositories = Lazy.of(() -> {
112
113             Repositories repositories = new Repositories(context);
114
115             this.toEntityConverter = Optional.of(new ToEntityConverter(repositories, conversionService));
116             this.toIdConverter = Optional.of(new ToIdConverter(repositories, conversionService));
117
118             return repositories;
119         });
120     }
121
122     /**
123      * Converter to create domain types from any source that can be converted into the domain types identifier type.
124      *
125      * @author Oliver Gierke
126      * @since 1.10
127      */

128     private static class ToEntityConverter implements ConditionalGenericConverter {
129
130         private final RepositoryInvokerFactory repositoryInvokerFactory;
131         private final Repositories repositories;
132         private final ConversionService conversionService;
133
134         /**
135          * Creates a new {@link ToEntityConverter} for the given {@link Repositories} and {@link ConversionService}.
136          *
137          * @param repositories must not be {@literal null}.
138          * @param conversionService must not be {@literal null}.
139          */

140         public ToEntityConverter(Repositories repositories, ConversionService conversionService) {
141
142             this.repositoryInvokerFactory = new DefaultRepositoryInvokerFactory(repositories, conversionService);
143             this.repositories = repositories;
144             this.conversionService = conversionService;
145         }
146
147         /*
148          * (non-Javadoc)
149          * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
150          */

151         @Nonnull
152         @Override
153         public Set<ConvertiblePair> getConvertibleTypes() {
154             return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
155         }
156
157         /*
158          * (non-Javadoc)
159          * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
160          */

161         @Nullable
162         @Override
163         public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
164
165             if (source == null || !StringUtils.hasText(source.toString())) {
166                 return null;
167             }
168
169             if (sourceType.equals(targetType)) {
170                 return source;
171             }
172
173             Class<?> domainType = targetType.getType();
174             RepositoryInvoker invoker = repositoryInvokerFactory.getInvokerFor(domainType);
175             RepositoryInformation information = repositories.getRequiredRepositoryInformation(domainType);
176
177             Object id = conversionService.convert(source, information.getIdType());
178
179             return id == null ? null : invoker.invokeFindById(id).orElse(null);
180         }
181
182         /*
183          * (non-Javadoc)
184          * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
185          */

186         @Override
187         public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
188
189             if (sourceType.isAssignableTo(targetType)) {
190                 return false;
191             }
192
193             Class<?> domainType = targetType.getType();
194
195             if (!repositories.hasRepositoryFor(domainType)) {
196                 return false;
197             }
198
199             Optional<RepositoryInformation> repositoryInformation = repositories.getRepositoryInformationFor(domainType);
200
201             return repositoryInformation.map(it -> {
202
203                 Class<?> rawIdType = it.getIdType();
204
205                 return sourceType.equals(TypeDescriptor.valueOf(rawIdType))
206                         || conversionService.canConvert(sourceType.getType(), rawIdType);
207             }).orElseThrow(
208                     () -> new IllegalStateException(String.format("Couldn't find RepositoryInformation for %s!", domainType)));
209         }
210     }
211
212     /**
213      * Converter to turn domain types into their identifiers or any transitively convertible type.
214      *
215      * @author Oliver Gierke
216      * @since 1.10
217      */

218     static class ToIdConverter implements ConditionalGenericConverter {
219
220         private final Repositories repositories;
221         private final ConversionService conversionService;
222
223         public ToIdConverter(Repositories repositories, ConversionService conversionService) {
224
225             this.repositories = repositories;
226             this.conversionService = conversionService;
227         }
228
229         /*
230          * (non-Javadoc)
231          * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
232          */

233         @Nonnull
234         @Override
235         public Set<ConvertiblePair> getConvertibleTypes() {
236             return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
237         }
238
239         /*
240          * (non-Javadoc)
241          * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
242          */

243         @Nullable
244         @Override
245         public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
246
247             if (source == null || !StringUtils.hasText(source.toString())) {
248                 return null;
249             }
250
251             if (sourceType.equals(targetType)) {
252                 return source;
253             }
254
255             Class<?> domainType = sourceType.getType();
256
257             EntityInformation<Object, ?> entityInformation = repositories.getEntityInformationFor(domainType);
258
259             return conversionService.convert(entityInformation.getId(source), targetType.getType());
260         }
261
262         /*
263          * (non-Javadoc)
264          * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
265          */

266         @Override
267         public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
268
269             if (sourceType.isAssignableTo(targetType)) {
270                 return false;
271             }
272
273             Class<?> domainType = sourceType.getType();
274
275             if (!repositories.hasRepositoryFor(domainType)) {
276                 return false;
277             }
278
279             Optional<RepositoryInformation> information = repositories.getRepositoryInformationFor(domainType);
280
281             return information.map(it -> {
282
283                 Class<?> rawIdType = it.getIdType();
284
285                 return targetType.equals(TypeDescriptor.valueOf(rawIdType))
286                         || conversionService.canConvert(rawIdType, targetType.getType());
287
288             }).orElseThrow(
289                     () -> new IllegalStateException(String.format("Couldn't find RepositoryInformation for %s!", domainType)));
290         }
291     }
292 }
293