/*
 * Copyright 2012-2019 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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

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.boot.autoconfigure.jackson.JacksonAutoConfiguration;
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.cloud.stream.annotation.StreamMessageConverter;
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.MimeTypeUtils;

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

	// needed in the case where @RemoteApplicationEventScan is not used
	// otherwise RemoteApplicationEventRegistrar will register the bean
	@Bean
	@ConditionalOnMissingBean(name = "busJsonConverter")
	@StreamMessageConverter
	public AbstractMessageConverter busJsonConverter(
			@Autowired(required = false) ObjectMapper objectMapper) {
		return new BusJacksonMessageConverter(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 ObjectMapper mapper;

	private final boolean mapperCreated;

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

	private BusJacksonMessageConverter() {
		this(null);
	}

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

		if (objectMapper != null) {
			this.mapper = objectMapper;
			this.mapperCreated = false;
		}
		else {
			this.mapper = new ObjectMapper();
			this.mapperCreated = true;
		}
	}

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

	/* for testing */ ObjectMapper getMapper() {
		return this.mapper;
	}

	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) {
		Object result = null;
		try {
			Object payload = message.getPayload();

			if (payload instanceof byte[]) {
				try {
					result = this.mapper.readValue((byte[]) payload, targetClass);
				}
				catch (InvalidTypeIdException e) {
					return new UnknownRemoteApplicationEvent(new Object(), e.getTypeId(),
							(byte[]) payload);
				}
			}
			else if (payload instanceof String) {
				try {
					result = this.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() throws Exception {
		this.mapper.registerModule(new SubtypeModule(findSubTypes()));
	}

}