/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Matt Chapman, mpchapman@gmail.com - 89977 Make JDT .java agnostic
 *     Ferenc Hechler, ferenc_hechler@users.sourceforge.net - 83258 [jar exporter] Deploy java application as executable jar
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.jarpackager;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.filesystem.EFS;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.jface.operation.ModalContext;

import org.eclipse.ui.actions.WorkspaceModifyOperation;

import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.util.IClassFileReader;
import org.eclipse.jdt.core.util.ISourceAttribute;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.corext.util.Resources;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
import org.eclipse.jdt.ui.jarpackager.IJarBuilder;
import org.eclipse.jdt.ui.jarpackager.IJarBuilderExtension;
import org.eclipse.jdt.ui.jarpackager.IJarDescriptionWriter;
import org.eclipse.jdt.ui.jarpackager.IJarExportRunnable;
import org.eclipse.jdt.ui.jarpackager.JarPackageData;
import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper;

import org.eclipse.jdt.internal.ui.IJavaStatusConstants;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.util.BusyIndicatorRunnableContext;

/**
 * Operation for exporting a resource and its children to a new  JAR file.
 */
public class JarFileExportOperation extends WorkspaceModifyOperation implements IJarExportRunnable {

	private static class MessageMultiStatus extends MultiStatus {
		MessageMultiStatus(String pluginId, int code, String message, Throwable exception) {
			super(pluginId, code, message, exception);
		}
		/*
		 * allows to change the message
		 */
		@Override
		protected void setMessage(String message) {
			super.setMessage(message);
		}
	}

	private IJarBuilder fJarBuilder;
	private JarPackageData fJarPackage;
	private JarPackageData[] fJarPackages;
	private Shell fParentShell;
	private Map<String, ArrayList<IResource>> fJavaNameToClassFilesMap;
	private IContainer fClassFilesMapContainer;
	private Set<IContainer> fExportedClassContainers;
	private MessageMultiStatus fStatus;
	private StandardJavaElementContentProvider fJavaElementContentProvider;
	private boolean fFilesSaved;

	/**
	 * Creates an instance of this class.
	 *
	 * @param	jarPackage	the JAR package specification
	 * @param	parent	the parent for the dialog,
	 * 			or <code>null</code> if no dialog should be presented
	 */
	public JarFileExportOperation(JarPackageData jarPackage, Shell parent) {
		this(new JarPackageData[] {jarPackage}, parent);
	}

	/**
	 * Creates an instance of this class.
	 *
	 * @param	jarPackages		an array with JAR package data objects
	 * @param	parent			the parent for the dialog,
	 * 			or <code>null</code> if no dialog should be presented
	 */
	public JarFileExportOperation(JarPackageData[] jarPackages, Shell parent) {
		this(parent);
		fJarPackages= jarPackages;
	}

	private JarFileExportOperation(Shell parent) {
		fParentShell= parent;
		fStatus= new MessageMultiStatus(JavaPlugin.getPluginId(), IStatus.OK, "", null); //$NON-NLS-1$
		fJavaElementContentProvider= new StandardJavaElementContentProvider();
	}

	private void addToStatus(CoreException ex) {
		IStatus status= ex.getStatus();
		String message= ex.getLocalizedMessage();
		if (message == null || message.length() < 1) {
			message= JarPackagerMessages.JarFileExportOperation_coreErrorDuringExport;
			status= new Status(status.getSeverity(), status.getPlugin(), status.getCode(), message, ex);
		}
		fStatus.add(status);
	}

	/**
	 * Adds a new info to the list with the passed information.
	 * Normally the export operation continues after a warning.
	 * @param	message		the message
	 * @param	error 	the throwable that caused the warning, or <code>null</code>
	 */
	protected void addInfo(String message, Throwable error) {
		fStatus.add(new Status(IStatus.INFO, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, message, error));
	}

