1 /*
2  * Copyright 2011-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.jpa.repository.support;
17
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.Set;
24
25 import javax.persistence.IdClass;
26 import javax.persistence.metamodel.Attribute;
27 import javax.persistence.metamodel.EntityType;
28 import javax.persistence.metamodel.IdentifiableType;
29 import javax.persistence.metamodel.ManagedType;
30 import javax.persistence.metamodel.Metamodel;
31 import javax.persistence.metamodel.SingularAttribute;
32 import javax.persistence.metamodel.Type;
33 import javax.persistence.metamodel.Type.PersistenceType;
34
35 import org.springframework.beans.BeanWrapper;
36 import org.springframework.beans.BeanWrapperImpl;
37 import org.springframework.core.annotation.AnnotationUtils;
38 import org.springframework.data.jpa.provider.PersistenceProvider;
39 import org.springframework.data.jpa.util.JpaMetamodel;
40 import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
41 import org.springframework.data.util.ProxyUtils;
42 import org.springframework.lang.Nullable;
43 import org.springframework.util.Assert;
44
45 /**
46  * Implementation of {@link org.springframework.data.repository.core.EntityInformation} that uses JPA {@link Metamodel}
47  * to find the domain class' id field.
48  *
49  * @author Oliver Gierke
50  * @author Thomas Darimont
51  * @author Christoph Strobl
52  * @author Mark Paluch
53  * @author Jens Schauder
54  */

