/***************************************************************************
 * 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;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.io.Files;

import kieker.analysis.AnalysisController;
import kieker.analysis.model.analysisMetaModel.MIAnalysisMetaModelFactory;
import kieker.analysis.model.analysisMetaModel.MIDisplay;
import kieker.analysis.model.analysisMetaModel.MIDisplayConnector;
import kieker.analysis.model.analysisMetaModel.MIFilter;
import kieker.analysis.model.analysisMetaModel.MIInputPort;
import kieker.analysis.model.analysisMetaModel.MIOutputPort;
import kieker.analysis.model.analysisMetaModel.MIPlugin;
import kieker.analysis.model.analysisMetaModel.MIProject;
import kieker.analysis.model.analysisMetaModel.MIRepository;
import kieker.analysis.model.analysisMetaModel.MIView;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.ClassContainer;
import kieker.webgui.common.exception.NewerProjectException;
import kieker.webgui.common.exception.ProjectAlreadyExistingException;
import kieker.webgui.common.exception.ProjectNotExistingException;
import kieker.webgui.common.exception.ReflectionException;
import kieker.webgui.domain.ComponentListContainer;
import kieker.webgui.domain.pluginDecorators.AbstractAnalysisComponentDecorator;
import kieker.webgui.domain.pluginDecorators.FilterDecorator;
import kieker.webgui.domain.pluginDecorators.ReaderDecorator;
import kieker.webgui.domain.pluginDecorators.RepositoryDecorator;
import kieker.webgui.domain.pluginDecorators.VisualizationDecorator;
import kieker.webgui.persistence.IProjectDAO;
import kieker.webgui.persistence.impl.utility.Class2ModelInstanceConverter;
import kieker.webgui.persistence.impl.utility.CloseableURLClassLoader;
import kieker.webgui.persistence.impl.utility.PluginFinder;

import org.primefaces.model.UploadedFile;

import net.vidageek.mirror.dsl.Mirror;
import net.vidageek.mirror.exception.MirrorException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.WeakReferenceMonitor;
import org.springframework.util.WeakReferenceMonitor.ReleaseListener;

/**
 * This is an implementation of the {@link IProjectDAO} interface, which uses the file system to store the available projects and everything.
 * 
 * @author Nils Christian Ehmke
 */
@Service
public final class FSProjectDAOImpl implements IProjectDAO, ReleaseListener {

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

	private static final MIProject EMPTY_PROJECT = MIAnalysisMetaModelFactory.eINSTANCE.createProject();

	private static final String KIEKER_LIB = "kieker.jar";
	private static final String META_FILE = "meta.dat";

	private static final String KAX_EXTENSION = "kax";
	private static final String LIB_EXTENSION = "jar";

	private static final String LIB_DIRECTORY = "lib";
	private static final String ROOT_DIRECTORY = "data";

	private static final String PROPERTY_KEY_LAST_USER = "last user";
	private static final String PROPERTY_KEY_OWNER = "owner";
	private static final String PROPERTY_KEY_ANALYSIS_LAYOUT = "analysis layout";
	private static final String PROPERTY_KEY_COCKPIT_LAYOUT = "cockpit layout";

	private final Map<CloseableURLClassLoader, WeakReference<Object>> classLoaders = new ConcurrentHashMap<CloseableURLClassLoader, WeakReference<Object>>();
	private final Map<File, WeakReference<Object>> tempDirs = new ConcurrentHashMap<File, WeakReference<Object>>();
	private final Map<String, ComponentListContainer> availableComponents = new ConcurrentHashMap<String, ComponentListContainer>();

	@Autowired
	private Class2ModelInstanceConverter class2ModelInstanceConverter;
	@Autowired
	private PluginFinder pluginFinder;

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

	/**
	 * This method initializes the object. <b>Do not call this method manually. It will only be accessed by Spring.</b>
	 * 
	 * @throws IOException
	 *             If either the creation of the necessary directories failed or if something went wrong during the initialization of the projects.
	 */
	@PostConstruct
	public void initialize() throws IOException {
		// Make sure that the necessary directories exist and that the available component lists (containing plugins and repositories) are initialized.
		FSProjectDAOImpl.createDirectoryIfNecessary(ROOT_DIRECTORY);
		this.initializeAvailableComponentsListContainersForAllProjects();
	}

