dev.jbang.source.ProjectBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jbang-cli Show documentation
Show all versions of jbang-cli Show documentation
JBang Command Line Interface
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;
}
}