/*
 * Copyright 2012-2014 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.liquibase;

import java.io.IOException;

import liquibase.servicelocator.DefaultPackageScanClassResolver;
import liquibase.servicelocator.PackageScanClassResolver;
import org.apache.commons.logging.Log;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

/**
 * Liquibase {@link PackageScanClassResolver} implementation that uses Spring's resource
 * scanning to locate classes. This variant is safe to use with Spring Boot packaged
 * executable JARs.
 *
 * @author Phillip Webb
 */
public class SpringPackageScanClassResolver extends DefaultPackageScanClassResolver {

	private final Log logger;

	public SpringPackageScanClassResolver(Log logger) {
		this.logger = logger;
	}

	@Override
	protected void findAllClasses(String packageName, ClassLoader loader) {
		MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(
				loader);
		try {
			Resource[] resources = scan(loader, packageName);
			for (Resource resource : resources) {
				Class<?> clazz = loadClass(loader, metadataReaderFactory, resource);
				if (clazz != null) {
					addFoundClass(clazz);
				}
			}
		}
		catch (IOException ex) {
			throw new IllegalStateException(ex);
		}
	}

	private Resource[] scan(ClassLoader loader, String packageName) throws IOException {
		ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
				loader);
		String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
				+ ClassUtils.convertClassNameToResourcePath(packageName) + "/**/*.class";
		Resource[] resources = resolver.getResources(pattern);
		return resources;
	}

	private Class<?> loadClass(ClassLoader loader, MetadataReaderFactory readerFactory,
			Resource resource) {
		try {
			MetadataReader reader = readerFactory.getMetadataReader(resource);
			return ClassUtils.forName(reader.getClassMetadata().getClassName(), loader);
		}
		catch (ClassNotFoundException ex) {
			handleFailure(resource, ex);
			return null;
		}
		catch (LinkageError ex) {
			handleFailure(resource, ex);
			return null;
		}
		catch (Throwable ex) {
			if (this.logger.isWarnEnabled()) {
				this.logger.warn(
						"Unexpected failure when loading class resource " + resource, ex);
			}
			return null;
		}
	}

	private void handleFailure(Resource resource, Throwable ex) {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(
					"Ignoring candidate class resource " + resource + " due to " + ex);
		}
	}

}