	@Override
	public File getProjectFile(final String projectName) {
		return FSProjectDAOImpl.assembleKaxFile(projectName);
	}

	@Override
	public String getAnalysisLayout(final String projectName) {
		return FSProjectDAOImpl.getProperty(projectName, PROPERTY_KEY_ANALYSIS_LAYOUT, null);
	}

	@Override
	public String getCockpitLayout(final String projectName) {
		return FSProjectDAOImpl.getProperty(projectName, PROPERTY_KEY_COCKPIT_LAYOUT, null);
	}

	@Override
	public String getOwner(final String projectName) {
		return FSProjectDAOImpl.getProperty(projectName, PROPERTY_KEY_OWNER, "N/A");
	}

	@Override
	public String getLastUser(final String projectName) {
		return FSProjectDAOImpl.getProperty(projectName, PROPERTY_KEY_LAST_USER, "N/A");
	}

	@Override
	public void removeProject(final String projectName) throws IOException, ProjectNotExistingException {
		this.assertProjectExistence(projectName);

		// Simply try to remove the project directory
		final File projectDir = FSProjectDAOImpl.assembleProjectDir(projectName);
		final boolean result = FileSystemUtils.deleteRecursively(projectDir);

		if (!result) {
			throw new IOException("Unable to remove project '" + projectName + "'.)");
		}
	}

	@Override
	public void addProject(final String projectName, final String username) throws ProjectAlreadyExistingException, IOException {
		this.assertProjectAbsence(projectName);

		// Assemble all necessary paths and files for the given project
		final File projectDir = FSProjectDAOImpl.assembleProjectDir(projectName);
		final File projectFile = FSProjectDAOImpl.assembleKaxFile(projectName);
		final File metaFile = FSProjectDAOImpl.assembleMetaFile(projectName);
		final File libDir = this.assembleLibDir(projectName);

		try {
			// Try to create the directories and files
			if (projectDir.mkdir() && libDir.mkdir() && metaFile.createNewFile()) {
				// Try to save the file
				AnalysisController.saveToFile(projectFile, EMPTY_PROJECT);
				this.initializeAvailableComponentsListContainers(projectName);

				// Store the initial meta data
				final Properties properties = new Properties();
				properties.put(PROPERTY_KEY_OWNER, username);
				properties.put(PROPERTY_KEY_LAST_USER, username);
				FSProjectDAOImpl.savePropertiesFile(properties, projectName);
			} else {
				throw new IOException("Project-Directories could not be created.");
			}
		} catch (final IOException ex) {
			// Something went wrong. Remove the directories and files!
			libDir.delete();
			// Keep in mind that the potential remains of the file have to be deleted before the directory.
			projectFile.delete();
			projectDir.delete();

			// Rethrow the exception in order to inform the caller of this method
			throw new IOException("An IO Exception occured while saving the project.", ex);
		}
	}

