BusJacksonAutoConfiguration.java

/*
 * Copyright 2012-present 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.cloud.bus.jackson;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.cbor.CBORFactory;
import tools.jackson.dataformat.cbor.CBORMapper;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.bus.BusAutoConfiguration;
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
import org.springframework.cloud.bus.endpoint.RefreshBusEndpoint;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.event.UnknownRemoteApplicationEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;

/**
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Donovan Muller
 * @author Stefan Pfeiffer
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnBusEnabled
@ConditionalOnClass({ RefreshBusEndpoint.class, ObjectMapper.class })
@AutoConfigureBefore(value = BusAutoConfiguration.class,
		name = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
public class BusJacksonAutoConfiguration {

	// needed in the case where @RemoteApplicationEventScan is not used
	// otherwise RemoteApplicationEventRegistrar will register the bean
	@Bean
	@ConditionalOnMissingBean(name = "busJsonConverter")
	public AbstractMessageConverter busJsonConverter(@Autowired(required = false) JsonMapper objectMapper) {
		return new BusJacksonMessageConverter(objectMapper);
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(CBORFactory.class)
	protected static class CborConfiguration {

		@Bean
		public AbstractMessageConverter busCborConverter(CBORMapper objectMapper) {
			return new BusJacksonMessageConverter(new MimeType("application", "cbor"), objectMapper);
		}

	}

}

class BusJacksonMessageConverter extends AbstractMessageConverter implements InitializingBean {

	private static final Log log = LogFactory.getLog(BusJacksonMessageConverter.class);

	private static final String DEFAULT_PACKAGE = ClassUtils.getPackageName(RemoteApplicationEvent.class);

	private final MapperBuilder mapperBuilder;

	private final boolean mapperCreated;

	private String[] packagesToScan = new String[] { DEFAULT_PACKAGE };

	private BusJacksonMessageConverter() {
		this(null);
	}

	@Autowired(required = false)
	BusJacksonMessageConverter(@Nullable ObjectMapper objectMapper) {
		this(MimeTypeUtils.APPLICATION_JSON, objectMapper);
	}

	@Autowired(required = false)
	BusJacksonMessageConverter(MimeType mimeType, @Nullable ObjectMapper objectMapper) {
		super(mimeType);

		if (objectMapper != null) {
			this.mapperBuilder = objectMapper.rebuild();
			this.mapperCreated = false;
		}
		else {
			this.mapperBuilder = JsonMapper.builder();
			this.mapperCreated = true;
		}
	}

	/* for testing */ boolean isMapperCreated() {
		return this.mapperCreated;
	}

	/* for testing */ MapperBuilder getMapper() {
		return this.mapperBuilder;
	}

	public void setPackagesToScan(String[] packagesToScan) {
		List<String> packages = new ArrayList<>(Arrays.asList(packagesToScan));
		if (!packages.contains(DEFAULT_PACKAGE)) {
			packages.add(DEFAULT_PACKAGE);
		}
		this.packagesToScan = packages.toArray(new String[0]);
	}

	private Class<?>[] findSubTypes() {
		List<Class<?>> types = new ArrayList<>();
		if (this.packagesToScan != null) {
			for (String pkg : this.packagesToScan) {
				ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
						false);
				provider.addIncludeFilter(new AssignableTypeFilter(RemoteApplicationEvent.class));

				Set<BeanDefinition> components = provider.findCandidateComponents(pkg);
				for (BeanDefinition component : components) {
					try {
						types.add(Class.forName(component.getBeanClassName()));
					}
					catch (ClassNotFoundException e) {
						throw new IllegalStateException("Failed to scan classpath for remote event classes", e);
					}
				}
			}
		}
		if (log.isDebugEnabled()) {
			log.debug("Found sub types: " + types);
		}
		return types.toArray(new Class<?>[0]);
	}

	@Override
	protected boolean supports(Class<?> aClass) {
		// This converter applies only to RemoteApplicationEvent and subclasses
		return RemoteApplicationEvent.class.isAssignableFrom(aClass);
	}

	@Override
	public Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
		ObjectMapper mapper = this.mapperBuilder.build();
		Object result = null;
		try {
			Object payload = message.getPayload();

			if (payload instanceof byte[]) {
				try {
					result = mapper.readValue((byte[]) payload, targetClass);
				}
				catch (InvalidTypeIdException e) {
					return new UnknownRemoteApplicationEvent(new Object(), e.getTypeId(), (byte[]) payload);
				}
			}
			else if (payload instanceof String) {
				try {
					result = mapper.readValue((String) payload, targetClass);
				}
				catch (InvalidTypeIdException e) {
					return new UnknownRemoteApplicationEvent(new Object(), e.getTypeId(),
							((String) payload).getBytes());
				}
				// workaround for
				// https://github.com/spring-cloud/spring-cloud-stream/issues/1564
			}
			else if (payload instanceof RemoteApplicationEvent) {
				return payload;
			}
		}
		catch (Exception e) {
			this.logger.error(e.getMessage(), e);
			return null;
		}
		return result;
	}

	@Override
	public void afterPropertiesSet() {
		this.mapperBuilder.subtypeResolver().registerSubtypes(findSubTypes());
	}

}