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

dev.jbang.source.ProjectBuilder Maven / Gradle / Ivy

There is a newer version: 0.121.0
Show newest version
package dev.jbang.source;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;

import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

import dev.jbang.Settings;
import dev.jbang.catalog.Alias;
import dev.jbang.catalog.Catalog;
import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.ExitException;
import dev.jbang.dependencies.*;
import dev.jbang.source.buildsteps.JarBuildStep;
import dev.jbang.source.resolvers.*;
import dev.jbang.source.sources.JavaSource;
import dev.jbang.util.JavaUtil;
import dev.jbang.util.ModuleUtil;
import dev.jbang.util.PropertiesValueResolver;
import dev.jbang.util.Util;

/**
 * This class constructs a Project. It uses the options given by
 * the user on the command line or things that are part of the user's
 * environment.
 */
public class ProjectBuilder {
	private List additionalSources = new ArrayList<>();
	private List additionalResources = new ArrayList<>();
	private List additionalDeps = new ArrayList<>();
	private List additionalRepos = new ArrayList<>();
	private List additionalClasspaths = new ArrayList<>();
	private Map properties = new HashMap<>();
	private Source.Type forceType = null;
	private String mainClass;
	private String moduleName;
	private List compileOptions = Collections.emptyList();
	private List nativeOptions = Collections.emptyList();
	private Map manifestOptions = new HashMap<>();
	private File catalogFile;
	private Boolean nativeImage;
	private String javaVersion;
	private Boolean enablePreview;

	// Cached values
	private Properties contextProperties;
	private ModularClassPath mcp;
	private final Set buildRefs;

	ProjectBuilder() {
		buildRefs = new HashSet<>();
	}

	private ProjectBuilder(Set buildRefs) {
		this.buildRefs = buildRefs;
	}

	public ProjectBuilder setProperties(Map properties) {
		if (properties != null) {
			this.properties = properties;
		} else {
			this.properties = Collections.emptyMap();
		}
		return this;
	}

	public ProjectBuilder additionalSources(List sources) {
		if (sources != null) {
			this.additionalSources = new ArrayList<>(sources);
		} else {
			this.additionalSources = Collections.emptyList();
		}
		return this;
	}

	public ProjectBuilder additionalResources(List resources) {
		if (resources != null) {
			this.additionalResources = new ArrayList<>(resources);
		} else {
			this.additionalResources = Collections.emptyList();
		}
		return this;
	}

	public ProjectBuilder additionalDependencies(List deps) {
		if (deps != null) {
			this.additionalDeps = new ArrayList<>(deps);
		} else {
			this.additionalDeps = Collections.emptyList();
		}
		mcp = null;
		return this;
	}

	public ProjectBuilder additionalRepositories(List repos) {
		if (repos != null) {
			this.additionalRepos = repos;
		} else {
			this.additionalRepos = Collections.emptyList();
		}
		mcp = null;
		return this;
	}

	public ProjectBuilder additionalClasspaths(List cps) {
		if (cps != null) {
			this.additionalClasspaths = new ArrayList<>(cps);
		} else {
			this.additionalClasspaths = Collections.emptyList();
		}
		mcp = null;
		return this;
	}

	public ProjectBuilder forceType(Source.Type forceType) {
		this.forceType = forceType;
		return this;
	}

	public ProjectBuilder mainClass(String mainClass) {
		this.mainClass = mainClass;
		return this;
	}

	public ProjectBuilder moduleName(String moduleName) {
		this.moduleName = moduleName;
		return this;
	}

	public ProjectBuilder compileOptions(List compileOptions) {
		if (compileOptions != null) {
			this.compileOptions = compileOptions;
		} else {
			this.compileOptions = Collections.emptyList();
		}
		return this;
	}

	public ProjectBuilder nativeOptions(List nativeOptions) {
		if (nativeOptions != null) {
			this.nativeOptions = nativeOptions;
		} else {
			this.nativeOptions = Collections.emptyList();
		}
		return this;
	}

	public ProjectBuilder manifestOptions(Map manifestOptions) {
		if (manifestOptions != null) {
			this.manifestOptions = manifestOptions;
		} else {
			this.manifestOptions = Collections.emptyMap();
		}
		return this;
	}

	public ProjectBuilder nativeImage(Boolean nativeImage) {
		this.nativeImage = nativeImage;
		return this;
	}

