/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.builder.ant; import org.eclipse.che.api.builder.BuilderException; import org.eclipse.che.api.builder.dto.BuilderEnvironment; import org.eclipse.che.api.builder.dto.Dependency; import org.eclipse.che.api.builder.internal.BuildListener; import org.eclipse.che.api.builder.internal.BuildResult; import org.eclipse.che.api.builder.internal.BuildTask; import org.eclipse.che.api.builder.internal.Builder; import org.eclipse.che.api.builder.internal.BuilderConfiguration; import org.eclipse.che.api.builder.internal.BuilderTaskType; import org.eclipse.che.api.builder.internal.Constants; import org.eclipse.che.api.builder.internal.DependencyCollector; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.util.CommandLine; import org.eclipse.che.api.core.util.CustomPortService; import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.ide.ant.tools.AntBuildListener; import org.eclipse.che.ide.ant.tools.AntMessage; import org.eclipse.che.ide.ant.tools.AntUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.io.EOFException; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Builder based on Ant. * * @author andrew00x */ @Singleton public class AntBuilder extends Builder { private static final Logger LOG = LoggerFactory.getLogger(AntBuilder.class); private static final String DEPENDENCIES_JSON_FILE = "dependencies.json"; private static final String DEPENDENCIES_ZIP_FILE = "dependencies.zip"; private static final String BUILD_LISTENER_CLASS; private static final String BUILD_LISTENER_CLASS_PORT; private static final String BUILD_LISTENER_CLASS_PATH; private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final String CLASSPATH_SEPARATOR = System.getProperty("path.separator"); private static final String DEFAULT_JAR_WITH_DEPENDENCIES_NAME = "jar-with-dependencies.zip"; private static interface AntEventFilter { boolean accept(AntEvent event); } private static final AntEventFilter DEFAULT_ANT_EVENT_FILTER = new AntEventFilter() { @Override public boolean accept(AntEvent event) { return event.isStart() || event.isSuccessful() || event.isError() || event.isClasspath() || event.isPack(); } }; static { final Class<AntBuildListener> myBuildListenerClass = AntBuildListener.class; BUILD_LISTENER_CLASS = myBuildListenerClass.getName(); try { BUILD_LISTENER_CLASS_PATH = new java.io.File(myBuildListenerClass.getProtectionDomain().getCodeSource().getLocation().toURI()).getAbsolutePath(); } catch (URISyntaxException e) { // Not expected to be thrown throw new IllegalStateException(e); } BUILD_LISTENER_CLASS_PORT = "-D" + BUILD_LISTENER_CLASS + ".port"; } private final Map<Long, AntMessageServer> antMessageServers; private final CustomPortService portService; private final Map<String, String> antProperties; @Inject public AntBuilder(@Named(Constants.BASE_DIRECTORY) java.io.File rootDirectory, @Named(Constants.NUMBER_OF_WORKERS) int numberOfWorkers, @Named(Constants.QUEUE_SIZE) int queueSize, @Named(Constants.KEEP_RESULT_TIME) int cleanupTime, CustomPortService portService, EventService eventService) { super(rootDirectory, numberOfWorkers, queueSize, cleanupTime, eventService); this.portService = portService; antMessageServers = new ConcurrentHashMap<>(); Map<String, String> myAntProperties = null; try { myAntProperties = AntUtils.getAntEnvironmentInformation(); } catch (IOException e) { LOG.error(e.getMessage(), e); } if (myAntProperties == null) { antProperties = Collections.emptyMap(); } else { antProperties = Collections.unmodifiableMap(myAntProperties); } } @Override public String getName() { return "ant"; } @Override public String getDescription() { return "Apache Ant based builder implementation"; } @Override public Map<String, BuilderEnvironment> getEnvironments() { final Map<String, BuilderEnvironment> envs = new HashMap<>(4); final Map<String, String> properties = new HashMap<>(antProperties); properties.remove("Ant home"); properties.remove("Java home"); final BuilderEnvironment def = DtoFactory.getInstance().createDto(BuilderEnvironment.class) .withId("default") .withIsDefault(true) .withDisplayName(properties.get("Ant version")) .withProperties(properties); envs.put(def.getId(), def); return envs; } @Override protected CommandLine createCommandLine(BuilderConfiguration config) { final CommandLine commandLine = new CommandLine(AntUtils.getAntExecCommand()); commandLine.add(config.getTargets()); switch (config.getTaskType()) { case LIST_DEPS: case COPY_DEPS: commandLine.add("-keep-going"); // Not care about compilation errors, etc. We need classpath only. break; } commandLine.add("-listener", BUILD_LISTENER_CLASS, "-lib", BUILD_LISTENER_CLASS_PATH); commandLine.add(config.getOptions()); return commandLine; } @Override protected BuildResult getTaskResult(FutureBuildTask task, boolean successful) throws BuilderException { if (!successful) { return new BuildResult(false); } final AntMessageServer server = antMessageServers.get(task.getId()); if (server != null) { try { server.await(5000); // Give some time to the server to receive last messages from the AntBuildListener } catch (InterruptedException e) { // not expected to be thrown LOG.warn(e.getMessage(), e); } finally { server.stop = true; // force stop if server is not stopped yet } boolean antSuccessful = false; for (AntEvent event : server.receiver.events) { if (event.isSuccessful()) { antSuccessful = true; break; } } final BuilderConfiguration config = task.getConfiguration(); final java.io.File workDir = config.getWorkDir(); if (config.getTaskType() == BuilderTaskType.DEFAULT) { // Need successful status to continue. if (!antSuccessful) { return new BuildResult(false); } final BuildResult result = new BuildResult(true); boolean isJar = false; for (AntEvent event : server.receiver.events) { if (event.isPack()) { final java.io.File file = event.getPack(); if (file.exists()) { isJar = "jar".equals(event.getPacking()); result.getResults().add(file); } } } final FileFilter filter = new FileFilter() { final FileFilter system = AntUtils.newSystemFileFilter(); @Override public boolean accept(java.io.File pathname) { return system.accept(pathname) && !BUILD_LISTENER_CLASS_PATH.equals(pathname.getAbsolutePath()); } }; if (isJar && config.getRequest().isIncludeDependencies()) { result.getResults().clear(); // Get all needed dependencies from classpath. Do that only for jar files. We need to have single jar that contains all // dependencies of project otherwise we won't be able to run application on runner side. final java.io.File withDependencies = new java.io.File(workDir, DEFAULT_JAR_WITH_DEPENDENCIES_NAME); try { writeJarWithDependencies(withDependencies, server.receiver.events, filter); } catch (IOException e) { throw new BuilderException(e); } result.getResults().add(withDependencies); } return result; } else if (config.getTaskType() == BuilderTaskType.LIST_DEPS || config.getTaskType() == BuilderTaskType.COPY_DEPS) { // Status may be unsuccessful we are not care about it, we just need to get classpath. final BuildResult result = new BuildResult(true); final Set<java.io.File> classpath = new LinkedHashSet<>(); final FileFilter filter = new FileFilter() { final FileFilter system = AntUtils.newSystemFileFilter(); @Override public boolean accept(java.io.File pathname) { return system.accept(pathname) && !BUILD_LISTENER_CLASS_PATH.equals(pathname.getAbsolutePath()); } }; for (AntEvent event : server.receiver.events) { if (event.isClasspath()) { for (java.io.File item : event.getClasspath()) { if (item.exists() && filter.accept(item)) { classpath.add(item); } } } } if (config.getTaskType() == BuilderTaskType.LIST_DEPS) { try { final java.io.File file = new java.io.File(workDir, DEPENDENCIES_JSON_FILE); writeDependenciesJson(classpath, workDir, file); result.getResults().add(file); } catch (IOException e) { throw new BuilderException(e); } } else { try { final java.io.File file = new java.io.File(workDir, DEPENDENCIES_ZIP_FILE); writeDependenciesZip(classpath, file); result.getResults().add(file); } catch (IOException e) { throw new BuilderException(e); } } return result; } antMessageServers.remove(task.getId()); } throw new BuilderException("Failed to get build result."); } @PostConstruct @Override public void start() { super.start(); addBuildListener(new AntMessageServerStarter()); } @Override protected void cleanup(BuildTask task) { super.cleanup(task); // If nobody asked about build results AntMessageServer may be still in the Map. final AntMessageServer server = antMessageServers.remove(task.getId()); if (server != null) { portService.release(server.port); } } private int getPort() { final int port = portService.acquire(); if (port < 0) { throw new IllegalStateException("Cannot start build process, there are no free ports. "); } return port; } private void writeJarWithDependencies(java.io.File withDependencies, List<AntEvent> events, java.io.FileFilter filter) throws IOException { final UniqueNameChecker uniqueNameChecker = new UniqueNameChecker(); FileOutputStream fOut = null; ZipOutputStream zipOut = null; try { fOut = new FileOutputStream(withDependencies); zipOut = new ZipOutputStream(fOut); for (AntEvent event : events) { if (event.isPack()) { zipOut.putNextEntry(new ZipEntry("application.jar")); Files.copy(event.getPack().toPath(), zipOut); zipOut.closeEntry(); } if (event.isClasspath()) { for (java.io.File item : event.getClasspath()) { if (!item.isFile()) { continue; } if (item.exists() && filter.accept(item)) { zipOut.putNextEntry(new ZipEntry("lib/" + uniqueNameChecker.maybeAddIndex(item.getName()))); Files.copy(item.toPath(), zipOut); zipOut.closeEntry(); } } } } } finally { if (zipOut != null) { zipOut.close(); } if (fOut != null) { fOut.close(); } } } private void writeDependenciesJson(Set<java.io.File> classpath, java.io.File workDir, java.io.File jsonFile) throws IOException { final Path workDirPath = workDir.toPath(); final DependencyCollector collector = new DependencyCollector(); final UniqueNameChecker uniqueNameChecker = new UniqueNameChecker(); for (java.io.File file : classpath) { final Path path = file.toPath(); if (path.startsWith(workDirPath)) { // If library included in project show relative path to it. collector.addDependency( DtoFactory.getInstance().createDto(Dependency.class).withFullName(workDirPath.relativize(path).toString())); } else { // otherwise show just name of library. // Typically it may means that dependency is obtained with some dependency manager, // e.g. with builder over builder-ant-task. collector.addDependency( DtoFactory.getInstance().createDto(Dependency.class).withFullName(uniqueNameChecker.maybeAddIndex(file.getName()))); } } collector.writeJson(jsonFile); } private void writeDependenciesZip(Set<java.io.File> classpath, java.io.File zipFile) throws IOException { final UniqueNameChecker uniqueNameChecker = new UniqueNameChecker(); FileOutputStream fOut = null; ZipOutputStream zipOut = null; try { fOut = new FileOutputStream(zipFile); zipOut = new ZipOutputStream(fOut); for (java.io.File file : classpath) { if (!file.isFile()) { continue; // Skip directory with compiled sources of this project } zipOut.putNextEntry(new ZipEntry(uniqueNameChecker.maybeAddIndex(file.getName()))); Files.copy(file.toPath(), zipOut); zipOut.closeEntry(); } } finally { if (zipOut != null) { zipOut.close(); } if (fOut != null) { fOut.close(); } } } private class AntMessageServerStarter implements BuildListener { @Override public void begin(BuildTask task) { final int myPort = getPort(); antMessageServers.put(task.getId(), new AntMessageServer(myPort, new AntMessageReceiver(DEFAULT_ANT_EVENT_FILTER)).start()); task.getCommandLine().addPair(BUILD_LISTENER_CLASS_PORT, String.valueOf(myPort)); } @Override public void end(BuildTask task) { final AntMessageServer server = antMessageServers.get(task.getId()); if (server != null) { server.stop = true; // force stop if server is not stopped yet portService.release(server.port); } } } private static class UniqueNameChecker { final Map<String, Integer> indexes; UniqueNameChecker() { indexes = new HashMap<>(); } String maybeAddIndex(String str) { Integer index = indexes.get(str); if (index == null) { indexes.put(str, 1); return str; } indexes.put(str, index + 1); str = str + '(' + index + ')'; return str; } } private static class AntMessageReceiver { final List<AntEvent> events; final AntEventFilter filter; AntMessageReceiver(AntEventFilter filter) { this.filter = filter; events = new ArrayList<>(); } void receive(AntMessage message) { final AntEvent event = new AntEvent(message); if (filter.accept(event)) { events.add(event); } } } private static class AntEvent { final AntMessage message; java.io.File[] classpath; java.io.File pack; String packaging; AntEvent(AntMessage message) { this.message = message; final String antTask = message.getTask(); final String text = message.getText(); if (message.getType() == AntMessage.BUILD_LOG) { if ("javac".equals(antTask) && text != null && text.startsWith("Compilation argument")) { classpath = parseClasspath(text); } else if ("jar".equals(antTask) || "war".equals(antTask) || "ear".equals(antTask) || "zip".equals(antTask)) { packaging = antTask; // Ant send messages in format: Building jar|war|ear|zip: <absolute path to file>. Try to get this path. // Or Building MANIFEST-only jar: <absolute path to file>. if (text != null) { if (text.startsWith("Building MANIFEST-only jar: ")) { pack = new java.io.File(text.substring(28)); } else if (text.startsWith("Building jar: ") || text.startsWith("Building war: ") || text.startsWith("Building ear: ") || text.startsWith("Building zip: ")) { pack = new java.io.File(text.substring(14)); } } } } } boolean isClasspath() { return classpath != null; } boolean isPack() { return pack != null; } boolean isStart() { return message.getType() == AntMessage.BUILD_STARTED; } boolean isSuccessful() { return message.getType() == AntMessage.BUILD_SUCCESSFUL; } boolean isError() { return message.getType() == AntMessage.BUILD_ERROR; } // Get jar|war|ear|zip file if this event related to pack ant-task java.io.File getPack() { return pack; } String getPacking() { return packaging; } // Get classpath if this event related to compile ant-task java.io.File[] getClasspath() { return classpath; } String getText() { return message.getText(); } } private static java.io.File[] parseClasspath(String cmd) { final Scanner s1 = new Scanner(cmd); s1.useDelimiter(LINE_SEPARATOR); while (s1.hasNext()) { if ("'-classpath'".equals(s1.next())) { if (s1.hasNext()) { String str = s1.next(); if (!str.startsWith("-")) { List<java.io.File> classpath = new ArrayList<>(); final Scanner s2 = new Scanner(removeQuote(str)); s2.useDelimiter(CLASSPATH_SEPARATOR); while (s2.hasNext()) { String str2 = s2.next(); java.io.File file = new java.io.File(str2); if (file.exists()) { classpath.add(file); } } s2.close(); return classpath.toArray(new java.io.File[classpath.size()]); } } break; } } s1.close(); return new java.io.File[0]; } private static String removeQuote(String str) { if (str.charAt(0) == '\'') { str = str.substring(1); } final int len = str.length(); if (str.charAt(len - 1) == '\'') { str = str.substring(0, len - 1); } return str; } private class AntMessageServer { final AntMessageReceiver receiver; final int port; final CountDownLatch latch; volatile boolean stop; AntMessageServer(int port, AntMessageReceiver receiver) { this.port = port; this.receiver = receiver; latch = new CountDownLatch(1); } AntMessageServer start() { Thread thread = new Thread() { @Override public void run() { try { if (stop) { return; // If stop requested don't do anything } ServerSocket serverSocket = null; Socket mySocket = null; ObjectInputStream in = null; try { serverSocket = new ServerSocket(port); serverSocket.setSoTimeout( 30000); //This time is chosen empirically and necessary for some large projects. See IDEX-1957. mySocket = serverSocket.accept(); in = new ObjectInputStream(mySocket.getInputStream()); while (!stop) { AntMessage message; try { message = (AntMessage)in.readObject(); } catch (EOFException e) { message = null; } if (message == null) { stop = true; } else { receiver.receive(message); } } } catch (IOException | ClassNotFoundException e) { throw new IllegalStateException(e); } finally { if (in != null) { try { in.close(); } catch (IOException ignored) { } } if (mySocket != null) { try { mySocket.close(); } catch (IOException ignored) { } } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException ignored) { } } } } finally { latch.countDown(); } } }; thread.setDaemon(true); thread.start(); return this; } void await(long time) throws InterruptedException { latch.await(time, TimeUnit.MILLISECONDS); } } }