/***************************************************************************
 * Copyright 2014 Kieker Project (http://kieker-monitoring.net)
 *
 * 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 kieker.webgui.persistence.impl.utility;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import javax.annotation.Nullable;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.ClassContainer;

import org.springframework.stereotype.Service;

/**
 * This is a service which can be used to search for plugins and repositories within a given jar file. The classes are loaded using the reflection API.
 * 
 * @author Nils Christian Ehmke
 */
@Service
public final class PluginFinder {

	private static final Log LOG = LogFactory.getLog(PluginFinder.class);

	/**
	 * Default constructor. <b>Do not use this constructor. This bean is Spring managed.</b>
	 */
	public PluginFinder() {
		// No code necessary
	}

	/**
	 * This method delivers all non abstract classes which are available in the given jar, have the {@link kieker.analysis.repository.annotation.Repository}
	 * annotation, and are compatible with {@link kieker.analysis.repository.AbstractRepository}.
	 * 
	 * @param jarURL
	 *            The URL for the jar.
	 * @param classLoader
	 *            The class loader which should be used to load the classes.
	 * @param classContainer
	 *            The container for the necessary reflection classes.
	 * 
	 * @return A list containing all available repository-classes.
	 * 
	 * @throws IOException
	 *             If something went wrong during the opening of the jar file.
	 */
	public Collection<Class<?>> getAllRepositoriesWithinJar(final URL jarURL, final ClassLoader classLoader, final ClassContainer classContainer)
			throws IOException {
		// Get all classes within the Jar. Filter the classes which are not repositories. Filter the abstract classes.
		return Collections2.filter(Collections2.filter(this.getAllClassesWithinJar(jarURL, classLoader), new IsRepositoryPredicate(classContainer)),
				new IsNotAbstractPredicate());
	}

	/**
	 * This method delivers all non abstract classes which are available in the given jar, have the {@link kieker.analysis.plugin.annotation.Plugin} annotation, and
	 * are compatible with {@link kieker.analysis.plugin.reader.AbstractReaderPlugin}.
	 * 
	 * @param jarURL
	 *            The URL for the jar.
	 * @param classLoader
	 *            The class loader which should be used to load the classes.
	 * @param classContainer
	 *            The container for the necessary reflection classes.
	 * 
	 * @return A list containing all available plugin-classes.
	 * 
	 * @throws IOException
	 *             If something went wrong during the opening of the jar file.
	 */
	public Collection<Class<?>> getAllReadersWithinJar(final URL jarURL, final ClassLoader classLoader, final ClassContainer classContainer) throws IOException {
		// Get all classes within the Jar. Filter the classes which are not readers. Filter the abstract classes.
		return Collections2.filter(Collections2.filter(this.getAllClassesWithinJar(jarURL, classLoader), new IsReaderPredicate(classContainer)),
				new IsNotAbstractPredicate());
	}

	/**
	 * This method delivers all non abstract classes which are available in the given jar, have the {@link kieker.analysis.plugin.annotation.Plugin} annotation, and
	 * are compatible with {@link kieker.analysis.plugin.filter.AbstractFilterPlugin}.
	 * 
	 * @param jarURL
	 *            The URL for the jar.
	 * @param classLoader
	 *            The class loader which should be used to load the classes.
	 * @param classContainer
	 *            The container for the necessary reflection classes.
	 * 
	 * @return A list containing all available plugin-classes.
	 * 
	 * @throws IOException
	 *             If something went wrong during the opening of the jar file.
	 */
	public Collection<Class<?>> getAllFiltersWithinJar(final URL jarURL, final ClassLoader classLoader, final ClassContainer classContainer) throws IOException {
		// Get all classes within the Jar. Filter the classes which are not filters. Filter the abstract classes.
		return Collections2.filter(Collections2.filter(this.getAllClassesWithinJar(jarURL, classLoader), new IsFilterPredicate(classContainer)),
				new IsNotAbstractPredicate());
	}

	public Collection<Class<?>> getAllVisualizationsWithinJar(final URL jarURL, final ClassLoader classLoader, final ClassContainer classContainer)
			throws IOException {
		return Collections2.filter(Collections2.filter(this.getAllClassesWithinJar(jarURL, classLoader), new IsWebVisualiaztionFilterPredicate(classContainer)),
				new IsNotAbstractPredicate());
	}

