001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.codec.json; 018 019import java.lang.annotation.Annotation; 020import java.lang.reflect.Type; 021import java.util.Arrays; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026 027import com.fasterxml.jackson.annotation.JsonView; 028import com.fasterxml.jackson.databind.JavaType; 029import com.fasterxml.jackson.databind.ObjectMapper; 030import org.apache.commons.logging.Log; 031 032import org.springframework.core.GenericTypeResolver; 033import org.springframework.core.MethodParameter; 034import org.springframework.core.ResolvableType; 035import org.springframework.core.codec.Hints; 036import org.springframework.http.HttpLogging; 037import org.springframework.http.server.reactive.ServerHttpRequest; 038import org.springframework.http.server.reactive.ServerHttpResponse; 039import org.springframework.lang.Nullable; 040import org.springframework.util.Assert; 041import org.springframework.util.MimeType; 042import org.springframework.util.ObjectUtils; 043 044/** 045 * Base class providing support methods for Jackson 2.9 encoding and decoding. 046 * 047 * @author Sebastien Deleuze 048 * @author Rossen Stoyanchev 049 * @since 5.0 050 */ 051public abstract class Jackson2CodecSupport { 052 053 /** 054 * The key for the hint to specify a "JSON View" for encoding or decoding 055 * with the value expected to be a {@link Class}. 056 * @see <a href="https://www.baeldung.com/jackson-json-view-annotation">Jackson JSON Views</a> 057 */ 058 public static final String JSON_VIEW_HINT = Jackson2CodecSupport.class.getName() + ".jsonView"; 059 060 /** 061 * The key for the hint to access the actual ResolvableType passed into 062 * {@link org.springframework.http.codec.HttpMessageReader#read(ResolvableType, ResolvableType, ServerHttpRequest, ServerHttpResponse, Map)} 063 * (server-side only). Currently set when the method argument has generics because 064 * in case of reactive types, use of {@code ResolvableType.getGeneric()} means no 065 * MethodParameter source and no knowledge of the containing class. 066 */ 067 static final String ACTUAL_TYPE_HINT = Jackson2CodecSupport.class.getName() + ".actualType"; 068 069 private static final String JSON_VIEW_HINT_ERROR = 070 "@JsonView only supported for write hints with exactly 1 class argument: "; 071 072 private static final List<MimeType> DEFAULT_MIME_TYPES = Collections.unmodifiableList( 073 Arrays.asList( 074 new MimeType("application", "json"), 075 new MimeType("application", "*+json"))); 076 077 078 protected final Log logger = HttpLogging.forLogName(getClass()); 079 080 private final ObjectMapper objectMapper; 081 082 private final List<MimeType> mimeTypes; 083 084 085 /** 086 * Constructor with a Jackson {@link ObjectMapper} to use. 087 */ 088 protected Jackson2CodecSupport(ObjectMapper objectMapper, MimeType... mimeTypes) { 089 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 090 this.objectMapper = objectMapper; 091 this.mimeTypes = !ObjectUtils.isEmpty(mimeTypes) ? 092 Collections.unmodifiableList(Arrays.asList(mimeTypes)) : DEFAULT_MIME_TYPES; 093 } 094 095 096 public ObjectMapper getObjectMapper() { 097 return this.objectMapper; 098 } 099 100 /** 101 * Subclasses should expose this as "decodable" or "encodable" mime types. 102 */ 103 protected List<MimeType> getMimeTypes() { 104 return this.mimeTypes; 105 } 106 107 108 protected boolean supportsMimeType(@Nullable MimeType mimeType) { 109 return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType))); 110 } 111 112 protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) { 113 return this.objectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass)); 114 } 115 116 protected Map<String, Object> getHints(ResolvableType resolvableType) { 117 MethodParameter param = getParameter(resolvableType); 118 if (param != null) { 119 Map<String, Object> hints = null; 120 if (resolvableType.hasGenerics()) { 121 hints = new HashMap<>(2); 122 hints.put(ACTUAL_TYPE_HINT, resolvableType); 123 } 124 JsonView annotation = getAnnotation(param, JsonView.class); 125 if (annotation != null) { 126 Class<?>[] classes = annotation.value(); 127 Assert.isTrue(classes.length == 1, JSON_VIEW_HINT_ERROR + param); 128 hints = (hints != null ? hints : new HashMap<>(1)); 129 hints.put(JSON_VIEW_HINT, classes[0]); 130 } 131 if (hints != null) { 132 return hints; 133 } 134 } 135 return Hints.none(); 136 } 137 138 @Nullable 139 protected MethodParameter getParameter(ResolvableType type) { 140 return (type.getSource() instanceof MethodParameter ? (MethodParameter) type.getSource() : null); 141 } 142 143 @Nullable 144 protected abstract <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType); 145 146}