	public ProjectBuilder enablePreview(Boolean enablePreviewRequested) {
		this.enablePreview = enablePreviewRequested;
		return this;
	}

	public ProjectBuilder javaVersion(String javaVersion) {
		this.javaVersion = javaVersion;
		return this;
	}

	public ProjectBuilder catalog(File catalogFile) {
		this.catalogFile = catalogFile;
		return this;
	}

	private Properties getContextProperties() {
		if (contextProperties == null) {
			contextProperties = getContextProperties(properties);
		}
		return contextProperties;
	}

	public static Properties getContextProperties(Map properties) {
		Properties contextProperties = new Properties(System.getProperties());
		// early/eager init to property resolution will work.
		new Detector().detect(contextProperties, Collections.emptyList());

		contextProperties.putAll(properties);
		return contextProperties;
	}

	private List replaceAllProps(List items) {
		return items.stream()
					.map(item -> PropertiesValueResolver.replaceProperties(item, getContextProperties()))
					.collect(Collectors.toList());
	}

	private List allToMavenRepo(List repos) {
		return repos.stream().map(DependencyUtil::toMavenRepo).collect(Collectors.toList());

	}

	public Project build(String resource) {
		ResourceRef resourceRef = resolveChecked(getResourceResolver(), resource);
		return build(resourceRef);
	}

	private ResourceRef resolveChecked(ResourceResolver resolver, String resource) {
		Util.verboseMsg("Resolving resource ref: " + resource);
		boolean retryCandidate = catalogFile == null && !Util.isFresh() && Settings.getCacheEvict() > 0
				&& (Catalog.isValidName(resource) || Catalog.isValidCatalogReference(resource)
						|| Util.isRemoteRef(resource));
		ResourceRef ref = null;
		try {
			ref = resolver.resolve(resource);
		} catch (ExitException ee) {
			if (ee.getStatus() != BaseCommand.EXIT_INVALID_INPUT || !retryCandidate) {
				throw ee;
			}
		}
		if (ref == null && retryCandidate) {
			// We didn't get a result and the resource looks like something
			// that could be an alias or a remote URL, so we'll try again
			// with the cache evict set to 0, forcing Jbang to actually check
			// if all its cached information is up-to-date.
			Util.verboseMsg("Retry using cache-evict: " + resource);
			ref = Util.withCacheEvict(() -> resolver.resolve(resource));
		}
		if (ref == null || !Files.isReadable(ref.getFile())) {
			throw new ExitException(BaseCommand.EXIT_INVALID_INPUT,
					"Script or alias could not be found or read: '" + resource + "'");
		}
		Util.verboseMsg("Resolved resource ref as: " + ref);
		return ref;
	}

	// Only used by tests
	public Project build(Path resourceFile) {
		ResourceRef resourceRef = ResourceRef.forFile(resourceFile);
		return build(resourceRef);
	}

	public Project build(ResourceRef resourceRef) {
		if (!buildRefs.add(resourceRef)) {
			throw new ExitException(BaseCommand.EXIT_INVALID_INPUT,
					"Self-referencing project dependency found for: '" + resourceRef.getOriginalResource() + "'");
		}

		Project prj;
		if (resourceRef.getFile().getFileName().toString().endsWith(".jar")) {
			prj = createJarProject(resourceRef);
		} else if (Util.isPreview()
				&& resourceRef.getFile().getFileName().toString().equals(Project.BuildFile.jbang.fileName)) {
			prj = createJbangProject(resourceRef);
		} else {
			prj = createSourceProject(resourceRef);
		}
		return prj;
	}

	private Project createJarProject(ResourceRef resourceRef) {
		Project prj = new Project(resourceRef);
		if (resourceRef.getOriginalResource() != null
				&& DependencyUtil.looksLikeAGav(resourceRef.getOriginalResource())) {
			prj.getMainSourceSet().addDependency(resourceRef.getOriginalResource());
		}
		return updateProject(importJarMetadata(prj, moduleName != null && moduleName.isEmpty()));
	}

