/*
* Copyright 2015-2020 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
*
* https://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.repository.query;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
A representation of the type returned by a QueryMethod
. Author: Oliver Gierke, Christoph Strobl, Mark Paluch Since: 1.12
/**
* A representation of the type returned by a {@link QueryMethod}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.12
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class ReturnedType {
private static final Map<CacheKey, ReturnedType> CACHE = new ConcurrentReferenceHashMap<>(32);
private final @NonNull Class<?> domainType;
Creates a new ReturnedType
for the given returned type, domain type and ProjectionFactory
. Params: - returnedType – must not be null.
- domainType – must not be null.
- factory – must not be null.
Returns:
/**
* Creates a new {@link ReturnedType} for the given returned type, domain type and {@link ProjectionFactory}.
*
* @param returnedType must not be {@literal null}.
* @param domainType must not be {@literal null}.
* @param factory must not be {@literal null}.
* @return
*/
static ReturnedType of(Class<?> returnedType, Class<?> domainType, ProjectionFactory factory) {
Assert.notNull(returnedType, "Returned type must not be null!");
Assert.notNull(domainType, "Domain type must not be null!");
Assert.notNull(factory, "ProjectionFactory must not be null!");
return CACHE.computeIfAbsent(CacheKey.of(returnedType, domainType, factory.hashCode()), key -> {
return returnedType.isInterface()
? new ReturnedInterface(factory.getProjectionInformation(returnedType), domainType)
: new ReturnedClass(returnedType, domainType);
});
}
Returns the entity type.
Returns:
/**
* Returns the entity type.
*
* @return
*/
public final Class<?> getDomainType() {
return domainType;
}
Returns whether the given source object is an instance of the returned type.
Params: - source – can be null.
Returns:
/**
* Returns whether the given source object is an instance of the returned type.
*
* @param source can be {@literal null}.
* @return
*/
public final boolean isInstance(@Nullable Object source) {
return getReturnedType().isInstance(source);
}
Returns whether the type is projecting, i.e. not of the domain type.
Returns:
/**
* Returns whether the type is projecting, i.e. not of the domain type.
*
* @return
*/
public abstract boolean isProjecting();
Returns the type of the individual objects to return.
Returns:
/**
* Returns the type of the individual objects to return.
*
* @return
*/
public abstract Class<?> getReturnedType();
Returns whether the returned type will require custom construction.
Returns:
/**
* Returns whether the returned type will require custom construction.
*
* @return
*/
public abstract boolean needsCustomConstruction();
Returns the type that the query execution is supposed to pass to the underlying infrastructure. null is returned to indicate a generic type (a map or tuple-like type) shall be used. Returns:
/**
* Returns the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
* returned to indicate a generic type (a map or tuple-like type) shall be used.
*
* @return
*/
@Nullable
public abstract Class<?> getTypeToRead();
Returns the properties required to be used to populate the result.
Returns:
/**
* Returns the properties required to be used to populate the result.
*
* @return
*/
public abstract List<String> getInputProperties();
A ReturnedType
that's backed by an interface. Author: Oliver Gierke Since: 1.12
/**
* A {@link ReturnedType} that's backed by an interface.
*
* @author Oliver Gierke
* @since 1.12
*/
private static final class ReturnedInterface extends ReturnedType {
private final ProjectionInformation information;
private final Class<?> domainType;
Creates a new ReturnedInterface
from the given ProjectionInformation
and domain type. Params: - information – must not be null.
- domainType – must not be null.
/**
* Creates a new {@link ReturnedInterface} from the given {@link ProjectionInformation} and domain type.
*
* @param information must not be {@literal null}.
* @param domainType must not be {@literal null}.
*/
public ReturnedInterface(ProjectionInformation information, Class<?> domainType) {
super(domainType);
Assert.notNull(information, "Projection information must not be null!");
this.information = information;
this.domainType = domainType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReturnedType#getReturnedType()
*/
@Override
public Class<?> getReturnedType() {
return information.getType();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReturnedType#needsCustomConstruction()
*/
public boolean needsCustomConstruction() {
return isProjecting() && information.isClosed();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReturnedType#isProjecting()
*/
@Override
public boolean isProjecting() {
return !information.getType().isAssignableFrom(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReturnedType#getTypeToRead()
*/
@Nullable
@Override
public Class<?> getTypeToRead() {
return isProjecting() && information.isClosed() ? null : domainType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReturnedType#getInputProperties()
*/
@Override
public List<String> getInputProperties() {
List<String> properties = new ArrayList<>();
for (PropertyDescriptor descriptor : information.getInputProperties()) {
if (!properties.contains(descriptor.getName())) {
properties.add(descriptor.getName());
}
}
return properties;
}
}
A ReturnedType
that's backed by an actual class. Author: Oliver Gierke Since: 1.12
/**
* A {@link ReturnedType} that's backed by an actual class.
*
* @author Oliver Gierke
* @since 1.12
*/
private static final class ReturnedClass extends ReturnedType {
private static final Set<Class<?>> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class));
private final Class<?> type;
private final List<String> inputProperties;
Creates a new ReturnedClass
instance for the given returned type and domain type. Params: - returnedType – must not be null.
- domainType – must not be null.
/**
* Creates a new {@link ReturnedClass} instance for the given returned type and domain type.
*
* @param returnedType must not be {@literal null}.
* @param domainType must not be {@literal null}.
*/
public ReturnedClass(Class<?> returnedType, Class<?> domainType) {
super(domainType);
Assert.notNull(returnedType, "Returned type must not be null!");
Assert.notNull(domainType, "Domain type must not be null!");
Assert.isTrue(!returnedType.isInterface(), "Returned type must not be an interface!");
this.type = returnedType;
this.inputProperties = detectConstructorParameterNames(returnedType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getReturnedType()
*/
@Override
public Class<?> getReturnedType() {
return type;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedType#getTypeToRead()
*/
@Nonnull
public Class<?> getTypeToRead() {
return type;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedType#isProjecting()
*/
@Override
public boolean isProjecting() {
return isDto();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedType#needsCustomConstruction()
*/
public boolean needsCustomConstruction() {
return isDto() && !inputProperties.isEmpty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getInputProperties()
*/
@Override
public List<String> getInputProperties() {
return inputProperties;
}
private List<String> detectConstructorParameterNames(Class<?> type) {
if (!isDto()) {
return Collections.emptyList();
}
PreferredConstructor<?, ?> constructor = PreferredConstructorDiscoverer.discover(type);
if (constructor == null) {
return Collections.emptyList();
}
List<String> properties = new ArrayList<>(constructor.getConstructor().getParameterCount());
for (PreferredConstructor.Parameter<Object, ?> parameter : constructor.getParameters()) {
properties.add(parameter.getName());
}
return properties;
}
private boolean isDto() {
return !Object.class.equals(type) && //
!type.isEnum() && //
!isDomainSubtype() && //
!isPrimitiveOrWrapper() && //
!Number.class.isAssignableFrom(type) && //
!VOID_TYPES.contains(type) && //
!type.getPackage().getName().startsWith("java.");
}
private boolean isDomainSubtype() {
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
}
private boolean isPrimitiveOrWrapper() {
return ClassUtils.isPrimitiveOrWrapper(type);
}
}
@Value(staticConstructor = "of")
private static class CacheKey {
Class<?> returnedType, domainType;
int projectionFactoryHashCode;
}
}