/*******************************************************************************
 * Copyright (c) 2006, 2019 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
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.apt.util;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.zip.ZipException;

import javax.lang.model.SourceVersion;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;

import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.apt.util.JrtFileSystem.JrtFileObject;
import org.eclipse.jdt.internal.compiler.apt.util.ModuleLocationHandler.LocationContainer;
import org.eclipse.jdt.internal.compiler.apt.util.ModuleLocationHandler.LocationWrapper;
import org.eclipse.jdt.internal.compiler.apt.util.ModuleLocationHandler.ModuleLocationWrapper;
import org.eclipse.jdt.internal.compiler.batch.FileSystem;
import org.eclipse.jdt.internal.compiler.batch.FileSystem.Classpath;
import org.eclipse.jdt.internal.compiler.batch.Main;
import org.eclipse.jdt.internal.compiler.batch.Main.ResourceBundleFactory;
import org.eclipse.jdt.internal.compiler.batch.ModuleFinder;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.compiler.env.AccessRule;
import org.eclipse.jdt.internal.compiler.env.AccessRuleSet;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.parser.Parser;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.compiler.util.Util;

/**
 * Implementation of the Standard Java File Manager
 */
public class EclipseFileManager implements StandardJavaFileManager {
	private static final String NO_EXTENSION = "";//$NON-NLS-1$
	static final int HAS_EXT_DIRS = 1;
	static final int HAS_BOOTCLASSPATH = 2;
	static final int HAS_ENDORSED_DIRS = 4;
	static final int HAS_PROCESSORPATH = 8;
	static final int HAS_PROC_MODULEPATH = 16;

	Map<File, Archive> archivesCache;
	Charset charset;
	Locale locale;
	ModuleLocationHandler locationHandler;
	final Map<Location, URLClassLoader> classloaders;
	int flags;
	boolean isOnJvm9;
	File jrtHome;
	JrtFileSystem jrtSystem;
	public ResourceBundle bundle;
	String releaseVersion;
	
	public EclipseFileManager(Locale locale, Charset charset) {
		this.locale = locale == null ? Locale.getDefault() : locale;
		this.charset = charset == null ? Charset.defaultCharset() : charset;
		this.locationHandler = new ModuleLocationHandler();
		this.classloaders = new HashMap<>();
		this.archivesCache = new HashMap<>();
		this.isOnJvm9 = isRunningJvm9();
		try {
			initialize(Util.getJavaHome());
		} catch (IOException e) {
			e.printStackTrace();
			// ignore
		}
		try {
			this.bundle = ResourceBundleFactory.getBundle(this.locale);
		} catch(MissingResourceException e) {
			System.out.println("Missing resource : " + Main.bundleName.replace('.', '/') + ".properties for locale " + locale); //$NON-NLS-1$//$NON-NLS-2$
		}
	}
	protected void initialize(File javahome) throws IOException {
		if (this.isOnJvm9) {
			this.jrtSystem = new JrtFileSystem(javahome);
			this.archivesCache.put(javahome, this.jrtSystem);
			this.jrtHome = javahome;
			this.locationHandler.newSystemLocation(StandardLocation.SYSTEM_MODULES, this.jrtSystem);
		} else {
			this.setLocation(StandardLocation.PLATFORM_CLASS_PATH, getDefaultBootclasspath());
		}
		Iterable<? extends File> defaultClasspath = getDefaultClasspath();
		this.setLocation(StandardLocation.CLASS_PATH, defaultClasspath);
		// No annotation module path by default
		this.setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, defaultClasspath);
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#close()
	 */
	@Override
	public void close() throws IOException {
		this.locationHandler.close();
		for (Archive archive : this.archivesCache.values()) {
			archive.close();
		}
		this.archivesCache.clear();
		for (URLClassLoader cl : this.classloaders.values()) {
			cl.close();
		}
		this.classloaders.clear();
	}
	
	private void collectAllMatchingFiles(Location location, File file, String normalizedPackageName, Set<Kind> kinds, boolean recurse, ArrayList<JavaFileObject> collector) {
		if (file.equals(this.jrtHome)) {
			if (location instanceof ModuleLocationWrapper) {
				List<JrtFileObject> list = this.jrtSystem.list((ModuleLocationWrapper) location, normalizedPackageName, kinds, recurse, this.charset);
				for (JrtFileObject fo : list) {
					Kind kind = getKind(getExtension(fo.entryName));
					if (kinds.contains(kind)) {
						collector.add(fo);
					}
				}
			}
		} else if (isArchive(file)) {
			Archive archive = this.getArchive(file);
			if (archive == Archive.UNKNOWN_ARCHIVE) return;
			String key = normalizedPackageName;
			if (!normalizedPackageName.endsWith("/")) {//$NON-NLS-1$
				key += '/';
			}
			// we have an archive file
			if (recurse) {
				for (String packageName : archive.allPackages()) {
					if (packageName.startsWith(key)) {
						List<String[]> types = archive.getTypes(packageName);
						if (types != null) {
							for (String[] entry : types) {
								final Kind kind = getKind(getExtension(entry[0]));
								if (kinds.contains(kind)) {
									collector.add(archive.getArchiveFileObject(packageName + entry[0], entry[1], this.charset));
								}
							}
						}
					}
				}
			} else {
				List<String[]> types = archive.getTypes(key);
				if (types != null) {
					for (String[] entry : types) {
						final Kind kind = getKind(getExtension(entry[0]));
						if (kinds.contains(kind)) {
							collector.add(archive.getArchiveFileObject(key + entry[0], entry[1], this.charset));
						}
					}
				}
			}
		} else {
			// we must have a directory
			File currentFile = new File(file, normalizedPackageName);
			if (!currentFile.exists()) return;
			String path;
			try {
				path = currentFile.getCanonicalPath();
			} catch (IOException e) {
				return;
			}
			if (File.separatorChar == '/') {
				if (!path.endsWith(normalizedPackageName)) return;
			} else if (!path.endsWith(normalizedPackageName.replace('/', File.separatorChar))) return;
			File[] files = currentFile.listFiles();
			if (files != null) {
				// this was a directory
				for (File f : files) {
					if (f.isDirectory() && recurse) {
						collectAllMatchingFiles(location, file, normalizedPackageName + '/' + f.getName(), kinds, recurse, collector);
					} else {
						final Kind kind = getKind(f);
						if (kinds.contains(kind)) {
							collector.add(new EclipseFileObject(normalizedPackageName + f.getName(), f.toURI(), kind, this.charset));
						}
					}
				}
			}
		}
	}