	private Project createJbangProject(ResourceRef resourceRef) {
		Project prj = new Project(resourceRef);
		String contents = Util.readFileContent(resourceRef.getFile());
		TagReader tagReader = new TagReader.JbangProject(contents,
				it -> PropertiesValueResolver.replaceProperties(it, getContextProperties()));
		prj.setDescription(tagReader.getDescription().orElse(null));
		prj.setGav(tagReader.getGav().orElse(null));
		prj.setMainClass(tagReader.getMain().orElse(null));
		prj.setModuleName(tagReader.getModule().orElse(null));

		SourceSet ss = prj.getMainSourceSet();
		ss.addResources(tagReader.collectFiles(resourceRef,
				new SiblingResourceResolver(resourceRef, ResourceResolver.forResources())));
		ss.addDependencies(tagReader.collectBinaryDependencies());
		ss.addCompileOptions(tagReader.collectOptions("JAVAC_OPTIONS", "COMPILE_OPTIONS"));
		ss.addNativeOptions(tagReader.collectOptions("NATIVE_OPTIONS"));
		prj.addRepositories(tagReader.collectRepositories());
		prj.addRuntimeOptions(tagReader.collectOptions("JAVA_OPTIONS", "RUNTIME_OPTIONS"));
		tagReader.collectManifestOptions().forEach(kv -> {
			if (!kv.getKey().isEmpty()) {
				prj.getManifestAttributes().put(kv.getKey(), kv.getValue() != null ? kv.getValue() : "true");
			}
		});
		tagReader.collectAgentOptions().forEach(kv -> {
			if (!kv.getKey().isEmpty()) {
				prj.getManifestAttributes().put(kv.getKey(), kv.getValue() != null ? kv.getValue() : "true");
			}
		});
		String version = tagReader.getJavaVersion();
		if (version != null && JavaUtil.checkRequestedVersion(version)) {
			if (new JavaUtil.RequestedVersionComparator().compare(prj.getJavaVersion(), version) > 0) {
				prj.setJavaVersion(version);
			}
		}

		ResourceResolver resolver = getAliasResourceResolver(null);
		ResourceResolver siblingResolver = new SiblingResourceResolver(resourceRef, resolver);

		for (String srcDep : tagReader.collectSourceDependencies()) {
			ResourceRef subRef = resolver.resolve(srcDep, true);
			prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
		}

		boolean first = true;
		for (Source includedSource : tagReader.collectSources(resourceRef, siblingResolver)) {
			updateProject(includedSource, prj, resolver);
			if (first) {
				prj.setMainSource(includedSource);
				first = false;
			}
		}

		return updateProject(prj);
	}

	private Project createSourceProject(ResourceRef resourceRef) {
		Source src = createSource(resourceRef);
		Project prj = new Project(src);
		return updateProject(updateProjectMain(src, prj, getResourceResolver()));
	}

	private Source createSource(ResourceRef resourceRef) {
		return Source.forResourceRef(resourceRef,
				it -> PropertiesValueResolver.replaceProperties(it, getContextProperties()));

	}

	public Project build(Source src) {
		Project prj = new Project(src);
		return updateProject(updateProjectMain(src, prj, getResourceResolver()));
	}

	private Project importJarMetadata(Project prj, boolean importModuleName) {
		Path jar = prj.getResourceRef().getFile();
		if (jar != null && Files.exists(jar)) {
			try (JarFile jf = new JarFile(jar.toFile())) {
				String moduleName = ModuleUtil.getModuleName(jar);
				if (moduleName != null && importModuleName) {
					// We only import the module name if the project's module
					// name was set to an empty string, which basically means
					// "we want module support, but we don't know the name".
					prj.setModuleName(moduleName);
				}

				if (jf.getManifest() != null) {
					Attributes attrs = jf.getManifest().getMainAttributes();
					if (attrs.containsKey(Attributes.Name.MAIN_CLASS)) {
						prj.setMainClass(attrs.getValue(Attributes.Name.MAIN_CLASS));
					}
					String ver = attrs.getValue(JarBuildStep.ATTR_BUILD_JDK);
					if (ver != null) {
						// buildJdk = JavaUtil.parseJavaVersion(ver);
						prj.setJavaVersion(JavaUtil.parseJavaVersion(ver) + "+");
					}
				}

				Optional pom = jf.stream().filter(e -> e.getName().endsWith("/pom.xml")).findFirst();
				if (pom.isPresent()) {
					try (InputStream is = jf.getInputStream(pom.get())) {
						MavenXpp3Reader reader = new MavenXpp3Reader();
						Model model = reader.read(is);
						// GAVS of the form "group:xxxx:999-SNAPSHOT" are skipped
						if (!MavenCoordinate.DUMMY_GROUP.equals(model.getGroupId())
								|| !MavenCoordinate.DEFAULT_VERSION.equals(model.getVersion())) {
							String gav = model.getGroupId() + ":" + model.getArtifactId();
							// The version "999-SNAPSHOT" is ignored
							if (!MavenCoordinate.DEFAULT_VERSION.equals(model.getVersion())) {
								gav += ":" + model.getVersion();
							}
							prj.setGav(gav);
						}
					} catch (XmlPullParserException e) {
						Util.verboseMsg("Unable to read the JAR's pom.xml file", e);
					}
				}

			} catch (IOException e) {
				Util.warnMsg("Problem reading manifest from " + jar);
			}
		}
		return prj;
	}

