/*
* 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.projection;
import lombok.extern.slf4j.Slf4j;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.springframework.beans.BeanUtils;
import org.springframework.core.type.MethodMetadata;
import org.springframework.data.type.MethodsMetadata;
import org.springframework.data.type.classreading.MethodsMetadataReader;
import org.springframework.data.type.classreading.MethodsMetadataReaderFactory;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
Default implementation of ProjectionInformation
. Exposes all properties of the type as required input properties. Author: Oliver Gierke, Christoph Strobl, Mark Paluch Since: 1.12
/**
* Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input
* properties.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.12
*/
class DefaultProjectionInformation implements ProjectionInformation {
private final Class<?> projectionType;
private final List<PropertyDescriptor> properties;
Creates a new DefaultProjectionInformation
for the given type. Params: - type – must not be null.
/**
* Creates a new {@link DefaultProjectionInformation} for the given type.
*
* @param type must not be {@literal null}.
*/
DefaultProjectionInformation(Class<?> type) {
Assert.notNull(type, "Projection type must not be null!");
this.projectionType = type;
this.properties = new PropertyDescriptorSource(type).getDescriptors();
}
/*
* (non-Javadoc)
* @see org.springframework.data.projection.ProjectionInformation#getType()
*/
@Override
public Class<?> getType() {
return projectionType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.projection.ProjectionInformation#getInputProperties()
*/
public List<PropertyDescriptor> getInputProperties() {
return properties.stream()//
.filter(this::isInputProperty)//
.distinct()//
.collect(Collectors.toList());
}
/*
* (non-Javadoc)
* @see org.springframework.data.projection.ProjectionInformation#isDynamic()
*/
@Override
public boolean isClosed() {
return this.properties.equals(getInputProperties());
}
Returns whether the given PropertyDescriptor
describes an input property for the projection, i.e. a property that needs to be present on the source to be able to create reasonable projections for the type the descriptor was looked up on. Params: - descriptor – will never be null.
Returns:
/**
* Returns whether the given {@link PropertyDescriptor} describes an input property for the projection, i.e. a
* property that needs to be present on the source to be able to create reasonable projections for the type the
* descriptor was looked up on.
*
* @param descriptor will never be {@literal null}.
* @return
*/
protected boolean isInputProperty(PropertyDescriptor descriptor) {
return true;
}
Returns whether the given PropertyDescriptor
has a getter that is a Java 8 default method. Params: - descriptor – must not be null.
Returns:
/**
* Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method.
*
* @param descriptor must not be {@literal null}.
* @return
*/
private static boolean hasDefaultGetter(PropertyDescriptor descriptor) {
Method method = descriptor.getReadMethod();
return method != null && method.isDefault();
}
Internal helper to detect PropertyDescriptor
instances for a given type. Author: Mark Paluch, Oliver Gierke Since: 2.1 @soundtrack The Meters - Cissy Strut (Here Comes The Meter Man)
/**
* Internal helper to detect {@link PropertyDescriptor} instances for a given type.
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.1
* @soundtrack The Meters - Cissy Strut (Here Comes The Meter Man)
*/
@Slf4j
private static class PropertyDescriptorSource {
private final Class<?> type;
private final Optional<MethodsMetadata> metadata;
Creates a new PropertyDescriptorSource
for the given type. Params: - type – must not be null.
/**
* Creates a new {@link PropertyDescriptorSource} for the given type.
*
* @param type must not be {@literal null}.
*/
PropertyDescriptorSource(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
this.type = type;
this.metadata = getMetadata(type);
}
Returns PropertyDescriptor
s for all properties exposed by the given type and all its super interfaces. Returns:
/**
* Returns {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces.
*
* @return
*/
List<PropertyDescriptor> getDescriptors() {
return collectDescriptors().distinct().collect(StreamUtils.toUnmodifiableList());
}
Recursively collects PropertyDescriptor
s for all properties exposed by the given type and all its super interfaces. Returns:
/**
* Recursively collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super
* interfaces.
*
* @return
*/
private Stream<PropertyDescriptor> collectDescriptors() {
Stream<PropertyDescriptor> allButDefaultGetters = Arrays.stream(BeanUtils.getPropertyDescriptors(type)) //
.filter(it -> !hasDefaultGetter(it));
Stream<PropertyDescriptor> ownDescriptors = metadata.map(it -> filterAndOrder(allButDefaultGetters, it))
.orElse(allButDefaultGetters);
Stream<PropertyDescriptor> superTypeDescriptors = metadata.map(this::fromMetadata) //
.orElseGet(this::fromType) //
.flatMap(it -> new PropertyDescriptorSource(it).collectDescriptors());
return Stream.concat(ownDescriptors, superTypeDescriptors);
}
Returns a Stream
of PropertyDescriptor
ordered following the given MethodsMetadata
only returning methods seen by the given MethodsMetadata
. Params: - source – must not be null.
- metadata – must not be null.
Returns:
/**
* Returns a {@link Stream} of {@link PropertyDescriptor} ordered following the given {@link MethodsMetadata} only
* returning methods seen by the given {@link MethodsMetadata}.
*
* @param source must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @return
*/
private static Stream<PropertyDescriptor> filterAndOrder(Stream<PropertyDescriptor> source,
MethodsMetadata metadata) {
Map<String, Integer> orderedMethods = getMethodOrder(metadata);
if (orderedMethods.isEmpty()) {
return source;
}
return source.filter(descriptor -> descriptor.getReadMethod() != null)
.filter(descriptor -> orderedMethods.containsKey(descriptor.getReadMethod().getName()))
.sorted(Comparator.comparingInt(left -> orderedMethods.get(left.getReadMethod().getName())));
}
Returns a Stream
of interfaces using the given MethodsMetadata
as primary source for ordering. Params: - metadata – must not be null.
Returns:
/**
* Returns a {@link Stream} of interfaces using the given {@link MethodsMetadata} as primary source for ordering.
*
* @param metadata must not be {@literal null}.
* @return
*/
private Stream<Class<?>> fromMetadata(MethodsMetadata metadata) {
return Arrays.stream(metadata.getInterfaceNames()).map(it -> findType(it, type.getInterfaces()));
}
Returns a Stream
of interfaces using the given type as primary source for ordering. Returns:
/**
* Returns a {@link Stream} of interfaces using the given type as primary source for ordering.
*
* @return
*/
private Stream<Class<?>> fromType() {
return Arrays.stream(type.getInterfaces());
}
Attempts to obtain MethodsMetadata
from Class
. Returns Optional
containing MethodsMetadata
if metadata was read successfully, Optional.empty()
otherwise. Params: - type – must not be null.
Returns: the optional MethodsMetadata
.
/**
* Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing
* {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise.
*
* @param type must not be {@literal null}.
* @return the optional {@link MethodsMetadata}.
*/
private static Optional<MethodsMetadata> getMetadata(Class<?> type) {
try {
MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader());
MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type));
return Optional.of(metadataReader.getMethodsMetadata());
} catch (IOException e) {
LOG.info("Couldn't read class metadata for {}. Input property calculation might fail!", type);
return Optional.empty();
}
}
Find the type with the given name in the given array of Class
. Params: - name – must not be null or empty.
- types – must not be null.
Returns:
/**
* Find the type with the given name in the given array of {@link Class}.
*
* @param name must not be {@literal null} or empty.
* @param types must not be {@literal null}.
* @return
*/
private static Class<?> findType(String name, Class<?>[] types) {
return Arrays.stream(types) //
.filter(it -> name.equals(it.getName())) //
.findFirst()
.orElseThrow(() -> new IllegalStateException(
String.format("Did not find type %s in %s!", name, Arrays.toString(types))));
}
Returns a Map
containing method name to its positional index according to MethodsMetadata
. Params: - metadata –
Returns:
/**
* Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}.
*
* @param metadata
* @return
*/
private static Map<String, Integer> getMethodOrder(MethodsMetadata metadata) {
List<String> methods = metadata.getMethods() //
.stream() //
.map(MethodMetadata::getMethodName) //
.distinct() //
.collect(Collectors.toList());
return IntStream.range(0, methods.size()) //
.boxed() //
.collect(Collectors.toMap(methods::get, i -> i));
}
}
}