	private Iterable<? extends File> concatFiles(Iterable<? extends File> iterable, Iterable<? extends File> iterable2) {
		ArrayList<File> list = new ArrayList<>();
		if (iterable2 == null) return iterable;
		for (Iterator<? extends File> iterator = iterable.iterator(); iterator.hasNext(); ) {
			list.add(iterator.next());
		}
		for (Iterator<? extends File> iterator = iterable2.iterator(); iterator.hasNext(); ) {
			list.add(iterator.next());
		}
		return list;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#flush()
	 */
	@Override
	public void flush() throws IOException {
		for (Archive archive : this.archivesCache.values()) {
			archive.flush();
		}
	}

	private Archive getArchive(File f) {
		// check the archive (jar/zip) cache
		Archive archive = this.archivesCache.get(f);
		if (archive == null) {
			archive = Archive.UNKNOWN_ARCHIVE;
			// create a new archive
			if (f.exists()) {
				try {
					archive = new Archive(f);
				} catch (ZipException e) {
					// ignore
				} catch (IOException e) {
					// ignore
				}
				if (archive != null) {
					this.archivesCache.put(f, archive);
				}
			}
			this.archivesCache.put(f, archive);
		}
		return archive;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#getClassLoader(javax.tools.JavaFileManager.Location)
	 */
	@Override
	public ClassLoader getClassLoader(Location location) {
		validateNonModuleLocation(location);
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			// location is unknown
			return null;
		}
		URLClassLoader cl = this.classloaders.get(location);
		if (cl == null) {
			ArrayList<URL> allURLs = new ArrayList<>();
			for (File f : files) {
				try {
					allURLs.add(f.toURI().toURL());
				} catch (MalformedURLException e) {
					// the url is malformed - this should not happen
					throw new RuntimeException(e);
				}
			}
			URL[] result = new URL[allURLs.size()];
			cl = new URLClassLoader(allURLs.toArray(result), getClass().getClassLoader());
			this.classloaders.put(location, cl);
		}
		return cl;
	}

	private Iterable<? extends File> getPathsFrom(String path) {
		ArrayList<FileSystem.Classpath> paths = new ArrayList<>();
		ArrayList<File> files = new ArrayList<>();
		try {
			this.processPathEntries(Main.DEFAULT_SIZE_CLASSPATH, paths, path, this.charset.name(), false, false);
		} catch (IllegalArgumentException e) {
			return null;
		}
		for (FileSystem.Classpath classpath : paths) {
			files.add(new File(classpath.getPath()));
		}
		return files;
	}

	Iterable<? extends File> getDefaultBootclasspath() {
		List<File> files = new ArrayList<>();
		String javaversion = System.getProperty("java.version");//$NON-NLS-1$
		if(javaversion.length() > 3)
			javaversion = javaversion.substring(0, 3);
		long jdkLevel = CompilerOptions.versionToJdkLevel(javaversion);
		if (jdkLevel < ClassFileConstants.JDK1_6) {
			// wrong jdk - 1.6 or above is required
			return null;
		}

		for (FileSystem.Classpath classpath : org.eclipse.jdt.internal.compiler.util.Util.collectFilesNames()) {
			files.add(new File(classpath.getPath()));
		}
		return files;
	}

	Iterable<? extends File> getDefaultClasspath() {
		// default classpath
		ArrayList<File> files = new ArrayList<>();
		String classProp = System.getProperty("java.class.path"); //$NON-NLS-1$
		if ((classProp == null) || (classProp.length() == 0)) {
			return null;
		} else {
			StringTokenizer tokenizer = new StringTokenizer(classProp, File.pathSeparator);
			String token;
			while (tokenizer.hasMoreTokens()) {
				token = tokenizer.nextToken();
				File file = new File(token);
				if (file.exists()) {
					files.add(file);
				}
			}
		}
		return files;
	}

	private Iterable<? extends File> getEndorsedDirsFrom(String path) {
		ArrayList<FileSystem.Classpath> paths = new ArrayList<>();
		ArrayList<File> files = new ArrayList<>();
		try {
			this.processPathEntries(Main.DEFAULT_SIZE_CLASSPATH, paths, path, this.charset.name(), false, false);
		} catch (IllegalArgumentException e) {
			return null;
		}
		for (FileSystem.Classpath classpath : paths) {
			files.add(new File(classpath.getPath()));
		}
		return files;
	}

	private Iterable<? extends File> getExtdirsFrom(String path) {
		ArrayList<FileSystem.Classpath> paths = new ArrayList<>();
		ArrayList<File> files = new ArrayList<>();
		try {
			this.processPathEntries(Main.DEFAULT_SIZE_CLASSPATH, paths, path, this.charset.name(), false, false);
		} catch (IllegalArgumentException e) {
			return null;
		}
		for (FileSystem.Classpath classpath : paths) {
			files.add(new File(classpath.getPath()));
		}
		return files;
	}

	private String getExtension(File file) {
		String name = file.getName();
		return getExtension(name);
	}
	private String getExtension(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return EclipseFileManager.NO_EXTENSION;
		}
		return name.substring(index);
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#getFileForInput(javax.tools.JavaFileManager.Location, java.lang.String, java.lang.String)
	 */
	@Override
	public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
		validateNonModuleLocation(location);
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
		}
		String normalizedFileName = normalizedFileName(packageName, relativeName);
		for (File file : files) {
			if (file.isDirectory()) {
				// handle directory
				File f = new File(file, normalizedFileName);
				if (f.exists()) {
					return new EclipseFileObject(packageName + File.separator + relativeName, f.toURI(), getKind(f), this.charset);
				} else {
					continue; // go to next entry in the location
				}
			} else if (isArchive(file)) {
				// handle archive file
				Archive archive = getArchive(file);
				if (archive != Archive.UNKNOWN_ARCHIVE) {
					if (archive.contains(normalizedFileName)) {
						return archive.getArchiveFileObject(normalizedFileName, null, this.charset);
					}
				}
			}
		}
		return null;
	}

	private String normalizedFileName(String packageName, String relativeName) {
		StringBuilder sb = new StringBuilder();
		sb.append(normalized(packageName));
		if (sb.length() > 0) {
			sb.append('/');
		}
		sb.append(relativeName.replace('\\', '/'));
		return sb.toString();
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#getFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, java.lang.String, javax.tools.FileObject)
	 */
	@Override
	public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling)
			throws IOException {
		validateOutputLocation(location);
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
		}
		final Iterator<? extends File> iterator = files.iterator();
		if (iterator.hasNext()) {
			File file = iterator.next();
			String normalizedFileName = normalized(packageName) + '/' + relativeName.replace('\\', '/');
			File f = new File(file, normalizedFileName);
			return new EclipseFileObject(packageName + File.separator + relativeName, f.toURI(), getKind(f), this.charset);
		} else {
			throw new IllegalArgumentException("location is empty : " + location);//$NON-NLS-1$
		}
	}
	
	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#getJavaFileForInput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind)
	 */
	@Override
	public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {
		validateNonModuleLocation(location);
		if (kind != Kind.CLASS && kind != Kind.SOURCE) {
			throw new IllegalArgumentException("Invalid kind : " + kind);//$NON-NLS-1$
		}
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
		}
		String normalizedFileName = normalized(className);
		normalizedFileName += kind.extension;
		for (File file : files) {
			if (file.isDirectory()) {
				// handle directory
				File f = new File(file, normalizedFileName);
				if (f.exists()) {
					return new EclipseFileObject(className, f.toURI(), kind, this.charset);
				} else {
					continue; // go to next entry in the location
				}
			} else if (isArchive(file)) {
				// handle archive file
				Archive archive = getArchive(file);
				if (archive != Archive.UNKNOWN_ARCHIVE) {
					if (archive.contains(normalizedFileName)) {
						return archive.getArchiveFileObject(normalizedFileName, null, this.charset);
					}
				}
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
	 */
	@Override
	public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
			throws IOException {
		validateOutputLocation(location);
		if (kind != Kind.CLASS && kind != Kind.SOURCE) {
			throw new IllegalArgumentException("Invalid kind : " + kind);//$NON-NLS-1$
		}
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			if (!location.equals(StandardLocation.CLASS_OUTPUT)
					&& !location.equals(StandardLocation.SOURCE_OUTPUT))
				throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
			// we will use either the sibling or user.dir
			if (sibling != null) {
				String normalizedFileName = normalized(className);
				int index = normalizedFileName.lastIndexOf('/');
				if (index != -1) {
					normalizedFileName = normalizedFileName.substring(index + 1);
				}
				normalizedFileName += kind.extension;
				URI uri = sibling.toUri();
				URI uri2 = null;
				try {
					String path = uri.getPath();
					index = path.lastIndexOf('/');
					if (index != -1) {
						path = path.substring(0, index + 1);
						path += normalizedFileName;
					}
					uri2 = new URI(uri.getScheme(), uri.getHost(), path, uri.getFragment());
				} catch (URISyntaxException e) {
					throw new IllegalArgumentException("invalid sibling", e);//$NON-NLS-1$
				}
				return new EclipseFileObject(className, uri2, kind, this.charset);
			} else {
				String normalizedFileName = normalized(className);
				normalizedFileName += kind.extension;
				File f = new File(System.getProperty("user.dir"), normalizedFileName);//$NON-NLS-1$
				return new EclipseFileObject(className, f.toURI(), kind, this.charset);
			}
		}
		final Iterator<? extends File> iterator = files.iterator();
		if (iterator.hasNext()) {
			File file = iterator.next();
			String normalizedFileName = normalized(className);
			normalizedFileName += kind.extension;
			File f = new File(file, normalizedFileName);
			return new EclipseFileObject(className, f.toURI(), kind, this.charset);
		} else {
			throw new IllegalArgumentException("location is empty : " + location);//$NON-NLS-1$
		}
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#getJavaFileObjects(java.io.File[])
	 */
	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjects(File... files) {
		return getJavaFileObjectsFromFiles(Arrays.asList(files));
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#getJavaFileObjects(java.lang.String[])
	 */
	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjects(String... names) {
		return getJavaFileObjectsFromStrings(Arrays.asList(names));
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#getJavaFileObjectsFromFiles(java.lang.Iterable)
	 */
	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
		ArrayList<JavaFileObject> javaFileArrayList = new ArrayList<>();
		for (File f : files) {
			if (f.isDirectory()) {
				throw new IllegalArgumentException("file : " + f.getAbsolutePath() + " is a directory"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			javaFileArrayList.add(new EclipseFileObject(f.getAbsolutePath(), f.toURI(), getKind(f), this.charset));
		}
		return javaFileArrayList;
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#getJavaFileObjectsFromStrings(java.lang.Iterable)
	 */
	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(Iterable<String> names) {
		ArrayList<File> files = new ArrayList<>();
		for (String name : names) {
			files.add(new File(name));
		}
		return getJavaFileObjectsFromFiles(files);
	}

	public Kind getKind(File f) {
		return getKind(getExtension(f));
	}

	private Kind getKind(String extension) {
		if (Kind.CLASS.extension.equals(extension)) {
			return Kind.CLASS;
		} else if (Kind.SOURCE.extension.equals(extension)) {
			return Kind.SOURCE;
		} else if (Kind.HTML.extension.equals(extension)) {
			return Kind.HTML;
		}
		return Kind.OTHER;
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#getLocation(javax.tools.JavaFileManager.Location)
	 */
	@Override
	public Iterable<? extends File> getLocation(Location location) {
		if (location instanceof LocationWrapper) {
			return getFiles(((LocationWrapper) location).paths);
		}
		LocationWrapper loc = this.locationHandler.getLocation(location, ""); //$NON-NLS-1$
		if (loc == null) {
			return null;
		}
		return getFiles(loc.getPaths());
	}

	private Iterable<? extends File> getOutputDir(String string) {
		if ("none".equals(string)) {//$NON-NLS-1$
			return null;
		}
		File file = new File(string);
		if (file.exists() && !file.isDirectory()) {
			throw new IllegalArgumentException("file : " + file.getAbsolutePath() + " is not a directory");//$NON-NLS-1$//$NON-NLS-2$
		}
		ArrayList<File> list = new ArrayList<>(1);
		list.add(file);
		return list;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#handleOption(java.lang.String, java.util.Iterator)
	 */
	@Override
	public boolean handleOption(String current, Iterator<String> remaining) {
		try {
			switch(current) {
				case "-bootclasspath": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> bootclasspaths = getPathsFrom(remaining.next());
						if (bootclasspaths != null) {
							Iterable<? extends File> iterable = getLocation(StandardLocation.PLATFORM_CLASS_PATH);
							if ((this.flags & EclipseFileManager.HAS_ENDORSED_DIRS) == 0
									&& (this.flags & EclipseFileManager.HAS_EXT_DIRS) == 0) {
								// override default bootclasspath
								setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootclasspaths);
							} else if ((this.flags & EclipseFileManager.HAS_ENDORSED_DIRS) != 0) {
								// endorseddirs have been processed first
								setLocation(StandardLocation.PLATFORM_CLASS_PATH, 
										concatFiles(iterable, bootclasspaths));
							} else {
								// extdirs have been processed first
								setLocation(StandardLocation.PLATFORM_CLASS_PATH, 
										prependFiles(iterable, bootclasspaths));
							}
						}
						this.flags |= EclipseFileManager.HAS_BOOTCLASSPATH;
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--system": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> classpaths = getPathsFrom(remaining.next());
						if (classpaths != null) {
							Iterable<? extends File> iterable = getLocation(StandardLocation.SYSTEM_MODULES);
							if (iterable != null) {
								setLocation(StandardLocation.SYSTEM_MODULES,
									concatFiles(iterable, classpaths));
							} else {
								setLocation(StandardLocation.SYSTEM_MODULES, classpaths);
							}
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--upgrade-module-path": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> classpaths = getPathsFrom(remaining.next());
						if (classpaths != null) {
							Iterable<? extends File> iterable = getLocation(StandardLocation.UPGRADE_MODULE_PATH);
							if (iterable != null) {
								setLocation(StandardLocation.UPGRADE_MODULE_PATH,
									concatFiles(iterable, classpaths));
							} else {
								setLocation(StandardLocation.UPGRADE_MODULE_PATH, classpaths);
							}
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-classpath": //$NON-NLS-1$
				case "-cp": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> classpaths = getPathsFrom(remaining.next());
						if (classpaths != null) {
							Iterable<? extends File> iterable = getLocation(StandardLocation.CLASS_PATH);
							if (iterable != null) {
								setLocation(StandardLocation.CLASS_PATH,
									concatFiles(iterable, classpaths));
							} else {
								setLocation(StandardLocation.CLASS_PATH, classpaths);
							}
							if ((this.flags & EclipseFileManager.HAS_PROCESSORPATH) == 0) {
								setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, classpaths);
							} else if ((this.flags & EclipseFileManager.HAS_PROC_MODULEPATH) == 0) {
								if (this.isOnJvm9)
									setLocation(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, classpaths);
							}
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--module-path": //$NON-NLS-1$
				case "-p": //$NON-NLS-1$
					final Iterable<? extends File> classpaths = getPathsFrom(remaining.next());
					if (classpaths != null) {
						Iterable<? extends File> iterable = getLocation(StandardLocation.MODULE_PATH);
						if (iterable != null) {
							setLocation(StandardLocation.MODULE_PATH, concatFiles(iterable, classpaths));
						} else {
							setLocation(StandardLocation.MODULE_PATH, classpaths);
						}
						if ((this.flags & EclipseFileManager.HAS_PROCESSORPATH) == 0) {
							setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, classpaths);
						} else if ((this.flags & EclipseFileManager.HAS_PROC_MODULEPATH) == 0) {
							if (this.isOnJvm9)
								setLocation(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, classpaths);
						}
					}
					return true;
				case "-encoding": //$NON-NLS-1$
					if (remaining.hasNext()) {
						this.charset = Charset.forName(remaining.next());
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-sourcepath": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> sourcepaths = getPathsFrom(remaining.next());
						if (sourcepaths != null) setLocation(StandardLocation.SOURCE_PATH, sourcepaths);
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--module-source-path": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> sourcepaths = getPathsFrom(remaining.next());
						if (sourcepaths != null && this.isOnJvm9) 
							setLocation(StandardLocation.MODULE_SOURCE_PATH, sourcepaths);
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-extdirs": //$NON-NLS-1$
					if (this.isOnJvm9) {
						throw new IllegalArgumentException();
					}
					if (remaining.hasNext()) {
						Iterable<? extends File> iterable = getLocation(StandardLocation.PLATFORM_CLASS_PATH);
						setLocation(StandardLocation.PLATFORM_CLASS_PATH, 
								concatFiles(iterable, getExtdirsFrom(remaining.next())));
						this.flags |= EclipseFileManager.HAS_EXT_DIRS;
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-endorseddirs": //$NON-NLS-1$
					if (remaining.hasNext()) {
						Iterable<? extends File> iterable = getLocation(StandardLocation.PLATFORM_CLASS_PATH);
						setLocation(StandardLocation.PLATFORM_CLASS_PATH, 
								prependFiles(iterable, getEndorsedDirsFrom(remaining.next())));
						this.flags |= EclipseFileManager.HAS_ENDORSED_DIRS;
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-d": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> outputDir = getOutputDir(remaining.next());
						if (outputDir != null) {
							setLocation(StandardLocation.CLASS_OUTPUT, outputDir);
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-s": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> outputDir = getOutputDir(remaining.next());
						if (outputDir != null) {
							setLocation(StandardLocation.SOURCE_OUTPUT, outputDir);
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "-processorpath": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> processorpaths = getPathsFrom(remaining.next());
						if (processorpaths != null) {
							setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, processorpaths);
						}
						this.flags |= EclipseFileManager.HAS_PROCESSORPATH;
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--processor-module-path": //$NON-NLS-1$
					if (remaining.hasNext()) {
						final Iterable<? extends File> processorpaths = getPathsFrom(remaining.next());
						if (processorpaths != null && this.isOnJvm9) {
							setLocation(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, processorpaths);
							this.flags |= EclipseFileManager.HAS_PROC_MODULEPATH;
						}
						return true;
					} else {
						throw new IllegalArgumentException();
					}
				case "--release": //$NON-NLS-1$
					if (remaining.hasNext()) {
						this.releaseVersion = remaining.next();
						return true;
					} else {
						throw new IllegalArgumentException();
					}
			}
		} catch (IOException e) {
			// ignore
		}
		return false;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#hasLocation(javax.tools.JavaFileManager.Location)
	 */
	@Override
	public boolean hasLocation(Location location) {
		try {
			return getLocationForModule(location, "") != null; //$NON-NLS-1$
		} catch (IOException e) {
			// nothing to do
		}
		return false;
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#inferBinaryName(javax.tools.JavaFileManager.Location, javax.tools.JavaFileObject)
	 */
	@Override
	public String inferBinaryName(Location location, JavaFileObject file) {
		validateNonModuleLocation(location);
		String name = file.getName();
		JavaFileObject javaFileObject = null;
		int index = name.lastIndexOf('.');
		if (index != -1) {
			name = name.substring(0, index);
		}
		try {
			javaFileObject = getJavaFileForInput(location, name, file.getKind());
		} catch (IOException e) {
			// ignore
		} catch (IllegalArgumentException iae) {
			return null; // Either unknown kind or location not present
		}
		if (javaFileObject == null) {
			return null;
		}
		return name.replace('/', '.');
	}

	private boolean isArchive(File f) {
		String extension = getExtension(f);
		return extension.equalsIgnoreCase(".jar") || extension.equalsIgnoreCase(".zip");//$NON-NLS-1$//$NON-NLS-2$
	}

	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#isSameFile(javax.tools.FileObject, javax.tools.FileObject)
	 */
	@Override
	public boolean isSameFile(FileObject fileObject1, FileObject fileObject2) {
		// EclipseFileManager creates only EcliseFileObject
		if (!(fileObject1 instanceof EclipseFileObject)) throw new IllegalArgumentException("Unsupported file object class : " + fileObject1.getClass());//$NON-NLS-1$
		if (!(fileObject2 instanceof EclipseFileObject)) throw new IllegalArgumentException("Unsupported file object class : " + fileObject2.getClass());//$NON-NLS-1$
		return fileObject1.equals(fileObject2);
	}
	/* (non-Javadoc)
	 * @see javax.tools.OptionChecker#isSupportedOption(java.lang.String)
	 */
	@Override
	public int isSupportedOption(String option) {
		return Options.processOptionsFileManager(option);
	}

	/* (non-Javadoc)
	 * @see javax.tools.JavaFileManager#list(javax.tools.JavaFileManager.Location, java.lang.String, java.util.Set, boolean)
	 */
	@Override
	public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse)
			throws IOException {
		validateNonModuleLocation(location);
		Iterable<? extends File> allFilesInLocations = getLocation(location);
		if (allFilesInLocations == null) {
			throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
		}
		
		ArrayList<JavaFileObject> collector = new ArrayList<>();
		String normalizedPackageName = normalized(packageName);
		for (File file : allFilesInLocations) {
			collectAllMatchingFiles(location, file, normalizedPackageName, kinds, recurse, collector);
		}
		return collector;
	}

	private String normalized(String className) {
		char[] classNameChars = className.toCharArray();
		for (int i = 0, max = classNameChars.length; i < max; i++) {
			switch(classNameChars[i]) {
				case '\\' :
					classNameChars[i] = '/';
					break;
				case '.' :
					classNameChars[i] = '/';
			}
		}
		return new String(classNameChars);
	}

	private Iterable<? extends File> prependFiles(Iterable<? extends File> iterable,
			Iterable<? extends File> iterable2) {
		if (iterable2 == null) return iterable;
		ArrayList<File> list = new ArrayList<>();
		for (Iterator<? extends File> iterator = iterable2.iterator(); iterator.hasNext(); ) {
			list.add(iterator.next());
		}
		if (iterable != null) {
			for (Iterator<? extends File> iterator = iterable.iterator(); iterator.hasNext(); ) {
				list.add(iterator.next());
			}
		}
		return list;
	}
	private boolean isRunningJvm9() {
		return (SourceVersion.latest().compareTo(SourceVersion.RELEASE_8) > 0);
	}
	/* (non-Javadoc)
	 * @see javax.tools.StandardJavaFileManager#setLocation(javax.tools.JavaFileManager.Location, java.lang.Iterable)
	 */
	@Override
	public void setLocation(Location location, Iterable<? extends File> files) throws IOException {
		if (location.isOutputLocation()) {
			// output location
			int count = 0;
			for (Iterator<? extends File> iterator = files.iterator(); iterator.hasNext(); ) {
				iterator.next();
				count++;
			}
			if (count != 1) {
				throw new IllegalArgumentException("output location can only have one path");//$NON-NLS-1$
			}
		}
		this.locationHandler.setLocation(location, "", getPaths(files)); //$NON-NLS-1$
	}
	
	public void setLocale(Locale locale) {
		this.locale = locale == null ? Locale.getDefault() : locale;
		try {
			this.bundle = ResourceBundleFactory.getBundle(this.locale);
		} catch(MissingResourceException e) {
			System.out.println("Missing resource : " + Main.bundleName.replace('.', '/') + ".properties for locale " + locale); //$NON-NLS-1$//$NON-NLS-2$
			throw e;
		}
	}

	@SuppressWarnings({"unchecked", "rawtypes"})
	public void processPathEntries(final int defaultSize, final ArrayList paths,
			final String currentPath, String customEncoding, boolean isSourceOnly,
			boolean rejectDestinationPathOnJars) {

		String currentClasspathName = null;
		String currentDestinationPath = null;
		ArrayList currentRuleSpecs = new ArrayList(defaultSize);
		StringTokenizer tokenizer = new StringTokenizer(currentPath,
				File.pathSeparator + "[]", true); //$NON-NLS-1$
		ArrayList tokens = new ArrayList();
		while (tokenizer.hasMoreTokens()) {
			tokens.add(tokenizer.nextToken());
		}
		// state machine
		final int start = 0;
		final int readyToClose = 1;
		// 'path' 'path1[rule];path2'
		final int readyToCloseEndingWithRules = 2;
		// 'path[rule]' 'path1;path2[rule]'
		final int readyToCloseOrOtherEntry = 3;
		// 'path[rule];' 'path;' 'path1;path2;'
		final int rulesNeedAnotherRule = 4;
		// 'path[rule1;'
		final int rulesStart = 5;
		// 'path[' 'path1;path2['
		final int rulesReadyToClose = 6;
		// 'path[rule' 'path[rule1;rule2'
		final int destinationPathReadyToClose = 7;
		// 'path[-d bin'
		final int readyToCloseEndingWithDestinationPath = 8;
		// 'path[-d bin]' 'path[rule][-d bin]'
		final int destinationPathStart = 9;
		// 'path[rule]['
		final int bracketOpened = 10;
		// '.*[.*'
		final int bracketClosed = 11;
		// '.*([.*])+'
	
		final int error = 99;
		int state = start;
		String token = null;
		int cursor = 0, tokensNb = tokens.size(), bracket = -1;
		while (cursor < tokensNb && state != error) {
			token = (String) tokens.get(cursor++);
			if (token.equals(File.pathSeparator)) {
				switch (state) {
				case start:
				case readyToCloseOrOtherEntry:
				case bracketOpened:
					break;
				case readyToClose:
				case readyToCloseEndingWithRules:
				case readyToCloseEndingWithDestinationPath:
					state = readyToCloseOrOtherEntry;
					addNewEntry(paths, currentClasspathName, currentRuleSpecs,
							customEncoding, currentDestinationPath, isSourceOnly,
							rejectDestinationPathOnJars);
					currentRuleSpecs.clear();
					break;
				case rulesReadyToClose:
					state = rulesNeedAnotherRule;
					break;
				case destinationPathReadyToClose:
					throw new IllegalArgumentException(
							this.bind("configure.incorrectDestinationPathEntry", //$NON-NLS-1$
									currentPath));
				case bracketClosed:
					cursor = bracket + 1;
					state = rulesStart;
					break;
				default:
					state = error;
				}
			} else if (token.equals("[")) { //$NON-NLS-1$
				switch (state) {
				case start:
					currentClasspathName = ""; //$NON-NLS-1$
					//$FALL-THROUGH$
				case readyToClose:
					bracket = cursor - 1;
					//$FALL-THROUGH$
				case bracketClosed:
					state = bracketOpened;
					break;
				case readyToCloseEndingWithRules:
					state = destinationPathStart;
					break;
				case readyToCloseEndingWithDestinationPath:
					state = rulesStart;
					break;
				case bracketOpened:
				default:
					state = error;
				}
			} else if (token.equals("]")) { //$NON-NLS-1$
				switch (state) {
				case rulesReadyToClose:
					state = readyToCloseEndingWithRules;
					break;
				case destinationPathReadyToClose:
					state = readyToCloseEndingWithDestinationPath;
					break;
				case bracketOpened:
					state = bracketClosed;
					break;
				case bracketClosed:
				default:
					state = error;
				}
			} else {
				// regular word
				switch (state) {
				case start:
				case readyToCloseOrOtherEntry:
					state = readyToClose;
					currentClasspathName = token;
					break;
				case rulesStart:
					if (token.startsWith("-d ")) { //$NON-NLS-1$
						if (currentDestinationPath != null) {
							throw new IllegalArgumentException(
									this.bind("configure.duplicateDestinationPathEntry", //$NON-NLS-1$
											currentPath));
						}
						currentDestinationPath = token.substring(3).trim();
						state = destinationPathReadyToClose;
						break;
					} // else we proceed with a rule
					//$FALL-THROUGH$
				case rulesNeedAnotherRule:
					if (currentDestinationPath != null) {
						throw new IllegalArgumentException(
								this.bind("configure.accessRuleAfterDestinationPath", //$NON-NLS-1$
									currentPath));
					}
					state = rulesReadyToClose;
					currentRuleSpecs.add(token);
					break;
				case destinationPathStart:
					if (!token.startsWith("-d ")) { //$NON-NLS-1$
						state = error;
					} else {
						currentDestinationPath = token.substring(3).trim();
						state = destinationPathReadyToClose;
					}
					break;
				case bracketClosed:
					for (int i = bracket; i < cursor ; i++) {
						currentClasspathName += (String) tokens.get(i);
					}
					state = readyToClose;
					break;
				case bracketOpened:
					break;
				default:
					state = error;
				}
			}
			if (state == bracketClosed && cursor == tokensNb) {
				cursor = bracket + 1;
				state = rulesStart;
			}
		}
		switch(state) {
			case readyToCloseOrOtherEntry:
				break;
			case readyToClose:
			case readyToCloseEndingWithRules:
			case readyToCloseEndingWithDestinationPath:
				addNewEntry(paths, currentClasspathName, currentRuleSpecs,
					customEncoding, currentDestinationPath, isSourceOnly,
					rejectDestinationPathOnJars);
				break;
			case bracketOpened:
			case bracketClosed:
			default :
				// we go on anyway
		}
	}
	@SuppressWarnings({"rawtypes", "unchecked"})
	protected void addNewEntry(ArrayList paths, String currentClasspathName,
			ArrayList currentRuleSpecs, String customEncoding,
			String destPath, boolean isSourceOnly,
			boolean rejectDestinationPathOnJars) {

		int rulesSpecsSize = currentRuleSpecs.size();
		AccessRuleSet accessRuleSet = null;
		if (rulesSpecsSize != 0) {
			AccessRule[] accessRules = new AccessRule[currentRuleSpecs.size()];
			boolean rulesOK = true;
			Iterator i = currentRuleSpecs.iterator();
			int j = 0;
			while (i.hasNext()) {
				String ruleSpec = (String) i.next();
				char key = ruleSpec.charAt(0);
				String pattern = ruleSpec.substring(1);
				if (pattern.length() > 0) {
					switch (key) {
						case '+':
							accessRules[j++] = new AccessRule(pattern
									.toCharArray(), 0);
							break;
						case '~':
							accessRules[j++] = new AccessRule(pattern
									.toCharArray(),
									IProblem.DiscouragedReference);
							break;
						case '-':
							accessRules[j++] = new AccessRule(pattern
									.toCharArray(),
									IProblem.ForbiddenReference);
							break;
						case '?':
							accessRules[j++] = new AccessRule(pattern
									.toCharArray(),
									IProblem.ForbiddenReference, true/*keep looking for accessible type*/);
							break;
						default:
							rulesOK = false;
					}
				} else {
					rulesOK = false;
				}
			}
			if (rulesOK) {
	    		accessRuleSet = new AccessRuleSet(accessRules, AccessRestriction.COMMAND_LINE, currentClasspathName);
			} else {
				return;
			}
		}
		if (Main.NONE.equals(destPath)) {
			destPath = Main.NONE; // keep == comparison valid
		}
		if (rejectDestinationPathOnJars && destPath != null &&
				(currentClasspathName.endsWith(".jar") || //$NON-NLS-1$
					currentClasspathName.endsWith(".zip"))) { //$NON-NLS-1$
			throw new IllegalArgumentException(
					this.bind("configure.unexpectedDestinationPathEntryFile", //$NON-NLS-1$
								currentClasspathName));
			}
		FileSystem.Classpath currentClasspath = FileSystem.getClasspath(
				currentClasspathName,
				customEncoding,
				isSourceOnly,
				accessRuleSet,
				destPath, 
				null,
				this.releaseVersion);
		if (currentClasspath != null) {
			paths.add(currentClasspath);
		}
	}
	/*
	 * Lookup the message with the given ID in this catalog and bind its
	 * substitution locations with the given string.
	 */
	private String bind(String id, String binding) {
		return bind(id, new String[] { binding });
	}

	/*
	 * Lookup the message with the given ID in this catalog and bind its
	 * substitution locations with the given string values.
	 */
	private String bind(String id, String[] arguments) {
		if (id == null)
			return "No message available"; //$NON-NLS-1$
		String message = null;
		try {
			message = this.bundle.getString(id);
		} catch (MissingResourceException e) {
			// If we got an exception looking for the message, fail gracefully by just returning
			// the id we were looking for.  In most cases this is semi-informative so is not too bad.
			return "Missing message: " + id + " in: " + Main.bundleName; //$NON-NLS-2$ //$NON-NLS-1$
		}
		return MessageFormat.format(message, (Object[]) arguments);
	}
	private Iterable<? extends File> getFiles(final Iterable<? extends Path> paths) {
		if (paths == null)
			return null;
		return () -> new Iterator<File>() {
			Iterator<? extends Path> original = paths.iterator();
			@Override
			public boolean hasNext() {
				return this.original.hasNext();
			}
			@Override
			public File next() {
				return this.original.next().toFile();
			}
		};
	}
	private Iterable<? extends Path> getPaths(final Iterable<? extends File> files) {
		if (files == null)
			return null;
		return () -> new Iterator<Path>() {
			Iterator<? extends File> original = files.iterator();
			@Override
			public boolean hasNext() {
				return this.original.hasNext();
			}
			@Override
			public Path next() {
				return this.original.next().toPath();
			}
		};
	}

	private void validateFileObject(FileObject file) {
		// FIXME: fill-up
	}
	private void validateModuleLocation(Location location, String modName) {
		Objects.requireNonNull(location);
		if (modName == null) {
			throw new IllegalArgumentException("module must not be null"); //$NON-NLS-1$
		}
		if (this.isOnJvm9) {
			if (!location.isModuleOrientedLocation() && !location.isOutputLocation()) {
				throw new IllegalArgumentException("location is module related :" + location.getName()); //$NON-NLS-1$
			}
		}
	}
	private void validateNonModuleLocation(Location location) {
		Objects.requireNonNull(location);
		if (this.isOnJvm9) {
			if (location.isModuleOrientedLocation() && location.isOutputLocation()) {
				throw new IllegalArgumentException("location is module related :" + location.getName()); //$NON-NLS-1$
			}
		}
	}
	private void validateOutputLocation(Location location) {
		Objects.requireNonNull(location);
		if (!location.isOutputLocation()) {
			throw new IllegalArgumentException("location is not output location :" + location.getName()); //$NON-NLS-1$
		}
	}
	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjects(Path... paths) {
		return getJavaFileObjectsFromPaths(Arrays.asList(paths));
	}

	@Override
	public Iterable<? extends JavaFileObject> getJavaFileObjectsFromPaths(Iterable<? extends Path> paths) {
		return getJavaFileObjectsFromFiles(getFiles(paths));
	}

	@Override
	public Iterable<? extends Path> getLocationAsPaths(Location location) {
		if (location instanceof LocationWrapper) {
			return ((LocationWrapper) location).paths;
		}
		LocationWrapper loc = this.locationHandler.getLocation(location);
		if (loc == null) {
			return null;
		}
		return loc.getPaths();
	}

	@Override
	public void setLocationFromPaths(Location location, Collection<? extends Path> paths) throws IOException {
		setLocation(location, getFiles(paths));
		if (location == StandardLocation.MODULE_PATH) { 
			// FIXME: same for module source path?
			Map<String, String> options = new HashMap<>();
			// FIXME: Find a way to get the options from the EclipseCompiler and pass it to the parser.
			// FIXME: need to be the latest and not hardcoded value
			options.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_9);
			options.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_9);
			options.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_9);
			CompilerOptions compilerOptions = new CompilerOptions(options);
			ProblemReporter problemReporter = 
					new ProblemReporter(
						DefaultErrorHandlingPolicies.proceedWithAllProblems(),
						compilerOptions,
						new DefaultProblemFactory());
			for (Path path : paths) {
				List<Classpath> mp = ModuleFinder.findModules(path.toFile(), null, 
						new Parser(problemReporter, true), null, true, this.releaseVersion);
				for (Classpath cp : mp) {
					Collection<String> moduleNames = cp.getModuleNames(null);
					for (String string : moduleNames) {
						Path p = Paths.get(cp.getPath());
						setLocationForModule(StandardLocation.MODULE_PATH, string,  Collections.singletonList(p));
					}
				}
			}
		}
	}

	@Override
	public boolean contains(Location location, FileObject fo) throws IOException {
		validateFileObject(fo);
		Iterable<? extends File> files = getLocation(location);
		if (files == null) {
			throw new IllegalArgumentException("Unknown location : " + location);//$NON-NLS-1$
		}
		for (File file : files) {
			if (file.isDirectory()) {
				if (fo instanceof EclipseFileObject) {
					Path filepath = ((EclipseFileObject) fo).f.toPath();
					if (filepath.startsWith(Paths.get(file.toURI()).toAbsolutePath())) {
						return true;
					}
				}
			} else if (isArchive(file)) {
				if (fo instanceof ArchiveFileObject) {
					Archive archive = getArchive(file);
					if (archive != Archive.UNKNOWN_ARCHIVE) {
						if (archive.contains(((ArchiveFileObject) fo ).entryName)) {
							return true;
						}
					}
				}
			}
		}
		return false;
	}

	@Override
	public Location getLocationForModule(Location location, String moduleName) throws IOException {
		validateModuleLocation(location, moduleName);
		Location result = this.locationHandler.getLocation(location, moduleName);
		if (result == null && location == StandardLocation.CLASS_OUTPUT) {
			LocationWrapper wrapper = this.locationHandler.getLocation(StandardLocation.MODULE_SOURCE_PATH, moduleName);
			deriveOutputLocationForModules(moduleName, wrapper.paths);
			result = getLocationForModule(location, moduleName);
		} else if (result == null && location == StandardLocation.SOURCE_OUTPUT) {
			LocationWrapper wrapper = this.locationHandler.getLocation(StandardLocation.MODULE_SOURCE_PATH, moduleName);
			deriveSourceOutputLocationForModules(moduleName, wrapper.paths);
			result = getLocationForModule(location, moduleName);
		}
		return result;
	}

	@Override
	public Location getLocationForModule(Location location, JavaFileObject fo) {
		validateModuleLocation(location, ""); //$NON-NLS-1$
		Path path = null;
		if (fo instanceof ArchiveFileObject) {
			path = ((ArchiveFileObject) fo).file.toPath();
			return this.locationHandler.getLocation(location, path);
		} else if (fo instanceof EclipseFileObject) {
			path = ((EclipseFileObject) fo).f.toPath();
			try {
				path = path.toRealPath();
			} catch (IOException e) {
				e.printStackTrace();
			}
			LocationContainer container = this.locationHandler.getLocation(location);
			while (path != null) {
				Location loc = container.get(path);
				if (loc != null)
					return loc;
				path = path.getParent();
			}
		}
		return null;
	}

	@Override
	public <S> ServiceLoader<S> getServiceLoader(Location location, Class<S> service) throws IOException {
		// FIXME: Need special handling in case of module class loaders.
		return ServiceLoader.load(service, getClassLoader(location));
	}

	@Override
	public String inferModuleName(Location location) throws IOException {
		if (location instanceof ModuleLocationWrapper) {
			ModuleLocationWrapper wrapper = (ModuleLocationWrapper) location;
			return wrapper.modName;
		}
		return null;
	}

	@Override
	public Iterable<Set<Location>> listLocationsForModules(Location location) {
		validateModuleLocation(location, ""); //$NON-NLS-1$
		return this.locationHandler.listLocationsForModules(location);
	}

	@Override
	public Path asPath(FileObject file) {
		validateFileObject(file);
		EclipseFileObject eclFile = (EclipseFileObject) file;
		if (eclFile.f != null) {
			return eclFile.f.toPath();
		}
		return null;
	}
	private void deriveOutputLocationForModules(String moduleName, Collection<? extends Path> paths) {
		LocationWrapper wrapper = this.locationHandler.getLocation(StandardLocation.CLASS_OUTPUT, moduleName);
		if (wrapper == null) {
			// First get from our internally known location for legacy/unnamed location
			wrapper = this.locationHandler.getLocation(StandardLocation.CLASS_OUTPUT, ""); //$NON-NLS-1$
			if (wrapper == null) {
				wrapper = this.locationHandler.getLocation(StandardLocation.CLASS_OUTPUT);
			}
			if (wrapper != null) {
				Iterator<? extends Path> iterator = wrapper.paths.iterator();
				if (iterator.hasNext()) {
					try {
					// Per module output location is always a singleton list
					Path path = iterator.next().resolve(moduleName);
					this.locationHandler.setLocation(StandardLocation.CLASS_OUTPUT, moduleName, Collections.singletonList(path));
					} catch(Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	private void deriveSourceOutputLocationForModules(String moduleName, Collection<? extends Path> paths) {
		LocationWrapper wrapper = this.locationHandler.getLocation(StandardLocation.SOURCE_OUTPUT, moduleName);
		if (wrapper == null) {
			// First get from our internally known location for legacy/unnamed location
			wrapper = this.locationHandler.getLocation(StandardLocation.SOURCE_OUTPUT, ""); //$NON-NLS-1$
			if (wrapper == null) {
				wrapper = this.locationHandler.getLocation(StandardLocation.SOURCE_OUTPUT);
			}
			if (wrapper != null) {
				Iterator<? extends Path> iterator = wrapper.paths.iterator();
				if (iterator.hasNext()) {
					try {
					// Per module output location is always a singleton list
					Path path = iterator.next().resolve(moduleName);
					this.locationHandler.setLocation(StandardLocation.SOURCE_OUTPUT, moduleName, Collections.singletonList(path));
					} catch(Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	@Override
	public void setLocationForModule(Location location, String moduleName, Collection<? extends Path> paths) throws IOException {
		validateModuleLocation(location, moduleName);
		this.locationHandler.setLocation(location, moduleName, paths);
		if (location == StandardLocation.MODULE_SOURCE_PATH) {
			LocationWrapper wrapper = this.locationHandler.getLocation(StandardLocation.CLASS_OUTPUT, moduleName);
			if (wrapper == null) {
				wrapper = this.locationHandler.getLocation(StandardLocation.CLASS_OUTPUT, ""); //$NON-NLS-1$
				if (wrapper != null) {
					Iterator<? extends Path> iterator = wrapper.paths.iterator();
					if (iterator.hasNext()) {
						// Per module output location is always a singleton list
						Path path = iterator.next().resolve(moduleName);
						this.locationHandler.setLocation(StandardLocation.CLASS_OUTPUT, moduleName, Collections.singletonList(path));
					}
				}
			}
		}
	}
}