	private Project updateProject(Project prj) {
		SourceSet ss = prj.getMainSourceSet();
		prj.addRepositories(allToMavenRepo(replaceAllProps(additionalRepos)));
		ss.addDependencies(replaceAllProps(additionalDeps));
		ss.addClassPaths(replaceAllProps(additionalClasspaths));
		updateAllSources(prj, replaceAllProps(additionalSources));
		ss.addResources(allToFileRef(replaceAllProps(additionalResources)));
		ss.addCompileOptions(compileOptions);
		ss.addNativeOptions(nativeOptions);
		prj.putProperties(properties);
		prj.getManifestAttributes().putAll(manifestOptions);
		if (moduleName != null) {
			if (!moduleName.isEmpty() || !prj.getModuleName().isPresent()) {
				prj.setModuleName(moduleName);
			}
		}
		if (mainClass != null) {
			prj.setMainClass(mainClass);
		}
		if (javaVersion != null) {
			prj.setJavaVersion(javaVersion);
		}
		if (nativeImage != null) {
			prj.setNativeImage(nativeImage);
		}
		if (enablePreview != null) {
			prj.setEnablePreviewRequested(enablePreview);
		}
		return prj;
	}

	private void updateAllSources(Project prj, List sources) {
		Catalog catalog = catalogFile != null ? Catalog.get(catalogFile.toPath()) : null;
		ResourceResolver resolver = getResourceResolver();
		sources	.stream()
				.flatMap(f -> Util.explode(null, Util.getCwd(), f).stream())
				.map(s -> resolveChecked(resolver, s))
				.map(this::createSource)
				.forEach(src -> updateProject(src, prj, resolver));
	}

	private List allToFileRef(List resources) {
		ResourceResolver resolver = ResourceResolver.forResources();
		Function propsResolver = it -> PropertiesValueResolver.replaceProperties(it,
				getContextProperties());
		return resources.stream()
						.flatMap(f -> TagReader.explodeFileRef(null, Util.getCwd(), f).stream())
						.map(f -> TagReader.toFileRef(f, resolver))
						.collect(Collectors.toList());
	}

	/**
	 * Updates the given Project with all the information from the
	 * Source when that source is the main file. It updates certain
	 * things at the project level and then calls updateProject() which
	 * will update things at the SourceSet level.
	 *
	 * @param prj      The Project to update
	 * @param resolver The resolver to use for dependent (re)sources
	 * @return A Project
	 */
	private Project updateProjectMain(Source src, Project prj, ResourceResolver resolver) {
		prj.setDescription(src.tagReader.getDescription().orElse(null));
		prj.setGav(src.tagReader.getGav().orElse(null));
		prj.setMainClass(src.tagReader.getMain().orElse(null));
		prj.setModuleName(src.tagReader.getModule().orElse(null));
		if (prj.getMainSource() instanceof JavaSource) {
			prj.getMainSourceSet().addCompileOption("-g");
		}
		return updateProject(src, prj, resolver);
	}

