Synchronize Maven: resolve using batches

use
 RepositorySystem.resolveArtifacts() per plugin instead of
 RepositorySystem.resolveArtifact() per file

opens less TCP connections and is reproducible faster.
like ~ 8min -> 3min on my setup

Also use eclipse resource API instead of java file api.

Change-Id: Idf0f1f0265cc775890eaf797a1a2fefdb8db730f
diff --git a/org.eclipse.tea.library.build/src/org/eclipse/tea/library/build/tasks/maven/SynchronizeMavenArtifact.java b/org.eclipse.tea.library.build/src/org/eclipse/tea/library/build/tasks/maven/SynchronizeMavenArtifact.java
index c4a1f96..bb61c45 100644
--- a/org.eclipse.tea.library.build/src/org/eclipse/tea/library/build/tasks/maven/SynchronizeMavenArtifact.java
+++ b/org.eclipse.tea.library.build/src/org/eclipse/tea/library/build/tasks/maven/SynchronizeMavenArtifact.java
@@ -10,20 +10,22 @@
  *******************************************************************************/
 package org.eclipse.tea.library.build.tasks.maven;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
 import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 
 import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
 import org.apache.maven.wagon.ConnectionException;
@@ -42,6 +44,7 @@
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.repository.RepositoryPolicy;
 import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
 import org.eclipse.aether.resolution.ArtifactResult;
 import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.transport.TransporterFactory;
@@ -49,6 +52,7 @@
 import org.eclipse.aether.transport.wagon.WagonProvider;
 import org.eclipse.aether.transport.wagon.WagonTransporterFactory;
 import org.eclipse.core.internal.variables.StringVariableManager;
+import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IFolder;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResource;
@@ -56,7 +60,6 @@
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.NullProgressMonitor;
-import org.eclipse.core.runtime.Status;
 import org.eclipse.e4.core.di.annotations.Execute;
 import org.eclipse.jdt.apt.core.util.AptConfig;
 import org.eclipse.jdt.core.IClasspathEntry;
@@ -213,7 +216,7 @@
 			System.runFinalization();
 
 			for (PluginBuild pb : classpathManipulatorOfPlugin.keySet()) {
-				runSingle(log, tracker, pb, system, session, remotes);
+				synchronizePlugin(log, tracker, pb, system, session, remotes);
 			}
 		} catch (Exception e) {
 			log.error("error synchronizing maven artifacts", e);
@@ -251,23 +254,28 @@
 		return new MavenConfig(file);
 	}
 
