/*
* Copyright 2015-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.repository.query;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
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.Set;
import java.util.stream.Collectors;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.util.Optionals;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A representation of the type returned by a {@link QueryMethod}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @since 1.12
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class ReturnedType {
private final @NonNull Class<?> domainType;
/**
* 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 returnedType.isInterface()
? new ReturnedInterface(factory.getProjectionInformation(returnedType), domainType)
: new ReturnedClass(returnedType, domainType);
}
/**
* Returns the entity type.
*
* @return
*/
public final Class<?> getDomainType() {
return domainType;
}
/**
* Returns whether the given source object is an instance of the returned type.
*
* @param source can be {@literal null}.
* @return
*/
public final boolean isInstance(Object source) {
return getReturnedType().isInstance(source);
}
/**
* 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.
*
* @return
*/
public abstract Class<?> getReturnedType();
/**
* 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. {@literal null} is
* returned to indicate a generic type (a map or tuple-like type) shall be used.
*
* @return
*/
public abstract Class<?> getTypeToRead();
/**
* Returns the properties required to be used to populate the result.
*
* @return
*/
public abstract List<String> getInputProperties();
/**
* 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 {@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.ResultFactory.ReturnedTypeInformation#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.ResultFactory.ReturnedType#isProjecting()
*/
@Override
public boolean isProjecting() {
return !information.getType().isAssignableFrom(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#getTypeToRead()
*/
@Override
public Class<?> getTypeToRead() {
return isProjecting() && information.isClosed() ? null : domainType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ResultFactory.ReturnedTypeInformation#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 {@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 {@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}.
* @param projectionInformation
*/
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()
*/
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;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<String> detectConstructorParameterNames(Class<?> type) {
if (!isDto()) {
return Collections.emptyList();
}
PreferredConstructorDiscoverer<?, ?> discoverer = new PreferredConstructorDiscoverer(type);
return discoverer.getConstructor().map(it -> it.getParameters().stream()//
.flatMap(parameter -> Optionals.toStream(parameter.getName()))//
.collect(Collectors.toList())).orElseGet(Collections::emptyList);
}
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);
}
}
}