1 /*
2  * Copyright 2016-2020 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  *      https://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.web;
17
18 import java.io.IOException;
19 import java.lang.reflect.Type;
20 import java.util.Map;
21
22 import org.springframework.beans.BeansException;
23 import org.springframework.beans.factory.BeanClassLoaderAware;
24 import org.springframework.beans.factory.BeanFactory;
25 import org.springframework.beans.factory.BeanFactoryAware;
26 import org.springframework.core.ResolvableType;
27 import org.springframework.core.annotation.AnnotationUtils;
28 import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
29 import org.springframework.http.HttpInputMessage;
30 import org.springframework.http.MediaType;
31 import org.springframework.http.converter.HttpMessageConverter;
32 import org.springframework.http.converter.HttpMessageNotReadableException;
33 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
34 import org.springframework.lang.Nullable;
35 import org.springframework.util.Assert;
36 import org.springframework.util.ConcurrentReferenceHashMap;
37
38 import com.fasterxml.jackson.databind.ObjectMapper;
39 import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
40
41 /**
42  * {@link HttpMessageConverter} implementation to enable projected JSON binding to interfaces annotated with
43  * {@link ProjectedPayload}.
44  *
45  * @author Oliver Gierke
46  * @author Christoph Strobl
47  * @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive)
48  * @since 1.13
49  */

50 public class ProjectingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter
51         implements BeanClassLoaderAware, BeanFactoryAware {
52
53     private final SpelAwareProxyProjectionFactory projectionFactory;
54     private final Map<Class<?>, Boolean> supportedTypesCache = new ConcurrentReferenceHashMap<>();
55
56     /**
57      * Creates a new {@link ProjectingJackson2HttpMessageConverter} using a default {@link ObjectMapper}.
58      */

59     public ProjectingJackson2HttpMessageConverter() {
60         this.projectionFactory = initProjectionFactory(getObjectMapper());
61     }
62
63     /**
64      * Creates a new {@link ProjectingJackson2HttpMessageConverter} for the given {@link ObjectMapper}.
65      *
66      * @param mapper must not be {@literal null}.
67      */

68     public ProjectingJackson2HttpMessageConverter(ObjectMapper mapper) {
69
70         super(mapper);
71
72         this.projectionFactory = initProjectionFactory(mapper);
73     }
74
75     /**
76      * Creates a new {@link SpelAwareProxyProjectionFactory} with the {@link JsonProjectingMethodInterceptorFactory}
77      * registered for the given {@link ObjectMapper}.
78      *
79      * @param mapper must not be {@literal null}.
80      * @return
81      */

82     private static SpelAwareProxyProjectionFactory initProjectionFactory(ObjectMapper mapper) {
83
84         Assert.notNull(mapper, "ObjectMapper must not be null!");
85
86         SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
87         projectionFactory
88                 .registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory(new JacksonMappingProvider(mapper)));
89
90         return projectionFactory;
91     }
92
93     /*
94      * (non-Javadoc)
95      * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
96      */

97     @Override
98     public void setBeanClassLoader(ClassLoader classLoader) {
99         projectionFactory.setBeanClassLoader(classLoader);
100     }
101
102     /*
103      * (non-Javadoc)
104      * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
105      */

106     @Override
107     public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
108         projectionFactory.setBeanFactory(beanFactory);
109     }
110
111     /*
112      * (non-Javadoc)
113      * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canRead(java.lang.reflect.Type, java.lang.Class, org.springframework.http.MediaType)
114      */

115     @Override
116     public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
117
118         if (!canRead(mediaType)) {
119             return false;
120         }
121
122         ResolvableType owner = contextClass == null ? null : ResolvableType.forClass(contextClass);
123         Class<?> rawType = ResolvableType.forType(type, owner).resolve(Object.class);
124         Boolean result = supportedTypesCache.get(rawType);
125
126         if (result != null) {
127             return result;
128         }
129
130         result = rawType.isInterface() && AnnotationUtils.findAnnotation(rawType, ProjectedPayload.class) != null;
131         supportedTypesCache.put(rawType, result);
132
133         return result;
134     }
135
136     /*
137      * (non-Javadoc)
138      * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canWrite(java.lang.Class, org.springframework.http.MediaType)
139      */

140     @Override
141     public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
142         return false;
143     }
144
145     /*
146      * (non-Javadoc)
147      * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#read(java.lang.reflect.Type, java.lang.Class, org.springframework.http.HttpInputMessage)
148      */

149     @Override
150     public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
151             throws IOException, HttpMessageNotReadableException {
152         return projectionFactory.createProjection(ResolvableType.forType(type).resolve(Object.class),
153                 inputMessage.getBody());
154     }
155 }
156