Skip to content

Commit a98d5c7

Browse files
committed
DATACMNS-630 - Integrate projection infrastructure from Spring Data REST.
Ported the projection infrastructure previously residing in Spring Data REST and extended it by defaulting to a Map-backed source to store and retrieve data. Separated out the SpEL based functionality mostly for SOC-reasons and easier testability. Related tickets: DATAREST-437, DATACMNS-618, DATACMNS-89.
1 parent 758d4b9 commit a98d5c7

13 files changed

+1693
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2015 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+
* http://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.projection;
17+
18+
import java.beans.PropertyDescriptor;
19+
import java.lang.reflect.Method;
20+
import java.util.Map;
21+
22+
import org.aopalliance.intercept.MethodInterceptor;
23+
import org.aopalliance.intercept.MethodInvocation;
24+
import org.springframework.beans.BeanUtils;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.ReflectionUtils;
27+
28+
/**
29+
* {@link MethodInterceptor} to support accessor methods to store and retrieve values from a {@link Map}.
30+
*
31+
* @author Oliver Gierke
32+
* @since 1.10
33+
*/
34+
class MapAccessingMethodInterceptor implements MethodInterceptor {
35+
36+
private final Map<String, Object> map;
37+
38+
/**
39+
* Creates a new {@link MapAccessingMethodInterceptor} for the given {@link Map}.
40+
*
41+
* @param map must not be {@literal null}.
42+
*/
43+
public MapAccessingMethodInterceptor(Map<String, Object> map) {
44+
45+
Assert.notNull(map, "Map must not be null!");
46+
this.map = map;
47+
}
48+
49+
/*
50+
* (non-Javadoc)
51+
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
52+
*/
53+
@Override
54+
public Object invoke(MethodInvocation invocation) throws Throwable {
55+
56+
Method method = invocation.getMethod();
57+
58+
if (ReflectionUtils.isObjectMethod(method)) {
59+
return invocation.proceed();
60+
}
61+
62+
Accessor accessor = new Accessor(method);
63+
64+
if (accessor.isGetter()) {
65+
return map.get(accessor.getPropertyName());
66+
} else if (accessor.isSetter()) {
67+
map.put(accessor.getPropertyName(), invocation.getArguments()[0]);
68+
return null;
69+
}
70+
71+
throw new IllegalStateException("Should never get here!");
72+
}
73+
74+
/**
75+
* Helper value to abstract an accessor.
76+
*
77+
* @author Oliver Gierke
78+
*/
79+
private static final class Accessor {
80+
81+
private final PropertyDescriptor descriptor;
82+
private final Method method;
83+
84+
/**
85+
* Creates an {@link Accessor} for the given {@link Method}.
86+
*
87+
* @param method must not be {@literal null}.
88+
* @throws IllegalArgumentException in case the given method is not an accessor method.
89+
*/
90+
public Accessor(Method method) {
91+
92+
Assert.notNull(method, "Method must not be null!");
93+
94+
this.descriptor = BeanUtils.findPropertyForMethod(method);
95+
this.method = method;
96+
97+
Assert.notNull(descriptor, String.format("Invoked method %s is no accessor method!", method));
98+
}
99+
100+
/**
101+
* Returns whether the acessor is a getter.
102+
*
103+
* @return
104+
*/
105+
public boolean isGetter() {
106+
return method.equals(descriptor.getReadMethod());
107+
}
108+
109+
/**
110+
* Returns whether the accessor is a setter.
111+
*
112+
* @return
113+
*/
114+
public boolean isSetter() {
115+
return method.equals(descriptor.getWriteMethod());
116+
}
117+
118+
/**
119+
* Returns the name of the property this accessor handles.
120+
*
121+
* @return will never be {@literal null}.
122+
*/
123+
public String getPropertyName() {
124+
return descriptor.getName();
125+
}
126+
}
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2014-2015 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+
* http://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.projection;
17+
18+
import java.util.Arrays;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
import java.util.Map.Entry;
23+
24+
import org.aopalliance.intercept.MethodInterceptor;
25+
import org.aopalliance.intercept.MethodInvocation;
26+
import org.springframework.core.CollectionFactory;
27+
import org.springframework.data.util.ClassTypeInformation;
28+
import org.springframework.data.util.TypeInformation;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ClassUtils;
31+
32+
/**
33+
* {@link MethodInterceptor} to delegate the invocation to a different {@link MethodInterceptor} but creating a
34+
* projecting proxy in case the returned value is not of the return type of the invoked method.
35+
*
36+
* @author Oliver Gierke
37+
* @since 1.10
38+
*/
39+
class ProjectingMethodInterceptor implements MethodInterceptor {
40+
41+
private final ProjectionFactory factory;
42+
private final MethodInterceptor delegate;
43+
44+
/**
45+
* Creates a new {@link ProjectingMethodInterceptor} using the given {@link ProjectionFactory} and delegate
46+
* {@link MethodInterceptor}.
47+
*
48+
* @param factory the {@link ProjectionFactory} to use to create projections if types do not match, must not be
49+
* {@literal null}..
50+
* @param delegate the {@link MethodInterceptor} to trigger to create the source value, must not be {@literal null}..
51+
*/
52+
public ProjectingMethodInterceptor(ProjectionFactory factory, MethodInterceptor delegate) {
53+
54+
Assert.notNull(factory, "ProjectionFactory must not be null!");
55+
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!");
56+
57+
this.factory = factory;
58+
this.delegate = delegate;
59+
}
60+
61+
/*
62+
* (non-Javadoc)
63+
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
64+
*/
65+
@Override
66+
public Object invoke(MethodInvocation invocation) throws Throwable {
67+
68+
Object result = delegate.invoke(invocation);
69+
70+
if (result == null) {
71+
return null;
72+
}
73+
74+
TypeInformation<?> type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod());
75+
76+
if (type.isCollectionLike()) {
77+
return projectCollectionElements(asCollection(result), type);
78+
} else if (type.isMap()) {
79+
return projectMapValues((Map<?, ?>) result, type);
80+
} else {
81+
return getProjection(result, type.getType());
82+
}
83+
}
84+
85+
/**
86+
* Creates projections of the given {@link Collection}'s elements if necessary and returns a new collection containing
87+
* the projection results.
88+
*
89+
* @param sources must not be {@literal null}.
90+
* @param type must not be {@literal null}.
91+
* @return
92+
*/
93+
private Collection<Object> projectCollectionElements(Collection<?> sources, TypeInformation<?> type) {
94+
95+
Collection<Object> result = CollectionFactory.createCollection(type.getType(), sources.size());
96+
97+
for (Object source : sources) {
98+
result.add(getProjection(source, type.getComponentType().getType()));
99+
}
100+
101+
return result;
102+
}
103+
104+
/**
105+
* Creates projections of the given {@link Map}'s values if necessary and returns an new {@link Map} with the handled
106+
* values.
107+
*
108+
* @param sources must not be {@literal null}.
109+
* @param type must not be {@literal null}.
110+
* @return
111+
*/
112+
private Map<Object, Object> projectMapValues(Map<?, ?> sources, TypeInformation<?> type) {
113+
114+
Map<Object, Object> result = CollectionFactory.createMap(type.getType(), sources.size());
115+
116+
for (Entry<?, ?> source : sources.entrySet()) {
117+
result.put(source.getKey(), getProjection(source.getValue(), type.getMapValueType().getType()));
118+
}
119+
120+
return result;
121+
}
122+
123+
private Object getProjection(Object result, Class<?> returnType) {
124+
return ClassUtils.isAssignable(returnType, result.getClass()) ? result : factory.createProjection(returnType,
125+
result);
126+
}
127+
128+
/**
129+
* Turns the given value into a {@link Collection}. Will turn an array into a collection an wrap all other values into
130+
* a single-element collection.
131+
*
132+
* @param source must not be {@literal null}.
133+
* @return
134+
*/
135+
private static Collection<?> asCollection(Object source) {
136+
137+
Assert.notNull(source, "Source object must not be null!");
138+
139+
if (source instanceof Collection) {
140+
return (Collection<?>) source;
141+
} else if (source.getClass().isArray()) {
142+
return Arrays.asList((Object[]) source);
143+
} else {
144+
return Collections.singleton(source);
145+
}
146+
}
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2014-2015 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+
* http://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.projection;
17+
18+
import java.util.List;
19+
20+
/**
21+
* A factory to create projecting instances for other objects usually used to allow easy creation of representation
22+
* projections to define which properties of a domain objects shall be exported in which way.
23+
*
24+
* @author Oliver Gierke
25+
* @since 1.10
26+
*/
27+
public interface ProjectionFactory {
28+
29+
/**
30+
* Creates a projection of the given type for the given source object. The individual mapping strategy is defined by
31+
* the implementations.
32+
*
33+
* @param projectionType the type to create, must not be {@literal null}.
34+
* @param source the object to create a projection for, can be {@literal null}
35+
* @return
36+
*/
37+
<T> T createProjection(Class<T> projectionType, Object source);
38+
39+
/**
40+
* Creates a pojection instance for the given type.
41+
*
42+
* @param projectionType the type to create, must not be {@literal null}.
43+
* @return
44+
*/
45+
<T> T createProjection(Class<T> projectionType);
46+
47+
/**
48+
* Returns the properties that will be consumed by the given projection type.
49+
*
50+
* @param projectionType must not be {@literal null}.
51+
* @return
52+
*/
53+
List<String> getInputProperties(Class<?> projectionType);
54+
}

0 commit comments

Comments
 (0)