	/**
	 * This method delivers all classes which are available in the given jar.
	 * 
	 * @param jarURL
	 *            The URL for the jar.
	 * @param classLoader
	 *            The class loader which should be used to load the classes.
	 * 
	 * @return A list containing all available classes.
	 * 
	 * @throws IOException
	 *             If something went wrong while opening of the jar file.
	 */
	private Collection<Class<?>> getAllClassesWithinJar(final URL jarURL, final ClassLoader classLoader) throws IOException {
		JarInputStream stream = null;
		try {
			final List<Class<?>> result = new ArrayList<Class<?>>();

			// Open the jar as a stream and run through all entries within this file
			stream = new JarInputStream(jarURL.openStream());

			JarEntry jarEntry;
			while ((jarEntry = stream.getNextJarEntry()) != null) {
				try {
					// Assemble the correct name
					final String className = jarEntry.toString().replace('/', '.').replace(".class", "");

					// Try to find a class with the same name and put it into our list
					result.add(classLoader.loadClass(className));
				} catch (final Throwable ex) { // NOPMD (Generic throwable and empty catch block) NOCS (IllegalCatchCheck)
					// Ignore error.
				}
			}

			return result;
		} finally {
			// Don't forget to close the jar file again.
			if (stream != null) {
				try {
					stream.close();
				} catch (final IOException ex1) {
					// Log but ignore this fail.
					LOG.warn("Could not close stream.", ex1);
				}
			}
		}
	}

	/**
	 * A predicate which can be used to check whether a class is a repository or not.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class IsRepositoryPredicate implements Predicate<Class<?>> {

		private final ClassContainer classContainer;

		/**
		 * Creates a new predicate using the given class container.
		 * 
		 * @param classContainer
		 *            The class container to compare the classes.
		 */
		public IsRepositoryPredicate(final ClassContainer classContainer) {
			this.classContainer = classContainer;
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && elem.isAnnotationPresent(this.classContainer.getRepositoryAnnotationClass())
					&& this.classContainer.getAbstractRepositoryClass().isAssignableFrom(elem);
		}

	}

	/**
	 * A predicate which can be used to check whether a class is a reader or not.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class IsReaderPredicate implements Predicate<Class<?>> {

		private final ClassContainer classContainer;

		/**
		 * Creates a new predicate using the given class container.
		 * 
		 * @param classContainer
		 *            The class container to compare the classes.
		 */
		public IsReaderPredicate(final ClassContainer classAndMethodContainer) {
			this.classContainer = classAndMethodContainer;
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && elem.isAnnotationPresent(this.classContainer.getPluginAnnotationClass())
					&& this.classContainer.getAbstractReaderPluginClass().isAssignableFrom(elem);
		}

	}

	/**
	 * A predicate which can be used to check whether a class is a filter or not.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class IsFilterPredicate implements Predicate<Class<?>> {

		private final ClassContainer classContainer;

		/**
		 * Creates a new predicate using the given class container.
		 * 
		 * @param classContainer
		 *            The class container to compare the classes.
		 */
		public IsFilterPredicate(final ClassContainer classAndMethodContainer) {
			this.classContainer = classAndMethodContainer;
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && elem.isAnnotationPresent(this.classContainer.getPluginAnnotationClass())
					&& this.classContainer.getAbstractFilterPluginClass().isAssignableFrom(elem)
					&& !this.classContainer.getAbstractWebVisualizationFilterPluginClass().isAssignableFrom(elem);
		}
	}

	/**
	 * @author Nils Christian Ehmke
	 */
	private static final class IsWebVisualiaztionFilterPredicate implements Predicate<Class<?>> {

		private final ClassContainer classContainer;

		/**
		 * Creates a new predicate using the given class container.
		 * 
		 * @param classContainer
		 *            The class container to compare the classes.
		 */
		public IsWebVisualiaztionFilterPredicate(final ClassContainer classAndMethodContainer) {
			this.classContainer = classAndMethodContainer;
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && elem.isAnnotationPresent(this.classContainer.getPluginAnnotationClass())
					&& this.classContainer.getAbstractWebVisualizationFilterPluginClass().isAssignableFrom(elem);
		}
	}

	/**
	 * A predicate which can be used to check whether a class is abstract or not.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class IsNotAbstractPredicate implements Predicate<Class<?>> {

		/**
		 * Creates a new predicate.
		 */
		public IsNotAbstractPredicate() {
			// No code necessary
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && !Modifier.isAbstract(elem.getModifiers());
		}

	}

}
