/* * Copyright 2011-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.jpa.repository.support; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import javax.persistence.IdClass; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import javax.persistence.metamodel.Type.PersistenceType; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * Implementation of {@link org.springframework.data.repository.core.EntityInformation} that uses JPA {@link Metamodel} * to find the domain class' id field. * * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch */ public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> { private final IdMetadata<T> idMetadata; private final SingularAttribute<? super T, ?> versionAttribute; private final Metamodel metamodel; private final String entityName; /** * Creates a new {@link JpaMetamodelEntityInformation} for the given domain class and {@link Metamodel}. * * @param domainClass must not be {@literal null}. * @param metamodel must not be {@literal null}. */ public JpaMetamodelEntityInformation(Class<T> domainClass, Metamodel metamodel) { super(domainClass); Assert.notNull(metamodel, "Metamodel must not be null!"); this.metamodel = metamodel; ManagedType<T> type = metamodel.managedType(domainClass); if (type == null) { throw new IllegalArgumentException("The given domain class can not be found in the given Metamodel!"); } this.entityName = type instanceof EntityType ? ((EntityType<?>) type).getName() : null; if (!(type instanceof IdentifiableType)) { throw new IllegalArgumentException("The given domain class does not contain an id attribute!"); } IdentifiableType<T> identifiableType = (IdentifiableType<T>) type; this.idMetadata = new IdMetadata<T>(identifiableType); this.versionAttribute = findVersionAttribute(identifiableType, metamodel); } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.support.JpaEntityInformationSupport#getEntityName() */ @Override public String getEntityName() { return entityName != null ? entityName : super.getEntityName(); } /** * Returns the version attribute of the given {@link ManagedType} or {@literal null} if none available. * * @param type must not be {@literal null}. * @param metamodel must not be {@literal null}. * @return */ @SuppressWarnings("unchecked") private static <T> SingularAttribute<? super T, ?> findVersionAttribute(IdentifiableType<T> type, Metamodel metamodel) { try { return type.getVersion(Object.class); } catch (IllegalArgumentException o_O) { // Needs workarounds as the method is implemented with a strict type check on e.g. Hibernate < 4.3 } Set<SingularAttribute<? super T, ?>> attributes = type.getSingularAttributes(); for (SingularAttribute<? super T, ?> attribute : attributes) { if (attribute.isVersion()) { return attribute; } } Class<?> superType = type.getJavaType().getSuperclass(); try { ManagedType<?> managedSuperType = metamodel.managedType(superType); if (!(managedSuperType instanceof IdentifiableType)) { return null; } return (SingularAttribute<? super T, ?>) findVersionAttribute((IdentifiableType<T>) managedSuperType, metamodel); } catch (IllegalArgumentException o_O) { return null; } } /* * (non-Javadoc) * @see org.springframework.data.repository.core.EntityInformation#getId(java.lang.Object) */ @SuppressWarnings("unchecked") public Optional<ID> getId(T entity) { BeanWrapper entityWrapper = new DirectFieldAccessFallbackBeanWrapper(entity); if (idMetadata.hasSimpleId()) { return Optional.ofNullable((ID) entityWrapper.getPropertyValue(idMetadata.getSimpleIdAttribute().getName())); } BeanWrapper idWrapper = new IdentifierDerivingDirectFieldAccessFallbackBeanWrapper(idMetadata.getType(), metamodel); boolean partialIdValueFound = false; for (SingularAttribute<? super T, ?> attribute : idMetadata) { Object propertyValue = entityWrapper.getPropertyValue(attribute.getName()); if (propertyValue != null) { partialIdValueFound = true; } idWrapper.setPropertyValue(attribute.getName(), propertyValue); } return partialIdValueFound ? Optional.ofNullable((ID) idWrapper.getWrappedInstance()) : Optional.empty(); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.EntityInformation#getIdType() */ @SuppressWarnings("unchecked") public Class<ID> getIdType() { return (Class<ID>) idMetadata.getType(); } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getIdAttribute() */ public SingularAttribute<? super T, ?> getIdAttribute() { return idMetadata.getSimpleIdAttribute(); } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#hasCompositeId() */ public boolean hasCompositeId() { return !idMetadata.hasSimpleId(); } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getIdAttributeNames() */ public Iterable<String> getIdAttributeNames() { List<String> attributeNames = new ArrayList<String>(idMetadata.attributes.size()); for (SingularAttribute<? super T, ?> attribute : idMetadata.attributes) { attributeNames.add(attribute.getName()); } return attributeNames; } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.support.JpaEntityInformation#getCompositeIdAttributeValue(java.lang.Object, java.lang.String) */ public Object getCompositeIdAttributeValue(Object id, String idAttribute) { Assert.isTrue(hasCompositeId(), "Model must have a composite Id!"); return new DirectFieldAccessFallbackBeanWrapper(id).getPropertyValue(idAttribute); } /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.AbstractEntityInformation#isNew(java.lang.Object) */ @Override public boolean isNew(T entity) { if (versionAttribute == null || versionAttribute.getJavaType().isPrimitive()) { return super.isNew(entity); } BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity); Object versionValue = wrapper.getPropertyValue(versionAttribute.getName()); return versionValue == null; } /** * Simple value object to encapsulate id specific metadata. * * @author Oliver Gierke * @author Thomas Darimont */ private static class IdMetadata<T> implements Iterable<SingularAttribute<? super T, ?>> { private final IdentifiableType<T> type; private final Set<SingularAttribute<? super T, ?>> attributes; private Class<?> idType; @SuppressWarnings("unchecked") public IdMetadata(IdentifiableType<T> source) { this.type = source; this.attributes = (Set<SingularAttribute<? super T, ?>>) (source.hasSingleIdAttribute() ? Collections.singleton(source.getId(source.getIdType().getJavaType())) : source.getIdClassAttributes()); } public boolean hasSimpleId() { return attributes.size() == 1; } public Class<?> getType() { if (idType != null) { return idType; } // lazy initialization of idType field with tolerable benign data-race this.idType = tryExtractIdTypeWithFallbackToIdTypeLookup(); return this.idType; } private Class<?> tryExtractIdTypeWithFallbackToIdTypeLookup() { try { Type<?> idType2 = type.getIdType(); return idType2 == null ? fallbackIdTypeLookup(type) : idType2.getJavaType(); } catch (IllegalStateException e) { // see https://hibernate.onjira.com/browse/HHH-6951 return fallbackIdTypeLookup(type); } } private static Class<?> fallbackIdTypeLookup(IdentifiableType<?> type) { IdClass annotation = AnnotationUtils.findAnnotation(type.getJavaType(), IdClass.class); return annotation == null ? null : annotation.value(); } public SingularAttribute<? super T, ?> getSimpleIdAttribute() { return attributes.iterator().next(); } /* * (non-Javadoc) * @see java.lang.Iterable#iterator() */ public Iterator<SingularAttribute<? super T, ?>> iterator() { return attributes.iterator(); } } /** * Custom extension of {@link DirectFieldAccessFallbackBeanWrapper} that allows to derive the identifier if composite * keys with complex key attribute types (e.g. types that are annotated with {@code @Entity} themselves) are used. * * @author Thomas Darimont */ private static class IdentifierDerivingDirectFieldAccessFallbackBeanWrapper extends DirectFieldAccessFallbackBeanWrapper { private final Metamodel metamodel; public IdentifierDerivingDirectFieldAccessFallbackBeanWrapper(Class<?> type, Metamodel metamodel) { super(type); this.metamodel = metamodel; } /** * In addition to the functionality described in {@link BeanWrapperImpl} it is checked whether we have a nested * entity that is part of the id key. If this is the case, we need to derive the identifier of the nested entity. */ @Override @SuppressWarnings("unchecked") public void setPropertyValue(String propertyName, Object value) { if (!isIdentifierDerivationNecessary(value)) { super.setPropertyValue(propertyName, value); return; } // Derive the identifier from the nested entity that is part of the composite key. @SuppressWarnings("rawtypes") JpaMetamodelEntityInformation nestedEntityInformation = new JpaMetamodelEntityInformation(value.getClass(), this.metamodel); if (!nestedEntityInformation.getJavaType().isAnnotationPresent(IdClass.class)) { Object nestedIdPropertyValue = new DirectFieldAccessFallbackBeanWrapper(value) .getPropertyValue(nestedEntityInformation.getIdAttribute().getName()); super.setPropertyValue(propertyName, nestedIdPropertyValue); return; } // We have an IdClass property, we need to inspect the current value in order to map potentially multiple id // properties correctly. BeanWrapper sourceIdValueWrapper = new DirectFieldAccessFallbackBeanWrapper(value); BeanWrapper targetIdClassTypeWrapper = new BeanWrapperImpl(nestedEntityInformation.getIdType()); for (String idAttributeName : (Iterable<String>) nestedEntityInformation.getIdAttributeNames()) { targetIdClassTypeWrapper.setPropertyValue(idAttributeName, extractActualIdPropertyValue(sourceIdValueWrapper, idAttributeName)); } super.setPropertyValue(propertyName, targetIdClassTypeWrapper.getWrappedInstance()); } private Object extractActualIdPropertyValue(BeanWrapper sourceIdValueWrapper, String idAttributeName) { Object idPropertyValue = sourceIdValueWrapper.getPropertyValue(idAttributeName); if (idPropertyValue != null) { Class<? extends Object> idPropertyValueType = idPropertyValue.getClass(); if (ClassUtils.isPrimitiveOrWrapper(idPropertyValueType)) { return idPropertyValue; } return new DirectFieldAccessFallbackBeanWrapper(idPropertyValue) .getPropertyValue(tryFindSingularIdAttributeNameOrUseFallback(idPropertyValueType, idAttributeName)); } return null; } private String tryFindSingularIdAttributeNameOrUseFallback(Class<? extends Object> idPropertyValueType, String fallbackIdTypePropertyName) { ManagedType<? extends Object> idPropertyType = metamodel.managedType(idPropertyValueType); for (SingularAttribute<?, ?> sa : idPropertyType.getSingularAttributes()) { if (sa.isId()) { return sa.getName(); } } return fallbackIdTypePropertyName; } /** * @param value * @return {@literal true} if the given value is not {@literal null} and a mapped persistable entity otherwise * {@literal false} */ private boolean isIdentifierDerivationNecessary(Object value) { if (value == null) { return false; } try { ManagedType<? extends Object> managedType = this.metamodel.managedType(value.getClass()); return managedType != null && managedType.getPersistenceType() == PersistenceType.ENTITY; } catch (IllegalArgumentException iae) { // no mapped type return false; } } } }