/*
 * Copyright 2012-2017 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
 *
 *      http://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.boot.actuate.endpoint;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadata;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvider;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Primary;

/**
 * A {@link PublicMetrics} implementation that provides data source usage statistics.
 *
 * @author Stephane Nicoll
 * @since 1.2.0
 */
public class DataSourcePublicMetrics implements PublicMetrics {

	private static final String DATASOURCE_SUFFIX = "dataSource";

	@Autowired
	private ApplicationContext applicationContext;

	@Autowired
	private Collection<DataSourcePoolMetadataProvider> providers;

	private final Map<String, DataSourcePoolMetadata> metadataByPrefix = new HashMap<>();

	@PostConstruct
	public void initialize() {
		DataSource primaryDataSource = getPrimaryDataSource();
		DataSourcePoolMetadataProvider provider = new DataSourcePoolMetadataProviders(
				this.providers);
		for (Map.Entry<String, DataSource> entry : this.applicationContext
				.getBeansOfType(DataSource.class).entrySet()) {
			String beanName = entry.getKey();
			DataSource bean = entry.getValue();
			String prefix = createPrefix(beanName, bean, bean.equals(primaryDataSource));
			DataSourcePoolMetadata poolMetadata = provider
					.getDataSourcePoolMetadata(bean);
			if (poolMetadata != null) {
				this.metadataByPrefix.put(prefix, poolMetadata);
			}
		}
	}

	@Override
	public Collection<Metric<?>> metrics() {
		Set<Metric<?>> metrics = new LinkedHashSet<>();
		for (Map.Entry<String, DataSourcePoolMetadata> entry : this.metadataByPrefix
				.entrySet()) {
			String prefix = entry.getKey();
			prefix = (prefix.endsWith(".") ? prefix : prefix + ".");
			DataSourcePoolMetadata metadata = entry.getValue();
			addMetric(metrics, prefix + "active", metadata.getActive());
			addMetric(metrics, prefix + "usage", metadata.getUsage());
		}
		return metrics;
	}

	private <T extends Number> void addMetric(Set<Metric<?>> metrics, String name,
			T value) {
		if (value != null) {
			metrics.add(new Metric<>(name, value));
		}
	}

	/**
	 * Create the prefix to use for the metrics to associate with the given
	 * {@link DataSource}.
	 * @param name the name of the data source bean
	 * @param dataSource the data source to configure
	 * @param primary if this data source is the primary data source
	 * @return a prefix for the given data source
	 */
	protected String createPrefix(String name, DataSource dataSource, boolean primary) {
		if (primary) {
			return "datasource.primary";
		}
		if (name.length() > DATASOURCE_SUFFIX.length()
				&& name.toLowerCase().endsWith(DATASOURCE_SUFFIX.toLowerCase())) {
			name = name.substring(0, name.length() - DATASOURCE_SUFFIX.length());
		}
		return "datasource." + name;
	}

	/**
	 * Attempt to locate the primary {@link DataSource} (i.e. either the only data source
	 * available or the one amongst the candidates marked as {@link Primary}). Return
	 * {@code null} if there no primary data source could be found.
	 * @return the primary datasource
	 */
	private DataSource getPrimaryDataSource() {
		try {
			return this.applicationContext.getBean(DataSource.class);
		}
		catch (NoSuchBeanDefinitionException ex) {
			return null;
		}
	}

}