	/**
	 * Adds a new warning to the list with the passed information.
	 * Normally the export operation continues after a warning.
	 * @param	message		the message
	 * @param	error	the throwable that caused the warning, or <code>null</code>
	 */
	private void addWarning(String message, Throwable error) {
		fStatus.add(new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, message, error));
	}

	/**
	 * Adds a new error to the list with the passed information.
	 * Normally an error terminates the export operation.
	 * @param	message		the message
	 * @param	error 	the throwable that caused the error, or <code>null</code>
	 */
	private void addError(String message, Throwable error) {
		fStatus.add(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, message, error));
	}

	/**
	 * Answers the number of file resources specified by the JAR package.
	 *
	 * @return int
	 */
	private int countSelectedElements() {
		Set<IJavaProject> enclosingJavaProjects= new HashSet<>(10);
		int count= 0;

		int n= fJarPackage.getElements().length;
		for (int i= 0; i < n; i++) {
			Object element= fJarPackage.getElements()[i];

			IJavaProject javaProject= getEnclosingJavaProject(element);
			if (javaProject != null)
				enclosingJavaProjects.add(javaProject);

			IResource resource= null;
			if (element instanceof IJavaElement) {
				IJavaElement je= (IJavaElement)element;
				resource= je.getResource();
				if (resource == null) {
					if (element instanceof IPackageFragmentRoot) {
						IPackageFragmentRoot root= (IPackageFragmentRoot) element;
						if (root.isArchive()) {
							ZipFile file= null;
							try {
								file= JarPackagerUtil.getArchiveFile(root.getPath());
								if (file != null)
									count+= file.size();
							} catch (CoreException e) {
								JavaPlugin.log(e);
							} finally {
								try {
									if (file != null) {
										file.close();
									}
								} catch (IOException e) {
									addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_CloseZipFileError_message, new Object[] { JavaElementLabels.getElementLabel(root, JavaElementLabels.ALL_DEFAULT), e.getLocalizedMessage() }), e);
								}
							}
						} else if (root.isExternal()) {
							try {
								count+= getClassFileCount(root.getChildren());
							} catch (JavaModelException e) {
								JavaPlugin.log(e);
							}
						}
					}
					continue;
				}
			} else if (element instanceof IResource) {
				resource= (IResource) element;
			}
			if (resource != null) {
				if (resource.getType() == IResource.FILE)
					count++;
				else
					count+= getTotalChildCount((IContainer) resource);
			}
		}

		if (fJarPackage.areOutputFoldersExported()) {
			if (!fJarPackage.areJavaFilesExported())
				count= 0;
			Iterator<IJavaProject> iter= enclosingJavaProjects.iterator();
			while (iter.hasNext()) {
				IJavaProject javaProject= iter.next();
				IContainer[] outputContainers;
				try {
					outputContainers= getOutputContainers(javaProject);
				} catch (CoreException ex) {
					addToStatus(ex);
					continue;
				}
				for (IContainer outputContainer : outputContainers) {
					count += getTotalChildCount(outputContainer);
				}

			}
		}

		return count;
	}

	private int getClassFileCount(IJavaElement[] children) throws JavaModelException {
		int result= 0;
		for (IJavaElement child : children) {
			if (child instanceof IClassFile) {
				result++;
			} else if (child instanceof IPackageFragment) {
				IPackageFragment fragment= (IPackageFragment) child;
				result+= getClassFileCount(fragment.getChildren());
			}
		}
		return result;
	}

	private int getTotalChildCount(IContainer container) {
		IResource[] members;
		try {
			members= container.members();
		} catch (CoreException ex) {
			return 0;
		}
		int count= 0;
		for (IResource member : members) {
			if (member.getType() == IResource.FILE) {
				count++;
			} else {
				count += getTotalChildCount((IContainer) member);
			}
		}
		return count;
	}

	/**
	 * Exports the passed resource to the JAR file
	 *
	 * @param element the resource or JavaElement to export
	 * @param progressMonitor the progress monitor
	 * @throws InterruptedException thrown on cancel
	 */
	private void exportElement(Object element, IProgressMonitor progressMonitor) throws InterruptedException {
		int leadSegmentsToRemove= 1;
		IPackageFragmentRoot pkgRoot= null;
		boolean isInJavaProject= false;
		IResource resource= null;
		ITypeRoot typeRootElement= null;
		IJavaProject jProject= null;
		if (element instanceof IJavaElement) {
			isInJavaProject= true;
			IJavaElement je= (IJavaElement)element;
			if (!(je instanceof ITypeRoot)) {
				exportJavaElement(progressMonitor, je);
				return;
			}
			typeRootElement= (ITypeRoot) je;
			jProject= typeRootElement.getJavaProject();
			pkgRoot= JavaModelUtil.getPackageFragmentRoot(je);
			resource= typeRootElement.getResource();
		} else if (element instanceof IResource) {
			resource= (IResource) element;
		} else {
			return;
		}

		if (!resource.isAccessible()) {
			addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_resourceNotFound, BasicElementLabels.getPathLabel(resource.getFullPath(), false)), null);
			return;
		}

		if (resource.getType() == IResource.FILE) {
			if (!isInJavaProject) {
				// check if it's a Java resource
				try {
					isInJavaProject= resource.getProject().hasNature(JavaCore.NATURE_ID);
				} catch (CoreException ex) {
					addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_projectNatureNotDeterminable, BasicElementLabels.getPathLabel(resource.getFullPath(), false)), ex);
					return;
				}
				if (isInJavaProject) {
					IJavaElement je= JavaCore.create(resource);
					if (je instanceof ITypeRoot && je.exists()) {
						exportElement(je, progressMonitor);
						return;
					}

					jProject= JavaCore.create(resource.getProject());
					try {
						IPackageFragment pkgFragment= jProject.findPackageFragment(resource.getFullPath().removeLastSegments(1));
						if (pkgFragment != null)
							pkgRoot= JavaModelUtil.getPackageFragmentRoot(pkgFragment);
						else
							pkgRoot= findPackageFragmentRoot(jProject, resource.getFullPath().removeLastSegments(1));
					} catch (JavaModelException ex) {
						addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_javaPackageNotDeterminable, BasicElementLabels.getPathLabel(resource.getFullPath(), false)), ex);
						return;
					}
				}
			}

			if (pkgRoot != null && jProject != null) {
				leadSegmentsToRemove= pkgRoot.getPath().segmentCount();
				boolean isOnBuildPath;
				isOnBuildPath= jProject.isOnClasspath(resource);
				if (!isOnBuildPath || (mustUseSourceFolderHierarchy() && !IPackageFragmentRoot.DEFAULT_PACKAGEROOT_PATH.equals(pkgRoot.getElementName())))
					leadSegmentsToRemove--;
			}

			IPath destinationPath= resource.getFullPath().removeFirstSegments(leadSegmentsToRemove);

			if (typeRootElement != null) {
				exportClassFiles(progressMonitor, typeRootElement, destinationPath);
			}

			exportResource(progressMonitor, pkgRoot, isInJavaProject, resource, destinationPath);

			progressMonitor.worked(1);
			ModalContext.checkCanceled(progressMonitor);

		} else
			exportContainer(progressMonitor, (IContainer)resource);
	}

	private void exportJavaElement(IProgressMonitor progressMonitor, IJavaElement je) throws InterruptedException {
		if (je.getElementType() == IJavaElement.PACKAGE_FRAGMENT_ROOT && ((IPackageFragmentRoot) je).isArchive()) {
			IPackageFragmentRoot root= (IPackageFragmentRoot) je;
			ZipFile jarFile= null;
			try {
				jarFile= JarPackagerUtil.getArchiveFile(root.getPath());
				fJarBuilder.writeArchive(jarFile, progressMonitor);
			} catch (CoreException e) {
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_OpenZipFileError_message, new Object[] { JavaElementLabels.getElementLabel(root, JavaElementLabels.ALL_DEFAULT), e.getLocalizedMessage() }), e);
			} finally {
				try {
					if (jarFile != null) {
						jarFile.close();
					}
				} catch (IOException e) {
					addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_CloseZipFileError_message, new Object[] { JavaElementLabels.getElementLabel(root, JavaElementLabels.ALL_DEFAULT), e.getLocalizedMessage() }), e);
				}
			}
			return;
		} else if (je.getElementType() == IJavaElement.PACKAGE_FRAGMENT_ROOT && ((IPackageFragmentRoot) je).isExternal()) {
			//External class folder
			if (fJarBuilder instanceof IJarBuilderExtension) {
				exportExternalClassFolder(((IPackageFragmentRoot) je), progressMonitor);
			} else {
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_canNotExportExternalClassFolder_warning, BasicElementLabels.getPathLabel(je.getPath(), true)), null);
			}
			return;
		}

		for (Object child : fJavaElementContentProvider.getChildren(je)) {
			exportElement(child, progressMonitor);
		}
	}

	private void exportExternalClassFolder(IPackageFragmentRoot classFolder, IProgressMonitor progressMonitor) throws InterruptedException {
		try {
			for (IJavaElement child : classFolder.getChildren()) {
				exportExternalClassFolderElement(child, classFolder.getPath(), progressMonitor);
			}
		} catch (JavaModelException e) {
			addToStatus(e);
		}
	}

	private void exportExternalClassFolderElement(IJavaElement javaElement, IPath classFolderPath, IProgressMonitor progressMonitor) throws JavaModelException, InterruptedException {
		if (javaElement instanceof IClassFile) {
			IClassFile classFile= (IClassFile) javaElement;
			IPath path= classFile.getPath();

			IPath destination= path.removeFirstSegments(classFolderPath.segmentCount()).setDevice(null);

			try {
				((IJarBuilderExtension) fJarBuilder).writeFile(path.toFile(), destination);
			} catch (CoreException e) {
				handleCoreExceptionOnExport(e);
			} finally {
				progressMonitor.worked(1);
				ModalContext.checkCanceled(progressMonitor);
			}
		} else if (javaElement instanceof IPackageFragment) {
			for (IJavaElement child : ((IPackageFragment) javaElement).getChildren()) {
				exportExternalClassFolderElement(child, classFolderPath, progressMonitor);
			}
		}
	}

	private void exportResource(IProgressMonitor progressMonitor, IResource resource, int leadingSegmentsToRemove) throws InterruptedException {
		if (resource instanceof IContainer) {
			IContainer container= (IContainer)resource;
			IResource[] children;
			try {
				children= container.members();
			} catch (CoreException e) {
				// this should never happen because an #isAccessible check is done before #members is invoked
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_errorDuringExport, BasicElementLabels.getPathLabel(container.getFullPath(), false)), e);
				return;
			}
			for (IResource child : children) {
				exportResource(progressMonitor, child, leadingSegmentsToRemove);
			}
		} else if (resource instanceof IFile) {
			try {
				IPath destinationPath= resource.getFullPath().removeFirstSegments(leadingSegmentsToRemove);
				progressMonitor.subTask(Messages.format(JarPackagerMessages.JarFileExportOperation_exporting, BasicElementLabels.getPathLabel(destinationPath, false)));
				fJarBuilder.writeFile((IFile)resource, destinationPath);
			} catch (CoreException ex) {
				handleCoreExceptionOnExport(ex);
			} finally {
				progressMonitor.worked(1);
				ModalContext.checkCanceled(progressMonitor);
			}
		}
	}

	private void exportContainer(IProgressMonitor progressMonitor, IContainer container) throws InterruptedException {
		if (container.getType() == IResource.FOLDER && isOutputFolder((IFolder)container))
			return;



		IResource[] children= null;
		try {
			children= container.members();
		} catch (CoreException exception) {
			// this should never happen because an #isAccessible check is done before #members is invoked
			addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_errorDuringExport, BasicElementLabels.getPathLabel(container.getFullPath(), false)), exception);
		}
		if (children != null) {
			IJavaProject javaProject= JavaCore.create(container.getProject());
			boolean isOnCP= javaProject.isOnClasspath(container);
			for (IResource child : children) {
				if (isOnCP || !javaProject.isOnClasspath(child) || isInternalJar(child))
					exportElement(child, progressMonitor);
			}
		}
	}

	/**
	 * Tells whether the given resource is an internal JAR.
	 *
	 * @param resource the resource to test
	 * @return <code>true</code> if it is an internal JAR, <code>false</code> otherwise
	 * @since 3.6
	 */
	private boolean isInternalJar(IResource resource) {
		if (resource.getType() != IResource.FILE)
			return false;

		IJavaElement je= JavaCore.create(resource);
		if (je == null || je.getElementType() != IJavaElement.PACKAGE_FRAGMENT_ROOT)
			return false;

		IPackageFragmentRoot root= (IPackageFragmentRoot)je;
		return root.isArchive() && !root.isExternal();
	}

	private IPackageFragmentRoot findPackageFragmentRoot(IJavaProject jProject, IPath path) throws JavaModelException {
		if (jProject == null || path == null || path.segmentCount() <= 0)
			return null;
		IPackageFragmentRoot pkgRoot= jProject.findPackageFragmentRoot(path);
		if (pkgRoot != null)
			return pkgRoot;
		else
			return findPackageFragmentRoot(jProject, path.removeLastSegments(1));
	}

	private void exportResource(IProgressMonitor progressMonitor, IPackageFragmentRoot pkgRoot, boolean isInJavaProject, IResource resource, IPath destinationPath) {

		// Handle case where META-INF/MANIFEST.MF is part of the exported files
		if (fJarPackage.areClassFilesExported() && "META-INF/MANIFEST.MF".equals(destinationPath.toString())) {//$NON-NLS-1$
			if (fJarPackage.isManifestGenerated())
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_didNotAddManifestToJar, BasicElementLabels.getPathLabel(resource.getFullPath(), false)), null);
			return;
		}

		boolean isNonJavaResource= !isInJavaProject || pkgRoot == null;
		boolean isInClassFolder= false;
		try {
			isInClassFolder= pkgRoot != null && !pkgRoot.isArchive() && pkgRoot.getKind() == IPackageFragmentRoot.K_BINARY;
		} catch (JavaModelException ex) {
			addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_cantGetRootKind, BasicElementLabels.getPathLabel(resource.getFullPath(), false)), ex);
		}
		if ((fJarPackage.areClassFilesExported() &&
					((isNonJavaResource || (pkgRoot != null && !isJavaFile(resource) && !isClassFile(resource)))
					|| isInClassFolder && isClassFile(resource)))
			|| (fJarPackage.areJavaFilesExported() && (isNonJavaResource || (pkgRoot != null && !isClassFile(resource)) || (isInClassFolder && isClassFile(resource) && !fJarPackage.areClassFilesExported())))) {
			try {
				progressMonitor.subTask(Messages.format(JarPackagerMessages.JarFileExportOperation_exporting, BasicElementLabels.getPathLabel(destinationPath, false)));
				fJarBuilder.writeFile((IFile)resource, destinationPath);
			} catch (CoreException ex) {
				handleCoreExceptionOnExport(ex);
			}
		}
	}

	private boolean isOutputFolder(IFolder folder) {
		try {
			IJavaProject javaProject= JavaCore.create(folder.getProject());
			IPath outputFolderPath= javaProject.getOutputLocation();
			return folder.getFullPath().equals(outputFolderPath);
		} catch (JavaModelException ex) {
			return false;
		}
	}

	private void exportClassFiles(IProgressMonitor progressMonitor, ITypeRoot typeRootElement, IPath destinationPath) {
		if (fJarPackage.areClassFilesExported()) {
			try {
				if (!typeRootElement.exists())
					return;

				// find corresponding file(s) on classpath and export
				Iterator<? extends IResource> iter= filesOnClasspath(typeRootElement, destinationPath, progressMonitor);
				IPath baseDestinationPath= destinationPath.removeLastSegments(1);
				while (iter.hasNext()) {
					IFile file= (IFile)iter.next();
					IPath classFilePath= baseDestinationPath.append(file.getName());
					progressMonitor.subTask(Messages.format(JarPackagerMessages.JarFileExportOperation_exporting, BasicElementLabels.getPathLabel(classFilePath, false)));
					try {
						fJarBuilder.writeFile(file, classFilePath);
					} catch (CoreException ex) {
						handleCoreExceptionOnExport(ex);
					}
				}
			} catch (CoreException ex) {
				addToStatus(ex);
			}
		}
	}

	/**
	 * Exports the resources as specified by the JAR package.
	 * @param progressMonitor the progress monitor
	 * @throws InterruptedException thrown when cancelled
	 */
	private void exportSelectedElements(IProgressMonitor progressMonitor) throws InterruptedException {
		fExportedClassContainers= new HashSet<>(10);
		Set<IJavaProject> enclosingJavaProjects= new HashSet<>(10);
		int n= fJarPackage.getElements().length;
		for (int i= 0; i < n; i++) {
			Object element= fJarPackage.getElements()[i];
			exportElement(element, progressMonitor);
			if (fJarPackage.areOutputFoldersExported()) {
				IJavaProject javaProject= getEnclosingJavaProject(element);
				if (javaProject != null)
					enclosingJavaProjects.add(javaProject);
			}
		}
		if (fJarPackage.areOutputFoldersExported())
			exportOutputFolders(progressMonitor, enclosingJavaProjects);
	}

	private IJavaProject getEnclosingJavaProject(Object element) {
		if (element instanceof IJavaElement) {
			return ((IJavaElement)element).getJavaProject();
		} else if (element instanceof IResource) {
			IProject project= ((IResource)element).getProject();
			try {
				if (project.hasNature(JavaCore.NATURE_ID))
					return JavaCore.create(project);
			} catch (CoreException ex) {
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_projectNatureNotDeterminable, BasicElementLabels.getPathLabel(project.getFullPath(), false)), ex);
			}
		}
		return null;
	}

	private void exportOutputFolders(IProgressMonitor progressMonitor, Set<IJavaProject> javaProjects) throws InterruptedException {
		if (javaProjects == null)
			return;

		Iterator<IJavaProject> iter= javaProjects.iterator();
		while (iter.hasNext()) {
			IJavaProject javaProject= iter.next();
			IContainer[] outputContainers;
			try {
				outputContainers= getOutputContainers(javaProject);
			} catch (CoreException ex) {
				addToStatus(ex);
				continue;
			}
			for (IContainer outputContainer : outputContainers) {
				exportResource(progressMonitor, outputContainer, outputContainer.getFullPath().segmentCount());
			}

		}
	}

	private IContainer[] getOutputContainers(IJavaProject javaProject) throws CoreException {
		Set<IPath> outputPaths= new HashSet<>();
		boolean includeDefaultOutputPath= false;
		for (IPackageFragmentRoot root : javaProject.getPackageFragmentRoots()) {
			if (root != null) {
				IClasspathEntry cpEntry = root.getRawClasspathEntry();
				if (cpEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
					IPath location= cpEntry.getOutputLocation();
					if (location != null)
						outputPaths.add(location);
					else
						includeDefaultOutputPath= true;
				}
			}
		}

		if (includeDefaultOutputPath) {
			// Use default output location
			outputPaths.add(javaProject.getOutputLocation());
		}

		// Convert paths to containers
		Set<IContainer> outputContainers= new HashSet<>(outputPaths.size());
		Iterator<IPath> iter= outputPaths.iterator();
		while (iter.hasNext()) {
			IPath path= iter.next();
			if (javaProject.getProject().getFullPath().equals(path))
				outputContainers.add(javaProject.getProject());
			else {
				IFolder outputFolder= createFolderHandle(path);
				if (outputFolder == null || !outputFolder.isAccessible()) {
					String msg= JarPackagerMessages.JarFileExportOperation_outputContainerNotAccessible;
					addToStatus(new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, msg, null)));
				} else
					outputContainers.add(outputFolder);
			}
		}
		return outputContainers.toArray(new IContainer[outputContainers.size()]);
	}

	/**
	 * Returns an iterator on a list with files that correspond to the
	 * passed file and that are on the classpath of its project.
	 *
	 * @param	typeRootElement			the class file or compilation unit to evaluate the class files for
	 * @param	pathInJar		the path that the file has in the JAR (i.e. project and source folder segments removed)
	 * @param	progressMonitor			the progressMonitor to use
	 * @return	the iterator over the corresponding classpath files for the given file
	 * @throws CoreException if an exception occurs when looking for the files
	 */
	private Iterator<? extends IResource> filesOnClasspath(ITypeRoot typeRootElement, IPath pathInJar, IProgressMonitor progressMonitor) throws CoreException {
		IFile file= (IFile) typeRootElement.getResource();
		IJavaProject javaProject= typeRootElement.getJavaProject();
		IPackageFragmentRoot pkgRoot= JavaModelUtil.getPackageFragmentRoot(typeRootElement);

		// Allow JAR Package to provide its own strategy
		IFile[] classFiles= fJarPackage.findClassfilesFor(file);
		if (classFiles != null)
			return Arrays.asList(classFiles).iterator();

		if (!isJavaFile(file))
			return Collections.EMPTY_LIST.iterator();

		IPath outputPath= null;
		if (pkgRoot != null) {
			IClasspathEntry cpEntry= pkgRoot.getRawClasspathEntry();
			if (cpEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE)
				outputPath= cpEntry.getOutputLocation();
		}
		if (outputPath == null)
			// Use default output location
			outputPath= javaProject.getOutputLocation();

		IContainer outputContainer;
		if (javaProject.getProject().getFullPath().equals(outputPath))
			outputContainer= javaProject.getProject();
		else {
			outputContainer= createFolderHandle(outputPath);
			if (outputContainer == null || !outputContainer.isAccessible()) {
				String msg= JarPackagerMessages.JarFileExportOperation_outputContainerNotAccessible;
				throw new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, msg, null));
			}
		}

		// Java CU - search files with .class ending
		boolean hasErrors= hasCompileErrors(file);
		boolean hasWarnings= hasCompileWarnings(file);
		boolean canBeExported= canBeExported(hasErrors, hasWarnings);
		reportPossibleCompileProblems(file, hasErrors, hasWarnings, canBeExported);
		if (!canBeExported)
			return Collections.EMPTY_LIST.iterator();
		IContainer classContainer= outputContainer;
		if (pathInJar.segmentCount() > 1) {
			String lastSegment= pathInJar.segment(pathInJar.segmentCount() - 1);
			if (!JavaModelUtil.MODULE_INFO_JAVA.equals(lastSegment)
					|| !JavaModelUtil.is9OrHigher(javaProject)) {
				classContainer= outputContainer.getFolder(pathInJar.removeLastSegments(1));
			}
		}

		if (fExportedClassContainers.contains(classContainer))
			return Collections.EMPTY_LIST.iterator();

		if (JavaCore.DO_NOT_GENERATE.equals(javaProject.getOption(JavaCore.COMPILER_SOURCE_FILE_ATTR, true))) {
			IRegion region= JavaCore.newRegion();
			region.add(typeRootElement);
			IResource[] generatedResources= JavaCore.getGeneratedResources(region, false);
			if (generatedResources.length > 0)
				return Arrays.asList(generatedResources).iterator();
			// give the old code a last chance
		}
		if (fClassFilesMapContainer == null || !fClassFilesMapContainer.equals(classContainer)) {
			fJavaNameToClassFilesMap= buildJavaToClassMap(classContainer, progressMonitor);
			if (fJavaNameToClassFilesMap == null) {
				// Could not fully build map. fallback is to export whole directory
				String containerName= BasicElementLabels.getPathLabel(classContainer.getFullPath(), false);
				String msg= Messages.format(JarPackagerMessages.JarFileExportOperation_missingSourceFileAttributeExportedAll, containerName);
				addInfo(msg, null);
				fExportedClassContainers.add(classContainer);
				return getClassesIn(classContainer);
			}
			fClassFilesMapContainer= classContainer;
		}
		ArrayList<IResource> classFileList= fJavaNameToClassFilesMap.get(file.getName());
		if (classFileList == null || classFileList.isEmpty()) {
			String msg= Messages.format(JarPackagerMessages.JarFileExportOperation_classFileOnClasspathNotAccessible, BasicElementLabels.getPathLabel(file.getFullPath(), false));
			throw new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, msg, null));
		}
		return classFileList.iterator();
	}

	private Iterator<IResource> getClassesIn(IContainer classContainer) throws CoreException {
		IResource[] resources= classContainer.members();
		List<IResource> files= new ArrayList<>(resources.length);
		for (IResource resource : resources) {
			if (resource.getType() == IResource.FILE && isClassFile(resource)) {
				files.add(resource);
			}
		}
		return files.iterator();
	}

	/**
	 * Answers whether the given resource is a Java file.
	 * The resource must be a file whose file name ends with ".java",
	 * or an extension defined as Java source.
	 *
	 * @param file the file to test
	 * @return a <code>true<code> if the given resource is a Java file
	 */
	private boolean isJavaFile(IResource file) {
		return file != null
			&& file.getType() == IResource.FILE
			&& file.getFileExtension() != null
			&& JavaCore.isJavaLikeFileName(file.getName());
	}

	/**
	 * Answers whether the given resource is a class file.
	 * The resource must be a file whose file name ends with ".class".
	 *
	 * @param file the file to test
	 * @return a <code>true<code> if the given resource is a class file
	 */
	private boolean isClassFile(IResource file) {
		return file != null
			&& file.getType() == IResource.FILE
			&& file.getFileExtension() != null
			&& "class".equalsIgnoreCase(file.getFileExtension()); //$NON-NLS-1$
	}

	/*
	 * Builds and returns a map that has the class files
	 * for each java file in a given directory
	 */
	private Map<String, ArrayList<IResource>> buildJavaToClassMap(IContainer container, IProgressMonitor monitor) throws CoreException {
		if (container == null || !container.isAccessible())
			return new HashMap<>(0);
		/*
		 * XXX: Bug 6584: Need a way to get class files for a java file (or CU)
		 */
		IClassFileReader cfReader= null;
		IResource[] members= container.members();
		Map<String, ArrayList<IResource>> map= new HashMap<>(members.length);
		for (IResource member : members) {
			if (isClassFile(member)) {
				IFile classFile = (IFile) member;
				URI location= classFile.getLocationURI();
				if (location != null) {
					InputStream contents= null;
					try {
						contents= EFS.getStore(location).openInputStream(EFS.NONE, monitor);
						cfReader= ToolFactory.createDefaultClassFileReader(contents, IClassFileReader.CLASSFILE_ATTRIBUTES);
					} finally {
						try {
							if (contents != null)
								contents.close();
						} catch (IOException e) {
							throw new CoreException(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.ERROR,
								Messages.format(JarPackagerMessages.JarFileExportOperation_errorCannotCloseConnection, BasicElementLabels.getURLPart(Resources.getLocationString(classFile))),
								e));
						}
					}
					if (cfReader != null) {
						ISourceAttribute sourceAttribute= cfReader.getSourceFileAttribute();
						if (sourceAttribute == null) {
							/*
							* Can't fully build the map because one or more
							* class file does not contain the name of its
							* source file.
							*/
							addWarning(Messages.format(
								JarPackagerMessages.JarFileExportOperation_classFileWithoutSourceFileAttribute,
								BasicElementLabels.getURLPart(Resources.getLocationString(classFile))), null);
							return null;
						}
						String javaName= new String(sourceAttribute.getSourceFileName());
						ArrayList<IResource> classFiles= map.get(javaName);
						if (classFiles == null) {
							classFiles= new ArrayList<>(3);
							map.put(javaName, classFiles);
						}
						classFiles.add(classFile);
					}
				}
			}
		}
		return map;
	}

	/**
	 * Creates a folder resource handle for the folder with the given workspace path.
	 *
	 * @param folderPath the path of the folder to create a handle for
	 * @return the new folder resource handle
	 */
	private IFolder createFolderHandle(IPath folderPath) {
		if (folderPath.isValidPath(folderPath.toString()) && folderPath.segmentCount() >= 2)
			return JavaPlugin.getWorkspace().getRoot().getFolder(folderPath);
		else
			return null;
	}

	/**
	 * Handles core exceptions that are thrown by {@link IJarBuilder#writeFile(IFile, IPath)}.
	 *
	 * @param ex the core exception
	 * @since 3.5
	 */
	private void handleCoreExceptionOnExport(CoreException ex) {
		Throwable realEx= ex.getStatus().getException();
		if (realEx instanceof ZipException && realEx.getMessage() != null
				&& realEx.getMessage().startsWith("duplicate entry:")) //$NON-NLS-1$ hardcoded message string from java.util.zip.ZipOutputStream.putNextEntry(ZipEntry)
			addWarning(ex.getMessage(), realEx);
		else
			addToStatus(ex);
	}

	/**
	 * Returns the status of this operation.
	 * The result is a status object containing individual
	 * status objects.
	 *
	 * @return the status of this operation
	 */
	@Override
	public IStatus getStatus() {
		String message= null;
		switch (fStatus.getSeverity()) {
			case IStatus.OK:
			default:
				// defensive code in case new severity is defined
				message= ""; //$NON-NLS-1$
				break;
			case IStatus.INFO:
				message= JarPackagerMessages.JarFileExportOperation_exportFinishedWithInfo;
				break;
			case IStatus.WARNING:
				message= JarPackagerMessages.JarFileExportOperation_exportFinishedWithWarnings;
				break;
			case IStatus.ERROR:
				if (fJarPackages.length > 1)
					message= JarPackagerMessages.JarFileExportOperation_creationOfSomeJARsFailed;
				else
					message= JarPackagerMessages.JarFileExportOperation_jarCreationFailed;
				break;
		}
		fStatus.setMessage(message);
		return fStatus;
	}

	private boolean canBeExported(boolean hasErrors, boolean hasWarnings) {
		return (!hasErrors && !hasWarnings)
			|| (hasErrors && fJarPackage.areErrorsExported())
			|| (hasWarnings && fJarPackage.exportWarnings());
	}

	private void reportPossibleCompileProblems(IFile file, boolean hasErrors, boolean hasWarnings, boolean canBeExported) {
		if (hasErrors) {
			if (canBeExported)
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_exportedWithCompileErrors, BasicElementLabels.getPathLabel(file.getFullPath(), false)), null);
			else
				addError(Messages.format(JarPackagerMessages.JarFileExportOperation_notExportedDueToCompileErrors, BasicElementLabels.getPathLabel(file.getFullPath(), false)), null);
		}
		if (hasWarnings) {
			if (canBeExported)
				addWarning(Messages.format(JarPackagerMessages.JarFileExportOperation_exportedWithCompileWarnings, BasicElementLabels.getPathLabel(file.getFullPath(), false)), null);
			else
				addError(Messages.format(JarPackagerMessages.JarFileExportOperation_notExportedDueToCompileWarnings, BasicElementLabels.getPathLabel(file.getFullPath(), false)), null);
		}
	}

	/**
	 * Exports the resources as specified by the JAR package.
	 *
	 * @param	progressMonitor	the progress monitor that displays the progress
	 * @throws InvocationTargetException thrown when an ecxeption occurred
	 * @throws InterruptedException thrown when cancelled
	 * @see	#getStatus()
	 */
	@Override
	protected void execute(IProgressMonitor progressMonitor) throws InvocationTargetException, InterruptedException {
		int count= fJarPackages.length;
		progressMonitor.beginTask("", count); //$NON-NLS-1$
		try {
			for (int i= 0; i < count; i++) {
				SubProgressMonitor subProgressMonitor= new SubProgressMonitor(progressMonitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);
				fJarPackage= fJarPackages[i];
				if (fJarPackage != null)
					singleRun(subProgressMonitor);
			}
		} finally {
			progressMonitor.done();
		}
	}

	private void singleRun(IProgressMonitor progressMonitor) throws InvocationTargetException, InterruptedException {
		try {
			if (!preconditionsOK())
				throw new InvocationTargetException(null, JarPackagerMessages.JarFileExportOperation_jarCreationFailedSeeDetails);
			int totalWork= countSelectedElements();
			if (fJarPackage.areGeneratedFilesExported()
				&& ((!isAutoBuilding() && fJarPackage.isBuildingIfNeeded())
					|| (isAutoBuilding() && fFilesSaved))) {
				int subMonitorTicks= totalWork/10;
				totalWork += subMonitorTicks;
				progressMonitor.beginTask("", totalWork); //$NON-NLS-1$
				SubProgressMonitor subProgressMonitor= new SubProgressMonitor(progressMonitor, subMonitorTicks, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);
				buildProjects(subProgressMonitor);
			} else
				progressMonitor.beginTask("", totalWork); //$NON-NLS-1$

			fJarBuilder = fJarPackage.getJarBuilder();
			fJarBuilder.open(fJarPackage, fParentShell, fStatus);

			exportSelectedElements(progressMonitor);
			if (getStatus().getSeverity() != IStatus.ERROR) {
				progressMonitor.subTask(JarPackagerMessages.JarFileExportOperation_savingFiles);
				saveFiles();
			}
		} catch (CoreException ex) {
			addToStatus(ex);
		} finally {
			try {
				if (fJarBuilder != null)
					fJarBuilder.close();
			} catch (CoreException ex) {
				addToStatus(ex);
			}
			progressMonitor.done();
		}
	}

	private boolean preconditionsOK() {
		if (!fJarPackage.areGeneratedFilesExported() && !fJarPackage.areJavaFilesExported()) {
			addError(JarPackagerMessages.JarFileExportOperation_noExportTypeChosen, null);
			return false;
		}
		if (fJarPackage.getElements() == null || fJarPackage.getElements().length == 0) {
			addError(JarPackagerMessages.JarFileExportOperation_noResourcesSelected, null);
			return false;
		}
		if (fJarPackage.getAbsoluteJarLocation() == null) {
			addError(JarPackagerMessages.JarFileExportOperation_invalidJarLocation, null);
			return false;
		}
		File targetFile= fJarPackage.getAbsoluteJarLocation().toFile();
		if (targetFile.exists() && !targetFile.canWrite()) {
			addError(JarPackagerMessages.JarFileExportOperation_jarFileExistsAndNotWritable, null);
			return false;
		}
		if (!fJarPackage.isManifestAccessible()) {
			addError(JarPackagerMessages.JarFileExportOperation_manifestDoesNotExist, null);
			return false;
		}
		if (!fJarPackage.isMainClassValid(new BusyIndicatorRunnableContext())) {
			addError(JarPackagerMessages.JarFileExportOperation_invalidMainClass, null);
			return false;
		}

		if (fParentShell != null) {
			final boolean[] res= { false };
			fParentShell.getDisplay().syncExec(() -> {
				RefactoringSaveHelper refactoringSaveHelper= new RefactoringSaveHelper(RefactoringSaveHelper.SAVE_ALL_ALWAYS_ASK);
				res[0]= refactoringSaveHelper.saveEditors(fParentShell);
				fFilesSaved= refactoringSaveHelper.didSaveFiles();
			});
			if (!res[0]) {
				addError(JarPackagerMessages.JarFileExportOperation_fileUnsaved, null);
				return false;
			}
		}

		return true;
	}

	private void saveFiles() {
		// Save the manifest
		if (fJarPackage.areGeneratedFilesExported() && fJarPackage.isManifestGenerated() && fJarPackage.isManifestSaved()) {
			try {
				saveManifest();
			} catch (CoreException | IOException ex) {
				addError(JarPackagerMessages.JarFileExportOperation_errorSavingManifest, ex);
			}
		}

		// Save the description
		if (fJarPackage.isDescriptionSaved()) {
			try {
				saveDescription();
			} catch (CoreException | IOException ex) {
				addError(JarPackagerMessages.JarFileExportOperation_errorSavingDescription, ex);
			}
		}
	}

	private void saveDescription() throws CoreException, IOException {
		// Adjust JAR package attributes
		if (fJarPackage.isManifestReused())
			fJarPackage.setGenerateManifest(false);
		ByteArrayOutputStream objectStreamOutput= new ByteArrayOutputStream();
		IFile descriptionFile= fJarPackage.getDescriptionFile();
		String encoding= "UTF-8"; //$NON-NLS-1$
		try {
			encoding= descriptionFile.getCharset(true);
		} catch (CoreException exception) {
			JavaPlugin.log(exception);
		}
		IJarDescriptionWriter writer= fJarPackage.createJarDescriptionWriter(objectStreamOutput, encoding);
		ByteArrayInputStream fileInput= null;
		try {
			writer.write(fJarPackage);
			fileInput= new ByteArrayInputStream(objectStreamOutput.toByteArray());
			if (descriptionFile.isAccessible()) {
				if (fJarPackage.allowOverwrite() || JarPackagerUtil.askForOverwritePermission(fParentShell, descriptionFile.getFullPath(), false))
					descriptionFile.setContents(fileInput, true, true, null);
			} else
				descriptionFile.create(fileInput, true, null);
		} finally {
			if (fileInput != null)
				fileInput.close();
			if (writer != null)
				writer.close();
		}
	}

	private void saveManifest() throws CoreException, IOException {
		ByteArrayOutputStream manifestOutput= new ByteArrayOutputStream();
		Manifest manifest= fJarPackage.getManifestProvider().create(fJarPackage);
		manifest.write(manifestOutput);
		ByteArrayInputStream fileInput= new ByteArrayInputStream(manifestOutput.toByteArray());
		IFile manifestFile= fJarPackage.getManifestFile();
		if (manifestFile.isAccessible()) {
			if (fJarPackage.allowOverwrite() || JarPackagerUtil.askForOverwritePermission(fParentShell, manifestFile.getFullPath(), false))
				manifestFile.setContents(fileInput, true, true, null);
		} else
			manifestFile.create(fileInput, true, null);
	}

	private boolean isAutoBuilding() {
		return ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
	}

	private void buildProjects(IProgressMonitor progressMonitor) {
		Set<IProject> builtProjects= new HashSet<>(10);
		for (Object e : fJarPackage.getElements()) {
			IProject project= null;
			Object element= e;
			if (element instanceof IResource)
				project= ((IResource)element).getProject();
			else if (element instanceof IJavaElement)
				project= ((IJavaElement)element).getJavaProject().getProject();
			if (project != null && !builtProjects.contains(project)) {
				try {
					project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, progressMonitor);
				} catch (CoreException ex) {
					String message= Messages.format(JarPackagerMessages.JarFileExportOperation_errorDuringProjectBuild, BasicElementLabels.getResourceName(project));
					addError(message, ex);
				} finally {
					// don't try to build same project a second time even if it failed
					builtProjects.add(project);
				}
			}
		}
	}

	/**
	 * Tells whether the given resource (or its children) have compile errors.
	 * The method acts on the current build state and does not recompile.
	 *
	 * @param resource the resource to check for errors
	 * @return <code>true</code> if the resource (and its children) are error free
	 * @throws CoreException import org.eclipse.core.runtime.CoreException if there's a marker problem
	 */
	private boolean hasCompileErrors(IResource resource) throws CoreException {
		for (IMarker problemMarker : resource.findMarkers(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE)) {
			if (problemMarker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Tells whether the given resource (or its children) have compile errors.
	 * The method acts on the current build state and does not recompile.
	 *
	 * @param resource the resource to check for errors
	 * @return <code>true</code> if the resource (and its children) are error free
	 * @throws CoreException import org.eclipse.core.runtime.CoreException if there's a marker problem
	 */
	private boolean hasCompileWarnings(IResource resource) throws CoreException {
		for (IMarker problemMarker : resource.findMarkers(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE)) {
			if (problemMarker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_WARNING) {
				return true;
			}
		}
		return false;
	}

	private boolean mustUseSourceFolderHierarchy() {
		return fJarPackage.useSourceFolderHierarchy() && fJarPackage.areJavaFilesExported() && !fJarPackage.areGeneratedFilesExported();
	}
}
