All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.grails.cli.command.archive.ArchiveCommand Maven / Gradle / Ivy

/*
 * Copyright 2012-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.grails.cli.command.archive;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.jar.Manifest;

import groovy.lang.Grab;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;

import org.grails.cli.boot.SpringApplicationLauncher;
import org.grails.cli.archive.PackagedSpringApplicationLauncher;
import org.grails.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.Command;
import org.grails.cli.command.archive.ResourceMatcher.MatchedResource;
import org.grails.cli.command.options.CompilerOptionHandler;
import org.grails.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.grails.cli.command.options.SourceOptions;
import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.grails.cli.compiler.GroovyCompiler;
import org.grails.cli.compiler.GroovyCompilerConfiguration;
import org.grails.cli.compiler.RepositoryConfigurationFactory;
import org.grails.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;

/**
 * Abstract {@link Command} to create a self-contained executable archive file from a CLI
 * application.
 *
 * @author Andy Wilkinson
 * @author Phillip Webb
 * @author Andrey Stolyarov
 * @author Henri Kerola
 */
abstract class ArchiveCommand extends OptionParsingCommand {

	protected ArchiveCommand(String name, String description, OptionHandler optionHandler) {
		super(name, description, optionHandler);
	}

	@Override
	public String getUsageHelp() {
		return "[options] <" + getName() + "-name> ";
	}

	/**
	 * Abstract base {@link CompilerOptionHandler} for archive commands.
	 */
	protected abstract static class ArchiveOptionHandler extends CompilerOptionHandler {

		private final String type;

		private final Layout layout;

		private OptionSpec includeOption;

		private OptionSpec excludeOption;

		public ArchiveOptionHandler(String type, Layout layout) {
			this.type = type;
			this.layout = layout;
		}

		protected Layout getLayout() {
			return this.layout;
		}

		@Override
		protected void doOptions() {
			this.includeOption = option("include",
					"Pattern applied to directories on the classpath to find files to include in the resulting ")
				.withRequiredArg()
				.withValuesSeparatedBy(",")
				.defaultsTo("");
			this.excludeOption = option("exclude", "Pattern applied to directories on the classpath to find files to "
					+ "exclude from the resulting " + this.type)
				.withRequiredArg()
				.withValuesSeparatedBy(",")
				.defaultsTo("");
		}

		@Override
		protected ExitStatus run(OptionSet options) throws Exception {
			List nonOptionArguments = new ArrayList(options.nonOptionArguments());
			Assert.isTrue(nonOptionArguments.size() >= 2,
					() -> "The name of the resulting " + this.type + " and at least one source file must be specified");

			File output = new File((String) nonOptionArguments.remove(0));
			Assert.isTrue(output.getName().toLowerCase(Locale.ENGLISH).endsWith("." + this.type),
					() -> "The output '" + output + "' is not a " + this.type.toUpperCase(Locale.ENGLISH) + " file.");
			deleteIfExists(output);

			GroovyCompiler compiler = createCompiler(options);

			List classpath = getClassPathUrls(compiler);
			List classpathEntries = findMatchingClasspathEntries(classpath, options);

			String[] sources = new SourceOptions(nonOptionArguments).getSourcesArray();
			Class[] compiledClasses = compiler.compile(sources);

			List dependencies = getClassPathUrls(compiler);
			dependencies.removeAll(classpath);

			writeJar(output, compiledClasses, classpathEntries, dependencies);
			return ExitStatus.OK;
		}

		private void deleteIfExists(File file) {
			if (file.exists() && !file.delete()) {
				throw new IllegalStateException("Failed to delete existing file " + file.getPath());
			}
		}

		private GroovyCompiler createCompiler(OptionSet options) {
			List repositoryConfiguration = RepositoryConfigurationFactory
				.createDefaultRepositoryConfiguration();
			GroovyCompilerConfiguration configuration = new OptionSetGroovyCompilerConfiguration(options, this,
					repositoryConfiguration);
			GroovyCompiler groovyCompiler = new GroovyCompiler(configuration);
			groovyCompiler.getAstTransformations().add(0, new GrabAnnotationTransform());
			return groovyCompiler;
		}

		private List getClassPathUrls(GroovyCompiler compiler) {
			return new ArrayList<>(Arrays.asList(compiler.getLoader().getURLs()));
		}

		private List findMatchingClasspathEntries(List classpath, OptionSet options)
				throws IOException {
			ResourceMatcher matcher = new ResourceMatcher(options.valuesOf(this.includeOption),
					options.valuesOf(this.excludeOption));
			List roots = new ArrayList<>();
			for (URL classpathEntry : classpath) {
				roots.add(new File(URI.create(classpathEntry.toString())));
			}
			return matcher.find(roots);
		}