	/**
	 * Updates the given Project with all the information from the
	 * Source. This includes the current source file with all other
	 * source files it references, all resource files, anything to do with
	 * dependencies, repositories and class paths as well as compile time and
	 * runtime options.
	 *
	 * @param prj      The Project to update
	 * @param resolver The resolver to use for dependent (re)sources
	 * @return The given Project
	 */
	@Nonnull
	private Project updateProject(Source src, Project prj, ResourceResolver resolver) {
		ResourceRef srcRef = src.getResourceRef();
		if (!prj.getMainSourceSet().getSources().contains(srcRef)) {
			ResourceResolver sibRes1 = new SiblingResourceResolver(srcRef, ResourceResolver.forResources());
			SourceSet ss = prj.getMainSourceSet();
			ss.addSource(srcRef);
			ss.addResources(src.tagReader.collectFiles(srcRef, sibRes1));
			ss.addDependencies(src.collectBinaryDependencies());
			ss.addCompileOptions(src.getCompileOptions());
			ss.addNativeOptions(src.getNativeOptions());
			prj.addRepositories(src.tagReader.collectRepositories());
			prj.addRuntimeOptions(src.getRuntimeOptions());
			src.tagReader.collectManifestOptions().forEach(kv -> {
				if (!kv.getKey().isEmpty()) {
					prj.getManifestAttributes().put(kv.getKey(), kv.getValue() != null ? kv.getValue() : "true");
				}
			});
			src.tagReader.collectAgentOptions().forEach(kv -> {
				if (!kv.getKey().isEmpty()) {
					prj.getManifestAttributes().put(kv.getKey(), kv.getValue() != null ? kv.getValue() : "true");
				}
			});
			String version = src.tagReader.getJavaVersion();
			if (version != null && JavaUtil.checkRequestedVersion(version)) {
				if (new JavaUtil.RequestedVersionComparator().compare(prj.getJavaVersion(), version) > 0) {
					prj.setJavaVersion(version);
				}
			}
			for (String srcDep : src.collectSourceDependencies()) {
				ResourceRef subRef = sibRes1.resolve(srcDep, true);
				prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
			}
			ResourceResolver sibRes2 = new SiblingResourceResolver(srcRef, resolver);
			for (Source includedSource : src.tagReader.collectSources(srcRef, sibRes2)) {
				updateProject(includedSource, prj, resolver);
			}
		}
		return prj;
	}

	private ResourceResolver getResourceResolver() {
		Catalog cat = catalogFile != null ? Catalog.get(catalogFile.toPath()) : null;
		return new AliasResourceResolver(cat, this::getAliasResourceResolver);
	}

	private ResourceResolver getAliasResourceResolver(Alias alias) {
		if (alias != null) {
			updateFromAlias(alias);
		}
		return new CombinedResourceResolver(
				new RenamingScriptResourceResolver(forceType),
				new LiteralScriptResourceResolver(),
				new RemoteResourceResolver(false),
				new ClasspathResourceResolver(),
				new GavResourceResolver(this::resolveDependency),
				new FileResourceResolver());
	}

	private ModularClassPath resolveDependency(String dep) {
		if (mcp == null) {
			DependencyResolver resolver = new DependencyResolver()
																	.addDependency(dep)
																	.addRepositories(allToMavenRepo(
																			replaceAllProps(additionalRepos)))
																	.addDependencies(replaceAllProps(additionalDeps))
																	.addClassPaths(
																			replaceAllProps(additionalClasspaths));
			mcp = resolver.resolve();
		}
		return mcp;
	}

	private void updateFromAlias(Alias alias) {
		if (additionalSources.isEmpty()) {
			additionalSources(alias.sources);
		}
		if (additionalResources.isEmpty()) {
			additionalResources(alias.resources);
		}
		if (additionalDeps.isEmpty()) {
			additionalDependencies(alias.dependencies);
		}
		if (additionalRepos.isEmpty()) {
			additionalRepositories(alias.repositories);
		}
		if (additionalClasspaths.isEmpty()) {
			additionalClasspaths(alias.classpaths);
		}
		if (properties.isEmpty()) {
			setProperties(alias.properties);
		}
		if (javaVersion == null) {
			javaVersion(alias.javaVersion);
		}
		if (compileOptions.isEmpty()) {
			compileOptions(alias.compileOptions);
		}
		if (nativeImage == null) {
			nativeImage(alias.nativeImage);
		}
		if (nativeOptions.isEmpty()) {
			nativeOptions(alias.nativeOptions);
		}
		if (manifestOptions.isEmpty()) {
			manifestOptions(alias.manifestOptions);
		}
		if (enablePreview == null) {
			enablePreview(alias.enablePreview);
		}
	}

	public static boolean isAlias(ResourceRef resourceRef) {
		return resourceRef instanceof AliasResourceResolver.AliasedResourceRef;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy