/******************************************************************************* * Copyright (c) 2012-2016 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.generator.archetype; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.util.CommandLine; import org.eclipse.che.api.core.util.LineConsumer; import org.eclipse.che.api.core.util.ProcessUtil; import org.eclipse.che.api.core.util.StreamPump; import org.eclipse.che.commons.lang.IoUtil; import org.eclipse.che.commons.lang.ZipUtils; import org.eclipse.che.generator.archetype.dto.MavenArchetype; import org.eclipse.che.ide.maven.tools.MavenUtils; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.validation.constraints.NotNull; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Singleton; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * Generates projects with maven-archetype-plugin. * * @author Artem Zatsarynnyi */ @Singleton public class ArchetypeGenerator { private static final Logger LOG = LoggerFactory.getLogger(ArchetypeGenerator.class); private static final AtomicLong taskIdSequence = new AtomicLong(1); private final ConcurrentMap<Long, GenerationTask> tasks; /** Time of keeping the results (generated projects and logs) of project generation. After this time the results may be removed. */ private final long keepResultTimeMillis; private ExecutorService executor; private ScheduledExecutorService scheduler; private Path archetypeGeneratorTempFolder; @Inject public ArchetypeGenerator() { this.keepResultTimeMillis = TimeUnit.SECONDS.toMillis(60); tasks = new ConcurrentHashMap<>(); } /** Initialize generator. */ @PostConstruct void start() throws IOException { archetypeGeneratorTempFolder = Files.createTempDirectory("archetype-generator"); executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("ArchetypeGenerator-[%d]").setDaemon(true).build()); scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("ArchetypeGeneratorSchedulerPool-%d") .setDaemon(true).build()); scheduler.scheduleAtFixedRate(new Runnable() { public void run() { int num = 0; for (Iterator<GenerationTask> i = tasks.values().iterator(); i.hasNext(); ) { if (Thread.currentThread().isInterrupted()) { return; } final GenerationTask task = i.next(); if (task.isExpired()) { i.remove(); try { cleanup(task); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } num++; } } if (num > 0) { LOG.debug("Remove {} expired tasks", num); } } }, 1, 1, TimeUnit.MINUTES); } /** Stops generator and releases any associated resources. */ @PreDestroy void stop() { boolean interrupted = false; scheduler.shutdownNow(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { LOG.warn("Unable to terminate scheduler"); } } catch (InterruptedException e) { interrupted = true; } executor.shutdown(); try { if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { LOG.warn("Unable to terminate main pool"); } } } catch (InterruptedException e) { interrupted |= true; executor.shutdownNow(); } if (IoUtil.deleteRecursive(archetypeGeneratorTempFolder.toFile())) { LOG.warn("Error occurs on removing " + archetypeGeneratorTempFolder.toString()); } tasks.clear(); if (interrupted) { Thread.currentThread().interrupt(); } } public GenerationTask getTaskById(Long id) throws ServerException { final GenerationTask task = tasks.get(id); if (task == null) { throw new ServerException(String.format("Invalid task id: %d", id)); } return task; } /** * Generates a new project from the specified archetype. * * @param archetype * archetype from which need to generate new project * @param groupId * groupId of new project * @param artifactId * artifactId of new project * @param version * version of new project * @return generating task * @throws ServerException * if an error occurs while generating project */ public GenerationTask generateFromArchetype(@NotNull MavenArchetype archetype, @NotNull String groupId, @NotNull String artifactId, @NotNull String version) throws ServerException { Map<String, String> archetypeProperties = new HashMap<>(); archetypeProperties.put("-DinteractiveMode", "false"); // get rid of the interactivity of the archetype plugin archetypeProperties.put("-DarchetypeGroupId", archetype.getGroupId()); archetypeProperties.put("-DarchetypeArtifactId", archetype.getArtifactId()); archetypeProperties.put("-DarchetypeVersion", archetype.getVersion()); archetypeProperties.put("-DgroupId", groupId); archetypeProperties.put("-DartifactId", artifactId); archetypeProperties.put("-Dversion", version); if (archetype.getRepository() != null) { archetypeProperties.put("-DarchetypeRepository", archetype.getRepository()); } if (archetype.getProperties() != null) { archetypeProperties.putAll(archetype.getProperties()); } final File workDir; try { workDir = Files.createTempDirectory(archetypeGeneratorTempFolder, "project-").toFile(); } catch (IOException e) { throw new ServerException(e); } final File logFile = new File(workDir, workDir.getName() + ".log"); final GeneratorLogger logger = createLogger(logFile); final CommandLine commandLine = createCommandLine(archetypeProperties); final Callable<Boolean> callable = createTaskFor(commandLine, logger, workDir); final Long internalId = taskIdSequence.getAndIncrement(); final GenerationTask task = new GenerationTask(callable, internalId, workDir, artifactId, logger); tasks.put(internalId, task); executor.execute(task); return task; } private GeneratorLogger createLogger(File logFile) throws ServerException { try { return new GeneratorLogger(logFile); } catch (IOException e) { throw new ServerException(e); } } private CommandLine createCommandLine(Map<String, String> archetypeProperties) throws ServerException { final CommandLine commandLine = new CommandLine(MavenUtils.getMavenExecCommand()); commandLine.add("--batch-mode"); commandLine.add("org.apache.maven.plugins:maven-archetype-plugin:RELEASE:generate"); commandLine.add(archetypeProperties); return commandLine; } private Callable<Boolean> createTaskFor(final CommandLine commandLine, final GeneratorLogger logger, final File workDir) { return new Callable<Boolean>() { @Override public Boolean call() throws Exception { StreamPump output = null; int result = -1; try { ProcessBuilder processBuilder = new ProcessBuilder().command(commandLine.toShellCommand()) .directory(workDir).redirectErrorStream(true); Process process = processBuilder.start(); output = new StreamPump(); output.start(process, logger); try { result = process.waitFor(); } catch (InterruptedException e) { Thread.interrupted(); ProcessUtil.kill(process); } try { output.await(); // wait for logger } catch (InterruptedException e) { Thread.interrupted(); } } finally { if (output != null) { output.stop(); } } LOG.debug("Done: {}, exit code: {}", commandLine, result); return result == 0; } }; } /** * Gets result of GenerationTask. * * @param task * task * @param successful * reports whether generate process terminated normally or not. * Note: {@code true} is not indicated successful generating but only normal process termination. * @return GenerationResult * @throws ServerException * if an error occurs when try to get result */ private GenerationResult getTaskResult(GenerationTask task, boolean successful) throws ServerException { if (!successful) { return new GenerationResult(false, null, getLogFile(task)); } boolean mavenSuccess = false; BufferedReader logReader = null; try { logReader = new BufferedReader(task.getLogger().getReader()); String line; while ((line = logReader.readLine()) != null) { line = MavenUtils.removeLoggerPrefix(line); if ("BUILD SUCCESS".equals(line)) { mavenSuccess = true; break; } } } catch (IOException e) { throw new ServerException(e); } finally { if (logReader != null) { try { logReader.close(); } catch (IOException ignored) { } } } if (!mavenSuccess) { return new GenerationResult(false, null, getLogFile(task)); } final File workDir = task.getWorkDir(); final GenerationResult result = new GenerationResult(true, null, getLogFile(task)); final File projectFolder = new File(workDir, task.getArtifactId()); if (projectFolder.isDirectory() && projectFolder.list().length > 0) { final File zip = new File(workDir, "project.zip"); try { ZipUtils.zipDir(projectFolder.getAbsolutePath(), projectFolder, zip, IoUtil.ANY_FILTER); } catch (IOException e) { throw new ServerException(e); } result.setGeneratedProject(zip); } return result; } private File getLogFile(GenerationTask task) { return task.getLogger().getFile(); } private void cleanup(GenerationTask task) { File workDir = task.getWorkDir(); if (workDir != null && workDir.exists()) { if (!IoUtil.deleteRecursive(workDir)) { LOG.warn("Unable to delete directory {}", workDir); } } } /** Logger that will write to file all the logs of the project generation process. */ private static class GeneratorLogger implements LineConsumer { private final File file; private final Writer writer; private final boolean autoFlush; GeneratorLogger(File file) throws IOException { this.file = file; autoFlush = true; writer = Files.newBufferedWriter(file.toPath(), Charset.defaultCharset()); } Reader getReader() throws IOException { return Files.newBufferedReader(file.toPath(), Charset.defaultCharset()); } /** Get {@code File} where logs stored. */ File getFile() { return file; } @Override public void writeLine(String line) throws IOException { if (line != null) { writer.write(line); } writer.write('\n'); if (autoFlush) { writer.flush(); } } @Override public void close() throws IOException { writer.close(); } } class GenerationTask extends FutureTask<Boolean> { private final Long id; private final File workDir; private final String artifactId; private final GeneratorLogger logger; private GenerationResult result; /** Time when task was done (successfully ends, fails, cancelled) or -1 if task is not done yet. */ private long endTime; GenerationTask(Callable<Boolean> callable, Long id, File workDir, String artifactId, GeneratorLogger logger) { super(callable); this.id = id; this.workDir = workDir; this.artifactId = artifactId; this.logger = logger; endTime = -1L; } Long getId() { return id; } @Override protected void done() { super.done(); endTime = System.currentTimeMillis(); try { logger.close(); LOG.debug("Close logger {}", logger); } catch (IOException e) { LOG.warn(e.getMessage(), e); } } GeneratorLogger getLogger() { return logger; } /** * Get result of project generation. * * @return result of project generating or {@code null} if task is not done yet * @throws ServerException * if an error occurs when try to start project generating process or get its result. */ GenerationResult getResult() throws ServerException { if (!isDone()) { return null; } if (result == null) { boolean successful; try { successful = super.get(); } catch (InterruptedException e) { // Should not happen since we checked is task done or not. Thread.currentThread().interrupt(); successful = false; } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error)cause; } else if (cause instanceof ServerException) { throw (ServerException)cause; } else { throw new ServerException(cause.getMessage(), cause); } } catch (CancellationException ce) { successful = false; } result = ArchetypeGenerator.this.getTaskResult(this, successful); } return result; } File getWorkDir() { return workDir; } String getArtifactId() { return artifactId; } synchronized boolean isExpired() { return endTime > 0 && (endTime + keepResultTimeMillis) < System.currentTimeMillis(); } } }