		private void writeJar(File file, Class[] compiledClasses, List classpathEntries,
				List dependencies) throws IOException, URISyntaxException {
			final List libraries;
			try (JarWriter writer = new JarWriter(file)) {
				addManifest(writer, compiledClasses);
				addCliClasses(writer);
				for (Class compiledClass : compiledClasses) {
					addClass(writer, compiledClass);
				}
				libraries = addClasspathEntries(writer, classpathEntries);
			}
			libraries.addAll(createLibraries(dependencies));
			Repackager repackager = new Repackager(file);
			repackager.setMainClass(PackagedSpringApplicationLauncher.class.getName());
			repackager.repackage((callback) -> {
				for (Library library : libraries) {
					callback.library(library);
				}
			});
		}

		private List createLibraries(List dependencies) throws URISyntaxException {
			List libraries = new ArrayList<>();
			for (URL dependency : dependencies) {
				File file = new File(dependency.toURI());
				libraries.add(new Library(null, file, getLibraryScope(file), null, false, false, true));
			}
			return libraries;
		}

		private void addManifest(JarWriter writer, Class[] compiledClasses) throws IOException {
			Manifest manifest = new Manifest();
			manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
			manifest.getMainAttributes()
				.putValue(PackagedSpringApplicationLauncher.SOURCE_ENTRY, commaDelimitedClassNames(compiledClasses));
			writer.writeManifest(manifest);
		}

		private String commaDelimitedClassNames(Class[] classes) {
			StringBuilder builder = new StringBuilder();
			for (int i = 0; i < classes.length; i++) {
				if (i != 0) {
					builder.append(',');
				}
				builder.append(classes[i].getName());
			}
			return builder.toString();
		}

		protected void addCliClasses(JarWriter writer) throws IOException {
			addClass(writer, PackagedSpringApplicationLauncher.class);
			addClass(writer, SpringApplicationLauncher.class);
			Resource[] resources = new PathMatchingResourcePatternResolver()
				.getResources("org/grails/cli/groovy/**");
			for (Resource resource : resources) {
				String url = resource.getURL().toString();
				addResource(writer, resource, url.substring(url.indexOf("org/grails/cli/groovy/")));
			}
		}

		protected final void addClass(JarWriter writer, Class sourceClass) throws IOException {
			addClass(writer, sourceClass.getClassLoader(), sourceClass.getName());
		}

		protected final void addClass(JarWriter writer, ClassLoader classLoader, String sourceClass)
				throws IOException {
			if (classLoader == null) {
				classLoader = Thread.currentThread().getContextClassLoader();
			}
			String name = sourceClass.replace('.', '/') + ".class";
			InputStream stream = classLoader.getResourceAsStream(name);
			writer.writeEntry(this.layout.getClassesLocation() + name, stream);
		}

		private void addResource(JarWriter writer, Resource resource, String name) throws IOException {
			InputStream stream = resource.getInputStream();
			writer.writeEntry(name, stream);
		}

		private List addClasspathEntries(JarWriter writer, List entries) throws IOException {
			List libraries = new ArrayList<>();
			for (MatchedResource entry : entries) {
				if (entry.isRoot()) {
					libraries.add(new Library(null, entry.getFile(), LibraryScope.COMPILE, null, false, false, true));
				}
				else {
					writeClasspathEntry(writer, entry);
				}
			}
			return libraries;
		}

		protected void writeClasspathEntry(JarWriter writer, MatchedResource entry) throws IOException {
			writer.writeEntry(entry.getName(), new FileInputStream(entry.getFile()));
		}

		protected abstract LibraryScope getLibraryScope(File file);

	}

	/**
	 * {@link ASTTransformation} to change {@code @Grab} annotation values.
	 */
	private static class GrabAnnotationTransform implements ASTTransformation {

		@Override
		public void visit(ASTNode[] nodes, SourceUnit source) {
			for (ASTNode node : nodes) {
				if (node instanceof ModuleNode) {
					visitModule((ModuleNode) node);
				}
			}
		}

		private void visitModule(ModuleNode module) {
			for (ClassNode classNode : module.getClasses()) {
				AnnotationNode annotation = new AnnotationNode(new ClassNode(Grab.class));
				annotation.addMember("value", new ConstantExpression("groovy"));
				classNode.addAnnotation(annotation);
				// We only need to do it at most once
				break;
			}
			// Disable the addition of a static initializer that calls Grape.addResolver
			// because all the dependencies are local now
			disableGrabResolvers(module.getClasses());
			disableGrabResolvers(module.getImports());
		}

		private void disableGrabResolvers(List nodes) {
			for (AnnotatedNode classNode : nodes) {
				List annotations = classNode.getAnnotations();
				for (AnnotationNode node : new ArrayList<>(annotations)) {
					if (node.getClassNode().getNameWithoutPackage().equals("GrabResolver")) {
						node.setMember("initClass", new ConstantExpression(false));
					}
				}
			}
		}

	}

}