	@Override
	public void importProject(final String projectName, final String username, final UploadedFile file) throws ProjectAlreadyExistingException, IOException {
		// Add an empty project.
		this.addProject(projectName, username);

		// Now upload the kax file
		final File tempFile = File.createTempFile("java", ".tmp");

		final BufferedInputStream in = new BufferedInputStream(file.getInputstream());
		final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile));

		// Copy the data - the streams will be closed in this method.
		FileCopyUtils.copy(in, out);

		// Now load the kax file and use the resulting MIProject to overwrite the existing (newly created) kax file
		final MIProject project = AnalysisController.loadFromFile(tempFile);
		try {
			this.saveProject(projectName, project, 0, true, username, null, null);
		} catch (final ProjectNotExistingException ex) {
			// This should not happen!
			LOG.error("Could not save project.", ex);
		} catch (final NewerProjectException ex) {
			// This should not happen! The projects should be locked at this point.
			LOG.error("Could not save project.", ex);
		}
	}

	@Override
	public void copyProject(final String originalProjectName, final String newProjectName) throws ProjectNotExistingException, ProjectAlreadyExistingException,
			IOException {
		this.assertProjectAbsence(newProjectName);
		this.assertProjectExistence(originalProjectName);

		final File dstProjDir = FSProjectDAOImpl.assembleProjectDir(newProjectName);
		try {
			final File srcProjDir = FSProjectDAOImpl.assembleProjectDir(originalProjectName);

			// Copy all files and rename the kax file
			FileSystemUtils.copyRecursively(srcProjDir, dstProjDir);

			final File dstKaxFile = new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + newProjectName + File.separator + originalProjectName + "."
					+ FSProjectDAOImpl.KAX_EXTENSION);

			final File realDstKaxFile = FSProjectDAOImpl.assembleKaxFile(newProjectName);

			if (!dstKaxFile.renameTo(realDstKaxFile)) {
				throw new IOException("Could not rename KAX file.");
			}

			this.initializeAvailableComponentsListContainers(newProjectName);
		} catch (final IOException ex) {
			// Something went wrong. Remove everything
			FileSystemUtils.deleteRecursively(dstProjDir);

			// Rethrow the exception in order to inform the caller of this method
			throw new IOException("An IO Exception occured while copying the project.", ex);
		}
	}

	@Override
	public MIProject loadProject(final String projectName) throws ProjectNotExistingException, IOException {
		if (projectName == null) {
			throw new ProjectNotExistingException("Project is null");
		}

		try {
			// Load the project
			return AnalysisController.loadFromFile(FSProjectDAOImpl.assembleKaxFile(projectName).getAbsoluteFile());
		} catch (final FileNotFoundException ex) {
			throw new ProjectNotExistingException("A project with the name '" + projectName + "' does not exist.", ex);
		}
	}

	@Override
	public Object loadProject(final String projectName, final ClassContainer classAndMethodContainer) throws ProjectNotExistingException, IOException {
		if (projectName == null) {
			throw new ProjectNotExistingException("Project is null");
		}

		try {
			// Load the project
			final Object project = new Mirror().on(classAndMethodContainer.getAnalysisControllerClass()).invoke().method("loadFromFile")
					.withArgs(FSProjectDAOImpl.assembleKaxFile(projectName));
			return project;
		} catch (final MirrorException ex) {
			throw new IOException("Project could not be loaded.", ex);
		}
	}

	@Override
	public void saveProject(final String projectName, final MIProject project, final long timeStamp, final boolean overwriteNewerProject, // NOPMD (String params.)
			final String username, final String analysisLayout, final String cockpitLayout) throws ProjectNotExistingException, IOException, NewerProjectException {
		this.assertProjectExistence(projectName);

		// Check for a newer version first
		final long currTimeStamp = this.getCurrTimeStamp(projectName);
		if (!overwriteNewerProject && (currTimeStamp > timeStamp)) {
			throw new NewerProjectException("The project with the name '" + projectName + "' has a newer version on the FS.");
		}

		// Try to save it.
		this.addMissingIDs(project);
		AnalysisController.saveToFile(FSProjectDAOImpl.assembleKaxFile(projectName), project);

		// Store the new meta data
		final Properties properties = FSProjectDAOImpl.loadPropertiesFile(projectName);
		properties.put(PROPERTY_KEY_LAST_USER, username);
		if (analysisLayout != null) {
			properties.put(PROPERTY_KEY_ANALYSIS_LAYOUT, analysisLayout);
		}
		if (cockpitLayout != null) {
			properties.put(PROPERTY_KEY_COCKPIT_LAYOUT, cockpitLayout);
		}
		FSProjectDAOImpl.savePropertiesFile(properties, projectName);
	}

	private void addMissingIDs(final MIProject project) {
		this.addMissingIDs(project, this.collectUsedIDs(project));
	}

	private Set<String> collectUsedIDs(final MIProject project) {
		final Set<String> usedIDs = new TreeSet<String>();

		for (final MIPlugin plugin : project.getPlugins()) {
			if (plugin.getId() != null) {
				usedIDs.add(plugin.getId());
			}
			for (final MIOutputPort outputPort : plugin.getOutputPorts()) {
				if (outputPort.getId() != null) {
					usedIDs.add(outputPort.getId());
				}
			}

			if (plugin instanceof MIFilter) {
				for (final MIInputPort inputPort : ((MIFilter) plugin).getInputPorts()) {
					if (inputPort.getId() != null) {
						usedIDs.add(inputPort.getId());
					}
				}
			}

			for (final MIDisplay display : plugin.getDisplays()) {
				if (display.getId() != null) {
					usedIDs.add(display.getId());
				}
			}
		}
		for (final MIRepository repository : project.getRepositories()) {
			if (repository.getId() != null) {
				usedIDs.add(repository.getId());
			}
		}
		for (final MIView view : project.getViews()) {
			if (view.getId() != null) {
				usedIDs.add(view.getId());
			}

			for (final MIDisplayConnector displayConnector : view.getDisplayConnectors()) {
				if (displayConnector.getId() != null) {
					usedIDs.add(displayConnector.getId());
				}
			}
		}

		return usedIDs;
	}

	private void addMissingIDs(final MIProject project, final Set<String> usedIDs) {
		long idCounter = 0;

		for (final MIPlugin plugin : project.getPlugins()) {
			if (plugin.getId() == null) {
				while (usedIDs.contains(Long.toString(idCounter))) {
					idCounter++;
				}
				plugin.setId(Long.toString(idCounter++));
			}
			for (final MIOutputPort outputPort : plugin.getOutputPorts()) {
				if (outputPort.getId() == null) {
					while (usedIDs.contains(Long.toString(idCounter))) {
						idCounter++;
					}
					outputPort.setId(Long.toString(idCounter++));
				}
			}
			if (plugin instanceof MIFilter) {
				for (final MIInputPort inputPort : ((MIFilter) plugin).getInputPorts()) {
					if (inputPort.getId() == null) {
						while (usedIDs.contains(Long.toString(idCounter))) {
							idCounter++;
						}
						inputPort.setId(Long.toString(idCounter++));
					}
				}
			}
			for (final MIDisplay display : plugin.getDisplays()) {
				if (display.getId() == null) {
					while (usedIDs.contains(Long.toString(idCounter))) {
						idCounter++;
					}
					display.setId(Long.toString(idCounter++));
				}
			}
		}
		for (final MIRepository repository : project.getRepositories()) {
			if (repository.getId() == null) {
				while (usedIDs.contains(Long.toString(idCounter))) {
					idCounter++;
				}
				repository.setId(Long.toString(idCounter++));
			}
		}
		for (final MIView view : project.getViews()) {
			if (view.getId() == null) {
				while (usedIDs.contains(Long.toString(idCounter))) {
					idCounter++;
				}
				view.setId(Long.toString(idCounter++));
			}

			for (final MIDisplayConnector displayConnector : view.getDisplayConnectors()) {
				if (displayConnector.getId() == null) {
					while (usedIDs.contains(Long.toString(idCounter))) {
						idCounter++;
					}
					displayConnector.setId(Long.toString(idCounter++));
				}
			}
		}
	}

	@Override
	public long getCurrTimeStamp(final String projectName) throws ProjectNotExistingException {
		this.assertProjectExistence(projectName);
		return FSProjectDAOImpl.assembleKaxFile(projectName).lastModified();
	}

	@Override
	public void uploadLibrary(final UploadedFile file, final String projectName) throws ProjectNotExistingException, IOException {
		this.assertProjectExistence(projectName);

		// Prepare the files
		final File libDir = this.assembleLibDir(projectName);
		final File dstFile = new File(libDir, file.getFileName());

		// Get the streams
		final BufferedInputStream in = new BufferedInputStream(file.getInputstream());
		final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dstFile));

		// Copy the data - the streams will be closed in this method.
		FileCopyUtils.copy(in, out);

		// Reload the available components
		this.initializeAvailableComponentsListContainers(projectName);
	}

	@Override
	public ClassLoader getClassLoader(final String projectName, final Object requester) throws ProjectNotExistingException, IOException {
		this.assertProjectExistence(projectName);

		// Create a new temporary directory
		final File tempDir = this.createTemporaryDirectory();
		final List<URL> libs = new ArrayList<URL>();

		// Copy and collect all libraries of the project
		final File libDir = new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + projectName + File.separator + FSProjectDAOImpl.LIB_DIRECTORY);
		final File[] files = libDir.listFiles();
		if (files != null) {
			for (final File file : files) {
				if (file.getName().endsWith("." + FSProjectDAOImpl.LIB_EXTENSION)) {
					final File newLibFile = new File(tempDir, file.getName());
					FileCopyUtils.copy(file, newLibFile);
					try {
						libs.add(newLibFile.toURI().toURL());
					} catch (final MalformedURLException ex) {
						ex.printStackTrace();
					}
				}
			}
		}

		// Add the kieker lib!
		libs.add(this.getKiekerURL());

		// Now assemble the URL class loader
		final PrivilegedClassLoaderAction action = new PrivilegedClassLoaderAction(libs);
		final CloseableURLClassLoader classLoader = AccessController.doPrivileged(action);

		// Remember the requester
		final WeakReference<Object> ref = new WeakReference<Object>(requester);
		this.classLoaders.put(classLoader, ref);
		this.tempDirs.put(tempDir, ref);
		WeakReferenceMonitor.monitor(requester, this);

		return classLoader;
	}

	@Override
	public List<String> listAllLibraries(final String projectName) throws ProjectNotExistingException {
		this.assertProjectExistence(projectName);

		final List<String> result = new ArrayList<String>();

		// Run through the libs and put them into our list.
		final File[] files = this.assembleLibDir(projectName).listFiles();
		if (files != null) {
			for (final File file : files) {
				if (file.getName().endsWith("." + FSProjectDAOImpl.LIB_EXTENSION)) {
					result.add(file.getName());
				}
			}
		}

		return result;
	}

	@Override
	public Collection<String> listAllProjects() {
		final List<String> result = new ArrayList<String>();

		// Get all directories within our root-dir
		final File[] files = new File(FSProjectDAOImpl.ROOT_DIRECTORY).listFiles();
		for (final File file : files) {
			if (file.isDirectory()) {
				result.add(file.getName());
			}
		}

		return result;
	}

	@Override
	public ComponentListContainer getAvailableComponents(final String project) {
		return this.availableComponents.get(project);
	}

	@Override
	public boolean deleteLibrary(final String projectName, final String libName) throws IOException, ProjectNotExistingException {
		this.assertProjectExistence(projectName);

		final File libDir = this.assembleLibDir(projectName);
		final File libFile = new File(libDir, libName);
		final boolean result = libFile.delete();
		// Reload the available components
		this.initializeAvailableComponentsListContainers(projectName);

		return result;
	}

	@Override
	public void released() {
		synchronized (this.classLoaders) {
			int open = 0;

			final List<CloseableURLClassLoader> toBeClosed = new ArrayList<CloseableURLClassLoader>();
			final List<File> toBeRemoved = new ArrayList<File>();

			// Run through the class loaders and check which of them can be closed
			for (final Entry<CloseableURLClassLoader, WeakReference<Object>> entry : this.classLoaders.entrySet()) {
				if (entry.getValue().get() == null) {
					toBeClosed.add(entry.getKey());
				} else {
					open++;
				}
			}
			for (final Entry<File, WeakReference<Object>> entry : this.tempDirs.entrySet()) {
				if (entry.getValue().get() == null) {
					toBeRemoved.add(entry.getKey());
				}
			}

			for (final CloseableURLClassLoader classLoader : toBeClosed) {
				try {
					classLoader.close();
					this.classLoaders.remove(classLoader);
					FSProjectDAOImpl.LOG.info("Closed classloader (" + classLoader + ")");
				} catch (final IOException ex) {
					FSProjectDAOImpl.LOG.error("Could not close classloader (" + classLoader + ")");
				}
			}
			for (final File tempDir : toBeRemoved) {
				final boolean result = FileSystemUtils.deleteRecursively(tempDir);
				if (result) {
					this.tempDirs.remove(tempDir);
					FSProjectDAOImpl.LOG.info("Removed temporary directory (" + tempDir + ")");
				} else {
					FSProjectDAOImpl.LOG.error("Could not remove temporary directory (" + tempDir + ")");
				}
			}
			FSProjectDAOImpl.LOG.info(open + " classloaders still open.");
		}
	}

	private void initializeAvailableComponentsListContainersForAllProjects() throws IOException {
		for (final String project : this.listAllProjects()) {
			this.initializeAvailableComponentsListContainers(project);
		}
	}

	private void initializeAvailableComponentsListContainers(final String project) throws IOException {
		try {
			final Object dummyObject = new Object();
			// Deliver a dummy object as a requester to make sure that the classloader can be disposed. Once the program exits this scope, it can be released.
			final CloseableURLClassLoader classLoader = (CloseableURLClassLoader) this.getClassLoader(project, dummyObject); // NOPMD (No ordinary classloader)
			final ClassContainer classAndMethodContainer = new ClassContainer(classLoader);

			final List<ReaderDecorator> readers = new ArrayList<ReaderDecorator>();
			final List<FilterDecorator> filters = new ArrayList<FilterDecorator>();
			final List<RepositoryDecorator> repositories = new ArrayList<RepositoryDecorator>();
			final List<VisualizationDecorator> visualizations = new ArrayList<VisualizationDecorator>();

			// Update the components using all available dependencies
			final Collection<String> libs = this.listAllLibraries(project);
			for (final String lib : libs) {
				this.initializeAvailableComponentsLists(readers, filters, repositories, visualizations, this.getURL(lib, project), classLoader,
						classAndMethodContainer);
			}
			// Update the components using the Kieker library.
			this.initializeAvailableComponentsLists(readers, filters, repositories, visualizations, this.getKiekerURL(), classLoader, classAndMethodContainer);

			final DecoratorComparator decoratorComparator = new DecoratorComparator();
			Collections.sort(repositories, decoratorComparator);
			Collections.sort(readers, decoratorComparator);
			Collections.sort(filters, decoratorComparator);
			Collections.sort(visualizations, decoratorComparator);

			// Assemble the final object containing the available components - but make sure that the lists cannot be modified.
			final ComponentListContainer componentList = new ComponentListContainer(Collections.unmodifiableList(readers), Collections.unmodifiableList(filters),
					Collections.unmodifiableList(repositories), Collections.unmodifiableList(visualizations));
			this.availableComponents.put(project, componentList);

			classLoader.close();
		} catch (final ReflectionException ex) {
			FSProjectDAOImpl.LOG.warn("An error occured while initializing the project '" + project + "'.", ex);
		} catch (final ProjectNotExistingException ex) {
			// Technically this should not happen. Log but ignore this exception.
			FSProjectDAOImpl.LOG.warn("An error occured while initializing the project '" + project + "'.", ex);
		}
	}

	private void initializeAvailableComponentsLists(final List<ReaderDecorator> readers, final List<FilterDecorator> filters,
			final List<RepositoryDecorator> repositories, final List<VisualizationDecorator> visualizations, final URL lib, final ClassLoader classLoader,
			final ClassContainer classAndMethodContainer)
			throws IOException {
		// FInd the available classes within the library
		final Collection<Class<?>> repositoryClasses = this.pluginFinder.getAllRepositoriesWithinJar(lib, classLoader, classAndMethodContainer);
		final Collection<Class<?>> filterClasses = this.pluginFinder.getAllFiltersWithinJar(lib, classLoader, classAndMethodContainer);
		final Collection<Class<?>> visualizationClasses = this.pluginFinder.getAllVisualizationsWithinJar(lib, classLoader, classAndMethodContainer);
		final Collection<Class<?>> readerClasses = this.pluginFinder.getAllReadersWithinJar(lib, classLoader, classAndMethodContainer);

		// Rule those out which are for programmatic purposes only and transform the remaining with the converter service into model instances.
		final Collection<RepositoryDecorator> repositoryModelInstances = Collections2.transform(
				Collections2.filter(repositoryClasses, new IsNotProgrammaticOnly(classAndMethodContainer)), new ConvertRepositoryClass2ModelInstanceFunction(
						classAndMethodContainer));
		final Collection<FilterDecorator> filterModelInstances = Collections2.transform(Collections2.filter(filterClasses, new IsNotProgrammaticOnly(
				classAndMethodContainer)), new ConvertFilterClass2ModelInstanceFunction(classAndMethodContainer));
		final Collection<ReaderDecorator> readerModelInstances = Collections2.transform(Collections2.filter(readerClasses, new IsNotProgrammaticOnly(
				classAndMethodContainer)), new ConvertReaderClass2ModelInstanceFunction(classAndMethodContainer));
		final Collection<VisualizationDecorator> visualizationModelInstances = Collections2.transform(Collections2.filter(visualizationClasses,
				new IsNotProgrammaticOnly(classAndMethodContainer)), new ConvertVisualizationClass2ModelInstanceFunction(classAndMethodContainer));

		// Add the model instances to our lists.
		repositories.addAll(repositoryModelInstances);
		readers.addAll(readerModelInstances);
		filters.addAll(filterModelInstances);
		visualizations.addAll(visualizationModelInstances);
	}

	private static void createDirectoryIfNecessary(final String dirName) throws IOException {
		final File dir = new File(dirName);
		if (!dir.exists() && !dir.mkdir()) {
			// Try to create the directory
			throw new IOException("Could not create directory '" + dirName + "'.");
		}
	}

	private static final Properties loadPropertiesFile(final String projectName) throws IOException {
		final File metaFile = FSProjectDAOImpl.assembleMetaFile(projectName);
		final Properties properties = new Properties();

		FileInputStream in = null;
		try {
			in = new FileInputStream(metaFile);
			properties.load(in);
		} finally {
			if (in != null) {
				in.close();
			}
		}

		return properties;
	}

	private static final void savePropertiesFile(final Properties properties, final String projectName) throws IOException {
		final File metaFile = FSProjectDAOImpl.assembleMetaFile(projectName);
		FileOutputStream out = null;
		try {
			out = new FileOutputStream(metaFile);
			properties.store(out, "");
		} finally {
			if (out != null) {
				out.close();
			}
		}
	}

	private File createTemporaryDirectory() {
		return Files.createTempDir();
	}

	private void assertProjectExistence(final String projectName) throws ProjectNotExistingException {
		if (!FSProjectDAOImpl.assembleKaxFile(projectName).exists()) {
			throw new ProjectNotExistingException("A project with the name '" + projectName + "' does not exist.");
		}
	}

	private void assertProjectAbsence(final String projectName) throws ProjectAlreadyExistingException {
		if (FSProjectDAOImpl.assembleKaxFile(projectName).exists()) {
			throw new ProjectAlreadyExistingException("A project with the name '" + projectName + "' does already exist.");
		}
	}

	private URL getURL(final String lib, final String project) throws MalformedURLException {
		final File file = new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + project + File.separator + FSProjectDAOImpl.LIB_DIRECTORY + File.separator
				+ lib);
		return file.toURI().toURL();
	}

	private static File assembleProjectDir(final String projectName) {
		return new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + projectName);
	}

	private static File assembleKaxFile(final String projectName) {
		return new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + projectName + File.separator + projectName + "." + FSProjectDAOImpl.KAX_EXTENSION);
	}

	private static File assembleMetaFile(final String projectName) {
		return new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + projectName + File.separator + FSProjectDAOImpl.META_FILE);
	}

	private File assembleLibDir(final String projectName) {
		return new File(FSProjectDAOImpl.ROOT_DIRECTORY + File.separator + projectName + File.separator + FSProjectDAOImpl.LIB_DIRECTORY);
	}

	private URL getKiekerURL() {
		return Thread.currentThread().getContextClassLoader().getResource(FSProjectDAOImpl.KIEKER_LIB);
	}

	private static String getProperty(final String projectName, final String propertyKey, final String defaultValue) {
		try {
			final Properties properties = FSProjectDAOImpl.loadPropertiesFile(projectName);
			final String value = properties.getProperty(propertyKey);

			if (value != null) {
				return value;
			}
		} catch (final IOException ex) {
			FSProjectDAOImpl.LOG.warn("Could not open meta file.", ex);
		}

		return defaultValue;
	}

	/**
	 * This helper class is responsible for creating a classloader as a privileged action. This is recommended due to the java security manager.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private static final class PrivilegedClassLoaderAction implements PrivilegedAction<CloseableURLClassLoader> {
		/**
		 * The list of libraries used to create the class loader.
		 */
		private final List<URL> libs;

		/**
		 * Creates a new instance of this class using the given parameters.
		 * 
		 * @param libs
		 *            The list of libraries used to create the class loader.
		 */
		public PrivilegedClassLoaderAction(final List<URL> libs) {
			this.libs = libs;
		}

		/**
		 * Runs the action.
		 * 
		 * @return The class loader.
		 */
		@Override
		public CloseableURLClassLoader run() {
			// We use "null" as an explicit parent to make sure that the class loader is completely independent from the web gui class loader.
			return new CloseableURLClassLoader(this.libs.toArray(new URL[this.libs.size()]));
		}
	}

	private static class DecoratorComparator implements Comparator<AbstractAnalysisComponentDecorator<?>>, Serializable {

		private static final long serialVersionUID = -9109481172096330879L;

		public DecoratorComparator() {
			// Empty default constructor
		}

		@Override
		public int compare(final AbstractAnalysisComponentDecorator<?> o1, final AbstractAnalysisComponentDecorator<?> o2) {
			return o1.getName().compareTo(o2.getName());
		}

	}

	/**
	 * A predicate which can be used to check whether a class is for programmatic purposes only or not.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private final class IsNotProgrammaticOnly implements Predicate<Class<?>> {

		private final ClassContainer classAndMethodContainer;

		public IsNotProgrammaticOnly(final ClassContainer classAndMethodContainer) {
			this.classAndMethodContainer = classAndMethodContainer;
		}

		@Override
		public boolean apply(@Nullable final Class<?> elem) {
			return (elem != null) && !FSProjectDAOImpl.this.class2ModelInstanceConverter.isProgrammaticOnly(elem, this.classAndMethodContainer);
		}

	}

	/**
	 * A simple function to convert repository classes to model instances.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private final class ConvertRepositoryClass2ModelInstanceFunction implements Function<Class<?>, RepositoryDecorator> {

		private final ClassContainer classAndMethodContainer;

		public ConvertRepositoryClass2ModelInstanceFunction(final ClassContainer classAndMethodContainer) {
			this.classAndMethodContainer = classAndMethodContainer;
		}

		@Override
		@Nullable
		public RepositoryDecorator apply(@Nullable final Class<?> repository) {
			if (repository == null) {
				return null;
			}
			return FSProjectDAOImpl.this.class2ModelInstanceConverter.convertRepositoryClass2ModelInstance(repository, this.classAndMethodContainer);
		}

	}

	/**
	 * A simple function to convert reader classes to model instances.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private final class ConvertReaderClass2ModelInstanceFunction implements Function<Class<?>, ReaderDecorator> {

		private final ClassContainer classAndMethodContainer;

		public ConvertReaderClass2ModelInstanceFunction(final ClassContainer classAndMethodContainer) {
			this.classAndMethodContainer = classAndMethodContainer;
		}

		@Override
		@Nullable
		public ReaderDecorator apply(@Nullable final Class<?> reader) {
			if (reader == null) {
				return null;
			}
			return FSProjectDAOImpl.this.class2ModelInstanceConverter.convertReaderClass2ModelInstance(reader, this.classAndMethodContainer);
		}

	}

	/**
	 * A simple function to convert filter classes to model instances.
	 * 
	 * @author Nils Christian Ehmke
	 */
	private final class ConvertFilterClass2ModelInstanceFunction implements Function<Class<?>, FilterDecorator> {

		private final ClassContainer classAndMethodContainer;

		public ConvertFilterClass2ModelInstanceFunction(final ClassContainer classAndMethodContainer) {
			this.classAndMethodContainer = classAndMethodContainer;
		}

		@Override
		@Nullable
		public FilterDecorator apply(@Nullable final Class<?> filter) {
			if (filter == null) {
				return null;
			}
			return FSProjectDAOImpl.this.class2ModelInstanceConverter.convertFilterClass2ModelInstance(filter, this.classAndMethodContainer);
		}

	}

	private final class ConvertVisualizationClass2ModelInstanceFunction implements Function<Class<?>, VisualizationDecorator> {

		private final ClassContainer classAndMethodContainer;

		public ConvertVisualizationClass2ModelInstanceFunction(final ClassContainer classAndMethodContainer) {
			this.classAndMethodContainer = classAndMethodContainer;
		}

		@Override
		@Nullable
		public VisualizationDecorator apply(@Nullable final Class<?> filter) {
			if (filter == null) {
				return null;
			}
			return FSProjectDAOImpl.this.class2ModelInstanceConverter.convertVisualizationClass2ModelInstance(filter, this.classAndMethodContainer);
		}

	}

}
