content: /*
   * Copyright 2013-2023 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 com.alibaba.cloud.stream.binder.rocketmq.support;
  
  import java.io.IOException;
  import java.nio.charset.Charset;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  
  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.core.type.TypeReference;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import org.springframework.messaging.MessageHeaders;
  import org.springframework.util.ClassUtils;
  
  /**
   * jackson header mapper for RocketMQ. Header types are added to a special header
   * {@link #JSON_TYPES}.
   *
   * @author caotc
   * @since 2.1.1.RELEASE
   */
  public class JacksonRocketMQHeaderMapper extends AbstractRocketMQHeaderMapper {
  
  	private final static Logger log = LoggerFactory
  			.getLogger(JacksonRocketMQHeaderMapper.class);
  
  	private static final List<String> DEFAULT_TRUSTED_PACKAGES = Arrays
  			.asList("java.lang", "java.net", "java.util", "org.springframework.util");
  
  	/**
  	 * Header name for java types of other headers.
  	 */
  	public static final String JSON_TYPES = "spring_json_header_types";
  
  	private final ObjectMapper objectMapper;
  
  	private final Set<String> trustedPackages = new LinkedHashSet<>(
  			DEFAULT_TRUSTED_PACKAGES);
  
  	public JacksonRocketMQHeaderMapper(ObjectMapper objectMapper) {
  		this.objectMapper = objectMapper;
  	}
  
  	public JacksonRocketMQHeaderMapper(Charset charset, ObjectMapper objectMapper) {
  		super(charset);
  		this.objectMapper = objectMapper;
  	}
  
  	@Override
  	public Map<String, String> fromHeaders(MessageHeaders headers) {
  		final Map<String, String> target = new HashMap<>();
  		final Map<String, String> jsonHeaders = new HashMap<>();
  		headers.forEach((key, value) -> {
  			if (matches(key)) {
  				if (value instanceof String strValue) {
  					target.put(key, strValue);
  				}
  				else {
  					try {
  						String className = value.getClass().getName();
  						target.put(key, objectMapper.writeValueAsString(value));
  						jsonHeaders.put(key, className);
  					}
  					catch (Exception e) {
  						log.debug("Could not map " + key + " with type "
  								+ value.getClass().getName(), e);
  					}
  				}
  			}
  		});
  		if (jsonHeaders.size() > 0) {
  			try {
  				target.put(JSON_TYPES, objectMapper.writeValueAsString(jsonHeaders));
  			}
  			catch (IllegalStateException | JsonProcessingException e) {
  				log.error("Could not add json types header", e);
  			}
  		}
  		return target;
  	}
  
  	@Override
  	public MessageHeaders toHeaders(Map<String, String> source) {
  		final Map<String, Object> target = new HashMap<>();
  		final Map<String, String> jsonTypes = decodeJsonTypes(source);
  		source.forEach((key, value) -> {
  			if (matches(key) && !(key.equals(JSON_TYPES))) {
  				if (jsonTypes.containsKey(key)) {
  					Class<?> type = Object.class;
  					String requestedType = jsonTypes.get(key);
  					boolean trusted = trusted(requestedType);
  					if (trusted) {
  						try {
  							type = ClassUtils.forName(requestedType, null);
  						}
  						catch (Exception e) {
  							log.error("Could not load class for header: " + key, e);
  						}
  					}
  
  					if (trusted) {
  						try {
  							Object val = decodeValue(value, type);
  							target.put(key, val);
  						}
  						catch (IOException e) {
  							log.error("Could not decode json type: " + value
  									+ " for key: " + key, e);
  							target.put(key, value);
  						}
  					}
  					else {
  						target.put(key, new NonTrustedHeaderType(value, requestedType));
  					}
  				}
  				else {
  					target.put(key, value);
  				}
  			}
  		});
  		return new MessageHeaders(target);
  	}
  
  	/**
  	 * @param packagesToTrust the packages to trust.
  	 * @see #addTrustedPackages(Collection)
  	 */
  	public void addTrustedPackages(String... packagesToTrust) {
  		if (Objects.nonNull(packagesToTrust)) {
  			addTrustedPackages(Arrays.asList(packagesToTrust));
  		}
  	}
  
  	/**
  	 * Add packages to the trusted packages list (default {@code java.util, java.lang})
  	 * used when constructing objects from JSON. If any of the supplied packages is
  	 * {@code "*"}, all packages are trusted. If a class for a non-trusted package is
  	 * encountered, the header is returned to the application with value of type
  	 * {@link NonTrustedHeaderType}.
  	 * @param packagesToTrust the packages to trust.
  	 */
  	public void addTrustedPackages(Collection<String> packagesToTrust) {
  		if (packagesToTrust != null) {
  			for (String whiteList : packagesToTrust) {
  				if ("*".equals(whiteList)) {
  					this.trustedPackages.clear();
  					break;
  				}
  				else {
  					this.trustedPackages.add(whiteList);
  				}
  			}
  		}
  	}
  
  	public Set<String> getTrustedPackages() {
  		return this.trustedPackages;
  	}
  
  	public ObjectMapper getObjectMapper() {
  		return objectMapper;
  	}
  
  	private Object decodeValue(String jsonString, Class<?> type)
  			throws IOException, LinkageError {
  		Object value = objectMapper.readValue(jsonString, type);
  		if (type.equals(NonTrustedHeaderType.class)) {
  			// Upstream NTHT propagated; may be trusted here...
  			NonTrustedHeaderType nth = (NonTrustedHeaderType) value;
  			if (trusted(nth.getUntrustedType())) {
  				try {
  					value = objectMapper.readValue(nth.getHeaderValue(),
  							ClassUtils.forName(nth.getUntrustedType(), null));
  				}
  				catch (Exception e) {
  					log.error("Could not decode header: " + nth, e);
  				}
  			}
  		}
  		return value;
  	}
  
  	private Map<String, String> decodeJsonTypes(Map<String, String> source) {
  		if (source.containsKey(JSON_TYPES)) {
  			String value = source.get(JSON_TYPES);
  			try {
  				return objectMapper.readValue(value,
  						new TypeReference<Map<String, String>>() {
  						});
  			}
  			catch (IOException e) {
  				log.error("Could not decode json types: " + value, e);
  			}
  		}
  		return Collections.emptyMap();
  	}
  
  	protected boolean trusted(String requestedType) {
  		if (requestedType.equals(NonTrustedHeaderType.class.getName())) {
  			return true;
  		}
  		if (!this.trustedPackages.isEmpty()) {
  			int lastDot = requestedType.lastIndexOf('.');
  			if (lastDot < 0) {
  				return false;
  			}
  			String packageName = requestedType.substring(0, lastDot);
  			for (String trustedPackage : this.trustedPackages) {
  				if (packageName.equals(trustedPackage)
  						|| packageName.startsWith(trustedPackage + ".")) {
  					return true;
  				}
  			}
  			return false;
  		}
  		return true;
  	}
  
  	/**
  	 * Represents a header that could not be decoded due to an untrusted type.
  	 */
  	public static class NonTrustedHeaderType {
  
  		private String headerValue;
  
  		private String untrustedType;
  
  		public NonTrustedHeaderType() {
  			super();
  		}
  
  		NonTrustedHeaderType(String headerValue, String untrustedType) {
  			this.headerValue = headerValue;
  			this.untrustedType = untrustedType;
  		}
  
  		public void setHeaderValue(String headerValue) {
  			this.headerValue = headerValue;
  		}
  
  		public String getHeaderValue() {
  			return this.headerValue;
  		}
  
  		public void setUntrustedType(String untrustedType) {
  			this.untrustedType = untrustedType;
  		}
  
  		public String getUntrustedType() {
  			return this.untrustedType;
  		}
  
  		@Override
  		public String toString() {
  			return "NonTrustedHeaderType [headerValue=" + headerValue + ", untrustedType="
  					+ this.untrustedType + "]";
  		}
  
  	}
  
  }
  