55 public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {
56
57     private final IdMetadata<T> idMetadata;
58     private final Optional<SingularAttribute<? super T, ?>> versionAttribute;
59     private final Metamodel metamodel;
60     private final @Nullable String entityName;
61
62     /**
63      * Creates a new {@link JpaMetamodelEntityInformation} for the given domain class and {@link Metamodel}.
64      *
65      * @param domainClass must not be {@literal null}.
66      * @param metamodel must not be {@literal null}.
67      */

68     public JpaMetamodelEntityInformation(Class<T> domainClass, Metamodel metamodel) {
69
70         super(domainClass);
71
72         Assert.notNull(metamodel, "Metamodel must not be null!");
73         this.metamodel = metamodel;
74
75         ManagedType<T> type = metamodel.managedType(domainClass);
76
77         if (type == null) {
78             throw new IllegalArgumentException("The given domain class can not be found in the given Metamodel!");
79         }
80
81         this.entityName = type instanceof EntityType ? ((EntityType<?>) type).getName() : null;
82
83         if (!(type instanceof IdentifiableType)) {
84             throw new IllegalArgumentException("The given domain class does not contain an id attribute!");
85         }
86
87         IdentifiableType<T> identifiableType = (IdentifiableType<T>) type;
88
89         this.idMetadata = new IdMetadata<>(identifiableType);
90         this.versionAttribute = findVersionAttribute(identifiableType, metamodel);
91     }
92
93     /*
94      * (non-Javadoc)
95      * @see org.springframework.data.jpa.repository.support.JpaEntityInformationSupport#getEntityName()
96      */

97     @Override
98     public String getEntityName() {
99         return entityName != null ? entityName : super.getEntityName();
100     }
101
102     /**
103      * Returns the version attribute of the given {@link ManagedType} or {@literal nullif none available.
104      *
105      * @param type must not be {@literal null}.
106      * @param metamodel must not be {@literal null}.
107      * @return
108      */

109     @SuppressWarnings("unchecked")
110     private static <T> Optional<SingularAttribute<? super T, ?>> findVersionAttribute(IdentifiableType<T> type,
111             Metamodel metamodel) {
112
113         try {
114             return Optional.ofNullable(type.getVersion(Object.class));
115         } catch (IllegalArgumentException o_O) {
116             // Needs workarounds as the method is implemented with a strict type check on e.g. Hibernate < 4.3
117         }
118
119         Set<SingularAttribute<? super T, ?>> attributes = type.getSingularAttributes();
120
121         for (SingularAttribute<? super T, ?> attribute : attributes) {
122             if (attribute.isVersion()) {
123                 return Optional.of(attribute);
124             }
125         }
126
127         Class<?> superType = type.getJavaType().getSuperclass();
128
129         if (!JpaMetamodel.of(metamodel).isJpaManaged(superType)) {
130             return Optional.empty();
131         }
132
133         ManagedType<?> managedSuperType = metamodel.managedType(superType);
134
135         if (!(managedSuperType instanceof IdentifiableType)) {
136             return Optional.empty();
137         }
138
139         return findVersionAttribute((IdentifiableType<T>) managedSuperType, metamodel);
140     }
141
142     /*
143      * (non-Javadoc)
144      * @see org.springframework.data.repository.core.EntityInformation#getId(java.lang.Object)
145      */

146     @Override
147     @Nullable
148     @SuppressWarnings("unchecked")
149     public ID getId(T entity) {
150
151         // check if this is a proxy. If so use Proxy mechanics to access the id.
152         PersistenceProvider persistenceProvider = PersistenceProvider.fromMetamodel(metamodel);
153
154         if (persistenceProvider.shouldUseAccessorFor(entity)) {
155             return (ID) persistenceProvider.getIdentifierFrom(entity);
156         }
157
158         // if not a proxy use Spring mechanics to access the id.
159         BeanWrapper entityWrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
160
161         if (idMetadata.hasSimpleId()) {
162             return (ID) entityWrapper.getPropertyValue(idMetadata.getSimpleIdAttribute().getName());
163         }
164
165         BeanWrapper idWrapper = new IdentifierDerivingDirectFieldAccessFallbackBeanWrapper(idMetadata.getType(), metamodel);
166         boolean partialIdValueFound = false;
167
168         for (SingularAttribute<? super T, ?> attribute : idMetadata) {
169             Object propertyValue = entityWrapper.getPropertyValue(attribute.getName());
170
171             if (propertyValue != null) {
172                 partialIdValueFound = true;
173             }
174
175             idWrapper.setPropertyValue(attribute.getName(), propertyValue);
176         }
177
178         return partialIdValueFound ? (ID) idWrapper.getWrappedInstance() : null;
179     }
180
181     /*
182      * (non-Javadoc)
183      * @see org.springframework.data.repository.core.EntityInformation#getIdType()
184      */

185     @Override
186     @SuppressWarnings("unchecked")
187     public Class<ID> getIdType() {
188         return (Class<ID>) idMetadata.getType();
189     }
190
191     /*
192      * (non-Javadoc)
193      * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getIdAttribute()
194      */

195     @Override
196     public SingularAttribute<? super T, ?> getIdAttribute() {
197         return idMetadata.getSimpleIdAttribute();
198     }
199
200     /*
201      * (non-Javadoc)
202      * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#hasCompositeId()
203      */

204     @Override
205     public boolean hasCompositeId() {
206         return !idMetadata.hasSimpleId();
207     }
208
209     /*
210      * (non-Javadoc)
211      * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getIdAttributeNames()
212      */

213     @Override
214     public Iterable<String> getIdAttributeNames() {
215
216         List<String> attributeNames = new ArrayList<>(idMetadata.attributes.size());
217
218         for (SingularAttribute<? super T, ?> attribute : idMetadata.attributes) {
219             attributeNames.add(attribute.getName());
220         }
221
222         return attributeNames;
223     }
224
225     /*
226      * (non-Javadoc)
227      * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getCompositeIdAttributeValue(java.lang.Object, java.lang.String)
228      */

229     @Override
230     public Object getCompositeIdAttributeValue(Object id, String idAttribute) {
231
232         Assert.isTrue(hasCompositeId(), "Model must have a composite Id!");
233
234         return new DirectFieldAccessFallbackBeanWrapper(id).getPropertyValue(idAttribute);
235     }
236
237     /*
238      * (non-Javadoc)
239      * @see org.springframework.data.repository.core.support.AbstractEntityInformation#isNew(java.lang.Object)
240      */

241     @Override
242     public boolean isNew(T entity) {
243
244         if (!versionAttribute.isPresent()
245                 || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
246             return super.isNew(entity);
247         }
248
249         BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
250
251         return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
252     }
253
254     /**
255      * Simple value object to encapsulate id specific metadata.
256      *
257      * @author Oliver Gierke
258      * @author Thomas Darimont
259      */

260     private static class IdMetadata<T> implements Iterable<SingularAttribute<? super T, ?>> {
261
262         private final IdentifiableType<T> type;
263         private final Set<SingularAttribute<? super T, ?>> attributes;
264         private @Nullable Class<?> idType;
265
266         @SuppressWarnings("unchecked")
267         IdMetadata(IdentifiableType<T> source) {
268
269             this.type = source;
270             this.attributes = (Set<SingularAttribute<? super T, ?>>) (source.hasSingleIdAttribute()
271                     ? Collections.singleton(source.getId(source.getIdType().getJavaType()))
272                     : source.getIdClassAttributes());
273         }
274
275         boolean hasSimpleId() {
276             return attributes.size() == 1;
277         }
278
279         public Class<?> getType() {
280
281             if (idType != null) {
282                 return idType;
283             }
284
285             // lazy initialization of idType field with tolerable benign data-race
286             this.idType = tryExtractIdTypeWithFallbackToIdTypeLookup();
287
288             if (this.idType == null) {
289                 throw new IllegalStateException("Cannot resolve Id type from " + type);
290             }
291
292             return this.idType;
293         }
294
295         @Nullable
296         private Class<?> tryExtractIdTypeWithFallbackToIdTypeLookup() {
297
298             try {
299                 Type<?> idType = type.getIdType();
300                 return idType == null ? fallbackIdTypeLookup(type) : idType.getJavaType();
301             } catch (IllegalStateException e) {
302                 // see https://hibernate.onjira.com/browse/HHH-6951
303                 return fallbackIdTypeLookup(type);
304             }
305         }
306
307         @Nullable
308         private static Class<?> fallbackIdTypeLookup(IdentifiableType<?> type) {
309
310             IdClass annotation = AnnotationUtils.findAnnotation(type.getJavaType(), IdClass.class);
311             return annotation == null ? null : annotation.value();
312         }
313
314         SingularAttribute<? super T, ?> getSimpleIdAttribute() {
315             return attributes.iterator().next();
316         }
317
318         /*
319          * (non-Javadoc)
320          * @see java.lang.Iterable#iterator()
321          */

322         @Override
323         public Iterator<SingularAttribute<? super T, ?>> iterator() {
324             return attributes.iterator();
325         }
326     }
327
328     /**
329      * Custom extension of {@link DirectFieldAccessFallbackBeanWrapper} that allows to derive the identifier if composite
330      * keys with complex key attribute types (e.g. types that are annotated with {@code @Entity} themselves) are used.
331      *
332      * @author Thomas Darimont
333      */

334     private static class IdentifierDerivingDirectFieldAccessFallbackBeanWrapper
335             extends DirectFieldAccessFallbackBeanWrapper {
336
337         private final Metamodel metamodel;
338         private final JpaMetamodel jpaMetamodel;
339
340         IdentifierDerivingDirectFieldAccessFallbackBeanWrapper(Class<?> type, Metamodel metamodel) {
341             super(type);
342             this.metamodel = metamodel;
343             this.jpaMetamodel = JpaMetamodel.of(metamodel);
344         }
345
346         /**
347          * In addition to the functionality described in {@link BeanWrapperImpl} it is checked whether we have a nested
348          * entity that is part of the id key. If this is the case, we need to derive the identifier of the nested entity.
349          */

350         @Override
351         @SuppressWarnings({ "unchecked""rawtypes" })
352         public void setPropertyValue(String propertyName, @Nullable Object value) {
353
354             if (!isIdentifierDerivationNecessary(value)) {
355                 super.setPropertyValue(propertyName, value);
356                 return;
357             }
358
359             // Derive the identifier from the nested entity that is part of the composite key.
360             JpaMetamodelEntityInformation nestedEntityInformation = new JpaMetamodelEntityInformation(
361                     ProxyUtils.getUserClass(value), this.metamodel);
362
363             if (!nestedEntityInformation.getJavaType().isAnnotationPresent(IdClass.class)) {
364
365                 Object nestedIdPropertyValue = new DirectFieldAccessFallbackBeanWrapper(value)
366                         .getPropertyValue(nestedEntityInformation.getRequiredIdAttribute().getName());
367                 super.setPropertyValue(propertyName, nestedIdPropertyValue);
368                 return;
369             }
370
371             // We have an IdClass property, we need to inspect the current value in order to map potentially multiple id
372             // properties correctly.
373
374             BeanWrapper sourceIdValueWrapper = new DirectFieldAccessFallbackBeanWrapper(value);
375             BeanWrapper targetIdClassTypeWrapper = new BeanWrapperImpl(nestedEntityInformation.getIdType());
376
377             for (String idAttributeName : (Iterable<String>) nestedEntityInformation.getIdAttributeNames()) {
378                 targetIdClassTypeWrapper.setPropertyValue(idAttributeName,
379                         extractActualIdPropertyValue(sourceIdValueWrapper, idAttributeName));
380             }
381
382             super.setPropertyValue(propertyName, targetIdClassTypeWrapper.getWrappedInstance());
383         }
384
385         @Nullable
386         private Object extractActualIdPropertyValue(BeanWrapper sourceIdValueWrapper, String idAttributeName) {
387
388             Object idPropertyValue = sourceIdValueWrapper.getPropertyValue(idAttributeName);
389
390             if (idPropertyValue != null) {
391
392                 Class<?> idPropertyValueType = idPropertyValue.getClass();
393
394                 if (!jpaMetamodel.isJpaManaged(idPropertyValueType)) {
395                     return idPropertyValue;
396                 }
397
398                 return new DirectFieldAccessFallbackBeanWrapper(idPropertyValue)
399                         .getPropertyValue(tryFindSingularIdAttributeNameOrUseFallback(idPropertyValueType, idAttributeName));
400             }
401
402             return null;
403         }
404
405         private String tryFindSingularIdAttributeNameOrUseFallback(Class<?> idPropertyValueType,
406                 String fallbackIdTypePropertyName) {
407
408             ManagedType<?> idPropertyType = metamodel.managedType(idPropertyValueType);
409             for (SingularAttribute<?, ?> sa : idPropertyType.getSingularAttributes()) {
410                 if (sa.isId()) {
411                     return sa.getName();
412                 }
413             }
414
415             return fallbackIdTypePropertyName;
416         }
417
418         /**
419          * @param value
420          * @return {@literal trueif the given value is not {@literal null} and a mapped persistable entity otherwise
421          *         {@literal false}
422          */

423         private boolean isIdentifierDerivationNecessary(@Nullable Object value) {
424
425             if (value == null) {
426                 return false;
427             }
428
429             Class<?> userClass = ProxyUtils.getUserClass(value);
430
431             if (!this.jpaMetamodel.isJpaManaged(userClass)) {
432                 return false;
433             }
434
435             ManagedType<?> managedType = this.metamodel.managedType(userClass);
436
437             if (managedType == null) {
438                 throw new IllegalStateException("ManagedType must not be null. We checked that it exists before.");
439             }
440
441             return managedType.getPersistenceType() == PersistenceType.ENTITY;
442         }
443     }
444 }
445