-	private void runSingle(TaskingLog log, TaskProgressTracker tracker, PluginBuild hostPlugin, RepositorySystem system,
-			RepositorySystemSession session, List<RemoteRepository> remotes) throws CoreException {
+	private void synchronizePlugin(TaskingLog log, TaskProgressTracker tracker, PluginBuild hostPlugin,
+			RepositorySystem system, RepositorySystemSession session, List<RemoteRepository> remotes)
+			throws CoreException {
+		NullProgressMonitor monitor = new NullProgressMonitor();
 		IProject prj = hostPlugin.getData().getProject();
-		IFolder folder = prj.getFolder(MAVEN_DIRNAME);
-		if (!folder.exists()) {
-			folder.create(false, true, null);
-			log.warn("creating " + folder.getName() + "; make sure to add to the classpath of "
+		IFolder targetFolder = prj.getFolder(MAVEN_DIRNAME);
+		if (!targetFolder.exists()) {
+			targetFolder.create(false, true, monitor);
+			log.warn("creating " + targetFolder.getName() + "; make sure to add to the classpath of "
 					+ hostPlugin.getPluginName());
+			// write .gitignore
+			IFile gitignore = targetFolder.getFile(".gitignore");
+			gitignore.create(new ByteArrayInputStream("*.jar".getBytes(Charsets.UTF_8)), false, null);
 		}
-		File target = folder.getRawLocation().toFile();
 
-		Set<File> valid = new TreeSet<>(Comparator.comparing(File::getName));
+		List<ArtifactRequest> artifactRequests = new ArrayList<>();
+		log.info("synchronize maven artifacts for '" + hostPlugin.getPluginName() + "': "
+				+ hostPlugin.getMavenExternalJarDependencies().stream().map(artifact -> artifact.getCoordinates())
+						.collect(Collectors.joining(", ")));
 		for (MavenExternalJarBuild artifact : hostPlugin.getMavenExternalJarDependencies()) {
 			tracker.setTaskName(artifact.getCoordinates());
 			tracker.worked(1);
-			log.info("synchronize nexus coordinate " + artifact.getCoordinates() + " into "
-					+ hostPlugin.getPluginName());
 
 			Coordinate coord = new Coordinate(artifact.getCoordinates());
 
@@ -289,166 +297,132 @@
 			{
 				ArtifactRequest remoterq = new ArtifactRequest().setArtifact(mvn)
 						.setRepositories(remote ? remotes : null);
-				resolveArtifact(log, target, system, session, remoterq, valid);
+				artifactRequests.add(remoterq);
 			}
 
 			// resolve source bundle.
-			try {
-				Artifact srcmvn = new DefaultArtifact(coord.group, coord.artifact, "sources", coord.extension,
-						coord.version);
-				ArtifactRequest srcrq = new ArtifactRequest().setArtifact(srcmvn)
-						.setRepositories(remote ? remotes : null);
+			Artifact srcmvn = new DefaultArtifact(coord.group, coord.artifact, "sources", coord.extension,
+					coord.version);
+			ArtifactRequest srcrq = new ArtifactRequest().setArtifact(srcmvn).setRepositories(remote ? remotes : null);
+			artifactRequests.add(srcrq);
+		}
 
-				resolveArtifact(log, target, system, session, srcrq, valid);
-			} catch (Exception e) {
-				log.warn("No sources available for " + artifact.getCoordinates());
+		List<ArtifactResult> results = resolveArtifacts(log, system, session, artifactRequests);
+		Set<IFile> resolvedFiles = new HashSet<>();
+		for (ArtifactResult result : results) {
+			ArtifactRequest rq = result.getRequest();
+			Artifact mvn = rq.getArtifact();
+			String c = mvn.getClassifier();
+			if (result.isMissing() || !result.isResolved() || !result.getExceptions().isEmpty()) {
+				if ("sources".equals(c)) {
+					log.warn("No sources available for " + mvn.getGroupId() + ":" + mvn.getArtifactId() + ":"
+							+ mvn.getVersion());
+				} else {
+					String classifier = c == null || c.isEmpty() ? "" : (":" + c);
+					log.error("cannot resolve " + mvn.getGroupId() + ":" + mvn.getArtifactId() + classifier + ":"
+							+ mvn.getVersion());
+				}
+			} else {
+				// copy file to maven directory
+				File resolvedFile = result.getArtifact().getFile();
+				IFile targetFile = targetFolder.getFile(resolvedFile.getName());
+				resolvedFiles.add(targetFile);
+
+				Artifact artifact = result.getArtifact();
+				if (needUpdateFileInProjectsMavenFolder(log, targetFile, artifact)) {
+					updateFileInProjectsMavenFolder(log, targetFile, artifact);
+				}
 			}
 		}
 
 		// cleanup old files
-		for (File file : target.listFiles()) {
+		for (IResource file : targetFolder.members()) {
 			if (file.getName().equals(".gitignore")) {
 				continue;
 			}
 
-			boolean isValid = false;
-			for (File validFile : valid) {
-				if (file.equals(validFile)) {
-					isValid = true;
-				}
-			}
-
-			if (!isValid) {
+			if (!resolvedFiles.contains(file)) {
 				log.info("removing old maven artifact: " + file);
-				FileUtils.delete(file);
+				file.delete(true, null);
 			}
 		}
-
-		// write .gitignore
-		File gitignore = new File(target, ".gitignore");
-		if (!gitignore.exists()) {
-			try {
-				FileUtils.writeFileFromString(gitignore, Charsets.UTF_8, "*.jar");
-			} catch (IOException e) {
-				throw new CoreException(Status.error(e.getMessage(), e));
-			}
-		}
-
-		folder.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
+		return;
 	}
 
 	/**
-	 * Resolves an {@link ArtifactRequest}. Resolving means that it looks up the
-	 * bundle on the local repository and all servers. After successful
+	 * Resolves multiple {@link ArtifactRequest}. Resolving means that it looks
+	 * up the bundle on the local repository and all servers. After successful
 	 * resolution, the bundles is located in the local repository. After that,
 	 * this method copies the according file to the target location.
 	 *
 	 * @param controller
 	 *            used for logging
-	 * @param target
-	 *            the target directory to put the bundle into
 	 * @param system
 	 *            the {@link RepositorySystem} providing the resolution
 	 *            algorithm
 	 * @param session
 	 *            the {@link RepositorySystemSession} to use
-	 * @param rq
-	 *            the {@link ArtifactRequest} that defines what to resolve
-	 * @param resolved
-	 *            all resolved artifacts will be added to this list, even if no
-	 *            file has been changed on disk.
+	 * @param requests
+	 *            A Collection of {@link ArtifactRequest} that defines what to
+	 *            resolve
+	 * @returns list of {@link ArtifactResult}
 	 */
-	private void resolveArtifact(TaskingLog log, File target, RepositorySystem system, RepositorySystemSession session,
-			ArtifactRequest rq, Set<File> resolved) {
-		Artifact mvn = rq.getArtifact();
-
+	private List<ArtifactResult> resolveArtifacts(TaskingLog log, RepositorySystem system,
+			RepositorySystemSession session, Collection<ArtifactRequest> requests) {
+		List<ArtifactResult> results;
 		try {
-			ArtifactResult result = system.resolveArtifact(session, rq);
-			if (result.isMissing() || !result.isResolved()) {
-				log.warn("cannot resolve " + mvn.getGroupId() + ":" + mvn.getArtifactId() + ":" + mvn.getVersion() + ":"
-						+ mvn.getClassifier());
-				if (!result.getExceptions().isEmpty()) {
-					for (Exception e : result.getExceptions()) {
-						e.printStackTrace(log.error());
-					}
-				}
-				return;
-			}
-			if (properties.isVerboseMavenOutput()) {
-				if (!result.getExceptions().isEmpty()) {
-					for (Exception e : result.getExceptions()) {
-						e.printStackTrace(log.debug());
-					}
-				}
-			}
-			// download file to maven directory
-			File file = result.getArtifact().getFile();
-			addFileToProjectsMavenFolder(log, target, resolved, result, file);
-		} catch (Exception e) {
-			String c = mvn.getClassifier();
-			if (!"sources".equals(c)) {
-				String classifier = c == null || c.isEmpty() ? "" : (":" + c);
-				log.error("cannot resolve " + mvn.getGroupId() + ":" + mvn.getArtifactId() + classifier + ":"
-						+ mvn.getVersion());
-			}
-			throw new RuntimeException("failed to synchronize " + rq.getArtifact().getArtifactId(), e);
+			results = system.resolveArtifacts(session, requests);
+		} catch (ArtifactResolutionException e) {
+			results = e.getResults();
 		}
+		return results;
 	}
 
-	private static void addFileToProjectsMavenFolder(TaskingLog log, File target, Set<File> resolved,
-			ArtifactResult result, File file) throws IOException {
-		File targetFile = new File(target, file.getName());
-		resolved.add(targetFile);
+	private static boolean needUpdateFileInProjectsMavenFolder(TaskingLog log, IFile targetResource,
+			Artifact artifact) {
+		File targetFile = targetResource.getRawLocation().toFile();
+		File resolvedFile = artifact.getFile();
+		// no need to update if files are equal:
+		return !FileUtils.equals(targetFile, resolvedFile);
+	}
 
-		if (targetFile.exists() && !result.getArtifact().isSnapshot()) {
-			// it is a released file. try to update but don't fail if (for
-			// example) windows locks
-			// the file...
+	private static void updateFileInProjectsMavenFolder(TaskingLog log, IFile targetResource, Artifact artifact)
+			throws CoreException {
+		File targetFile = targetResource.getRawLocation().toFile();
+		File resolvedFile = artifact.getFile();
+		// update = delete existing + copy or link new file:
+		if (targetFile.exists()) {
 			if (!targetFile.delete()) {
-				if (targetFile.length() != file.length()) {
-					// would need update but can't
-					log.error("cannot update " + targetFile + " to new version, please make sure file is not locked");
-				}
-
-				return; // don't update
+				log.error("cannot update " + targetFile + ". Please make sure file is not locked");
+				return;
 			}
 		}
 
-		boolean copyNeeded = true;
-		// refresh of file/link:
 		try {
-			FileUtils.delete(targetFile);
-		} catch (java.lang.IllegalStateException e) { // cannot delete
-			if (FileUtils.equals(targetFile, file)) {
-				// ignore
-				log.info("Could not update file. Probably in use. Can be ignored since contents is the same: "
-						+ targetFile);
-				copyNeeded = false;
-			} else {
-				throw e;
-			}
-		}
-
-		if (copyNeeded) {
-			try {
-				targetFile = java.nio.file.Files.createSymbolicLink(targetFile.toPath(), file.toPath()).toFile();
-			} catch (IOException e) {
-				String exName = e.getClass().getName();
-				// don't spam missing rights for symlink creation
-				if (!Objects.equals(exName, lastExceptionName)) {
-					lastExceptionName = exName;
-					// Windows 10:
-					// "$file: Dem Client fehlt ein erforderliches Recht.\r\n"
-					String msg = e.getMessage();
-					if (msg != null) {
-						msg = msg.replace("\n", "\\n");
-						msg = msg.replace("\r", "\\r");
-					}
-					log.warn("cannot create symlink for: " + targetFile + " (" + exName + " " + msg + ")");
+			java.nio.file.Files.createSymbolicLink(targetFile.toPath(), resolvedFile.toPath());
+		} catch (IOException e) {
+			String exName = e.getClass().getName();
+			// don't spam missing rights for symlink creation
+			if (!Objects.equals(exName, lastExceptionName)) {
+				lastExceptionName = exName;
+				// Windows 10:
+				// "$file: Dem Client fehlt ein erforderliches Recht.\r\n"
+				String msg = e.getMessage();
+				if (msg != null) {
+					msg = msg.replace("\n", "\\n");
+					msg = msg.replace("\r", "\\r");
 				}
-				FileUtils.copyFileToDirectory(file, target);
+				log.warn("cannot create symlink for: " + targetFile + " (" + exName + " " + msg + ")");
+			}
+			try {
+				FileUtils.copyFile(resolvedFile, targetFile);
+			} catch (IOException copyException) {
+				log.error("Could not copy file.", copyException);
+				return;
 			}
 		}
+		targetResource.refreshLocal(IResource.DEPTH_ZERO, null);
+		return;
 	}
 
 	private List<RemoteRepository> createRemoteRepositories() {