diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java
index bac82ecb..a5732038 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java
@@ -60,6 +60,28 @@ public BazelWorkspace getBazelWorkspace() {
return bazelWorkspace;
}
+ /**
+ * Returns the folder where source files links to generated sources will be created.
+ *
+ * This can be used whenever there is a need to bring generated sources into a project (eg., .srcjar
).
+ * It is expected that this folder is used as a parent. It should only contain symlinks to folders with the
+ * generated sources. Do not copy generated sources in here.
+ *
+ *
+ * @param project
+ * @return the folder to use for generated sources
+ */
+ public IFolder getGeneratedSourcesFolder(BazelProject project) {
+ return project.getProject().getFolder("generated-srcs");
+ }
+
+ /**
+ * @see #getGeneratedSourcesFolder(BazelProject)
+ */
+ public IFolder getGeneratedSourcesFolderForTests(BazelProject project) {
+ return project.getProject().getFolder("generated-test-srcs");
+ }
+
/**
* @see #getVirtualSourceFolder(BazelProject)
*/
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java
index 202196fe..67c09126 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java
@@ -199,6 +199,16 @@ BazelBinary getBazelBinary() throws CoreException {
return getInfo().getBazelBinary();
}
+ /**
+ * {@return absolute file system location to the bazel-bin
symlink target}
+ *
+ * @throws CoreException
+ * if the workspace does not exist
+ */
+ public IPath getBazelBinLocation() throws CoreException {
+ return getInfo().getBazelBin();
+ }
+
/**
* Returns a Bazel package for the given label.
*
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java
index d02e2365..8698302f 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java
@@ -89,6 +89,7 @@
import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaResourceInfo;
import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSourceEntry;
import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSourceInfo;
+import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSrcJarEntry;
import com.salesforce.bazel.sdk.command.BazelCQueryWithStarlarkExpressionCommand;
/**
@@ -239,6 +240,36 @@ private void addSourceFolders(BazelProject project, List rawCla
}
if (javaSourceInfo.hasSourceDirectories()) {
for (IPath dir : javaSourceInfo.getSourceDirectories()) {
+ // check for srcjar
+ if (dir.isAbsolute()) {
+ // double check
+ if (!javaSourceInfo.matchAllSourceDirectoryEntries(dir, JavaSrcJarEntry.class::isInstance)) {
+ throw new IllegalStateException(
+ format("programming error: found unsupported content for source directory '%s'", dir));
+ }
+
+ var srcjarFolder =
+ getFileSystemMapper().getGeneratedSourcesFolder(project).getFolder(dir.lastSegment());
+ if (!srcjarFolder.exists()) {
+ createBuildPathProblem(
+ project,
+ Status.error(
+ format(
+ "Folder '%s' does not exist. However, it's required for the project classpath to resolve properly. Please investigat!",
+ srcjarFolder)));
+ } else {
+ rawClasspath.add(
+ JavaCore.newSourceEntry(
+ srcjarFolder.getFullPath(),
+ null,
+ null,
+ outputLocation,
+ classpathAttributes));
+ }
+
+ continue;
+ }
+
// when the directory is empty, the virtual "srcs" container must be used
// this logic here requires proper linking support in linkSourcesIntoProject method
var sourceFolder = dir.isEmpty() ? virtualSourceFolder : project.getProject().getFolder(dir);
@@ -874,6 +905,14 @@ protected void doInitializeClasspaths(List projects, BazelWorkspac
protected abstract List doProvisionProjects(Collection targets, SubMonitor monitor)
throws CoreException;
+ private void ensureFolderLinksToTarget(IFolder folderWhichShouldBeALink, IPath linkTarget, SubMonitor monitor)
+ throws CoreException {
+ if (folderWhichShouldBeALink.exists() && !folderWhichShouldBeALink.isLinked()) {
+ folderWhichShouldBeALink.delete(true, monitor.split(1));
+ }
+ folderWhichShouldBeALink.createLink(linkTarget, IResource.REPLACE, monitor.split(1));
+ }
+
protected BazelPackage expectCommonBazelPackage(Collection targets) throws CoreException {
List allPackages =
targets.stream().map(BazelTarget::getBazelPackage).distinct().collect(toList());
@@ -920,6 +959,23 @@ private IProject findProjectForLocation(IPath location) {
return null;
}
+ private void garbageCollectFolderContent(IFolder folder, Set membersToRetain, SubMonitor monitor)
+ throws CoreException {
+ if (!folder.exists()) {
+ return;
+ }
+
+ for (IResource member : folder.members()) {
+ if (!membersToRetain.contains(member)) {
+ member.delete(IResource.NONE, monitor.split(1));
+ }
+ }
+
+ if (folder.members().length == 0) {
+ folder.delete(IResource.NONE, monitor.split(1));
+ }
+ }
+
protected IWorkspace getEclipseWorkspace() {
return ResourcesPlugin.getWorkspace();
}
@@ -993,150 +1049,242 @@ private boolean isTestTarget(BazelTarget bazelTarget) throws CoreException {
return bazelTarget.getRuleClass().contains("test");
}
+ private void linkGeneratedSourceDirectories(JavaSourceInfo sourceInfo, IFolder generatedSourcesFolder,
+ SubMonitor monitor) throws CoreException {
+ if (!sourceInfo.hasSourceDirectories()) {
+ return;
+ }
+
+ var directories = sourceInfo.getSourceDirectories();
+
+ // capture created srcjar folders for GC later
+ var generatedSourcesMembers = new HashSet();
+
+ // check each directory
+ for (IPath dir : directories) {
+ // ignore non-srcjars
+ if (!dir.isAbsolute()) {
+ continue;
+ }
+
+ // double check
+ if (!sourceInfo.matchAllSourceDirectoryEntries(dir, JavaSrcJarEntry.class::isInstance)) {
+ throw new IllegalStateException(
+ format("programming error: found unsupported content for source directory '%s'", dir));
+ }
+
+ if (!generatedSourcesFolder.exists()) {
+ createFolderAndParents(generatedSourcesFolder, monitor.split(1));
+ }
+
+ var srcjarFolder = generatedSourcesFolder.getFolder(dir.lastSegment());
+ ensureFolderLinksToTarget(srcjarFolder, dir, monitor);
+ generatedSourcesMembers.add(srcjarFolder);
+ }
+
+ // cleanup any old generated srcjar folders
+ garbageCollectFolderContent(generatedSourcesFolder, generatedSourcesMembers, monitor);
+ }
+
/**
- * Creates Eclipse virtual files/folders for sources collected in the {@link JavaProjectInfo}.
+ * Creates Eclipse virtual folders for generated sources collected in the {@link JavaSourceInfo}.
*
* @param project
- * @param javaInfo
+ * @param sourceInfo
* @param progress
* @throws CoreException
*/
- protected void linkSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo, IProgressMonitor progress)
- throws CoreException {
+ protected void linkGeneratedSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo,
+ IProgressMonitor progress) throws CoreException {
var monitor = SubMonitor.convert(progress, 100);
try {
- var sourceInfo = javaInfo.getSourceInfo();
+ linkGeneratedSourceDirectories(
+ javaInfo.getSourceInfo(),
+ getFileSystemMapper().getGeneratedSourcesFolder(project),
+ monitor);
+ linkGeneratedSourceDirectories(
+ javaInfo.getTestSourceInfo(),
+ getFileSystemMapper().getGeneratedSourcesFolderForTests(project),
+ monitor);
+ } finally {
+ progress.done();
+ }
+ }
+
+ private void linkSourceDirectories(BazelProject project, JavaProjectInfo javaInfo, JavaSourceInfo sourceInfo,
+ IFolder virtualSourceFolder, IFolder generatedSourcesFolder, SubMonitor monitor) throws CoreException {
+ if (!sourceInfo.hasSourceDirectories()) {
+ return;
+ }
+
+ var directories = sourceInfo.getSourceDirectories();
+
+ // check each directory
+ NEXT_FOLDER: for (IPath dir : directories) {
+ // ignore srcjars
+ if (dir.isAbsolute()) {
+ continue;
+ }
+
+ IFolder sourceFolder;
+ if (dir.isEmpty()) {
+ // special case ... source is directly within the project
+ // this is usually the case when the Bazel package is a Java package
+ // in this case we need to link (emulate) its package structure
+ // however, we can only support this properly if this is the only folder
+ if (directories.size() > 1) {
+ createBuildPathProblem(
+ project,
+ Status.error(
+ "Impossible to support project: found multiple source directories which seems to be nested! Please consider restructuring the targets."));
+ continue NEXT_FOLDER;
+ }
+ // and there aren't any other source files to be linked
+ if (sourceInfo.hasSourceFilesWithoutCommonRoot()) {
+ createBuildPathProblem(
+ project,
+ Status.error(
+ "Impossible to support project: found mix of source files without common root and empty package fragment root! Please consider restructuring the targets."));
+ continue NEXT_FOLDER;
+ }
+ // check this maps to a single Java package
+ var detectedJavaPackagesForSourceDirectory = sourceInfo.getDetectedJavaPackagesForSourceDirectory(dir);
+ var packagePath = findCommonParentPackagePrefix(detectedJavaPackagesForSourceDirectory);
+ if (packagePath == null) {
+ createBuildPathProblem(
+ project,
+ Status.error(
+ format(
+ "Impossible to support project: an empty package fragment root must map to one Java package (got '%s')! Please consider restructuring the targets.",
+ detectedJavaPackagesForSourceDirectory.isEmpty() ? "none"
+ : detectedJavaPackagesForSourceDirectory.stream()
+ .map(IPath::toString)
+ .collect(joining(", ")))));
+ continue NEXT_FOLDER;
+ }
- if (sourceInfo.hasSourceFilesWithoutCommonRoot()) {
// create the "srcs" folder
- var virtualSourceFolder = getFileSystemMapper().getVirtualSourceFolder(project);
+ if (virtualSourceFolder.exists() && virtualSourceFolder.isLinked()) {
+ // delete it to ensure we start fresh
+ virtualSourceFolder.delete(true, monitor.split(1));
+ }
createFolderAndParents(virtualSourceFolder, monitor.split(1));
- // build emulated Java package structure and link files
- var files = sourceInfo.getSourceFilesWithoutCommonRoot();
- Set linkedFiles = new HashSet<>();
- for (JavaSourceEntry fileEntry : files) {
- // peek at Java package to find proper "root"
- var packagePath = fileEntry.getDetectedPackagePath();
- var packageFolder = virtualSourceFolder.getFolder(packagePath);
- if (!packageFolder.exists()) {
- createFolderAndParents(packageFolder, monitor.split(1));
- }
-
- // create link to file
- var file = packageFolder.getFile(fileEntry.getPath().lastSegment());
- file.createLink(fileEntry.getLocation(), IResource.REPLACE, monitor.split(1));
- // remember for cleanup
- linkedFiles.add(file);
+ // build emulated Java package structure and link the directory
+ var packageFolder = virtualSourceFolder.getFolder(packagePath);
+ if (!packageFolder.getParent().exists()) {
+ createFolderAndParents(packageFolder.getParent(), monitor.split(1));
}
+ ensureFolderLinksToTarget(packageFolder, javaInfo.getBazelPackage().getLocation(), monitor);
// remove all files not created as part of this loop
- deleteAllFilesNotInAllowList(virtualSourceFolder, linkedFiles, monitor.split(1));
+ deleteAllFilesNotInFolderList(virtualSourceFolder, packageFolder, monitor.split(1));
+
+ // done
+ break;
}
- if (sourceInfo.hasSourceDirectories()) {
- var directories = sourceInfo.getSourceDirectories();
- NEXT_FOLDER: for (IPath dir : directories) {
- IFolder sourceFolder;
- if (dir.isEmpty()) {
- // special case ... source is directly within the project
- // this is usually the case when the Bazel package is a Java package
- // in this case we need to link (emulate) its package structure
- // however, we can only support this properly if this is the only folder
- if (directories.size() > 1) {
- createBuildPathProblem(
- project,
- Status.error(
- "Impossible to support project: found multiple source directories which seems to be nested! Please consider restructuring the targets."));
- continue NEXT_FOLDER;
- }
- // and there aren't any other source files to be linked
- if (sourceInfo.hasSourceFilesWithoutCommonRoot()) {
- createBuildPathProblem(
- project,
- Status.error(
- "Impossible to support project: found mix of source files without common root and empty package fragment root! Please consider restructuring the targets."));
- continue NEXT_FOLDER;
- }
- // check this maps to a single Java package
- var detectedJavaPackagesForSourceDirectory =
- sourceInfo.getDetectedJavaPackagesForSourceDirectory(dir);
- var packagePath = findCommonParentPackagePrefix(detectedJavaPackagesForSourceDirectory);
- if (packagePath == null) {
- createBuildPathProblem(
- project,
- Status.error(
- format(
- "Impossible to support project: an empty package fragment root must map to one Java package (got '%s')! Please consider restructuring the targets.",
- detectedJavaPackagesForSourceDirectory.isEmpty() ? "none"
- : detectedJavaPackagesForSourceDirectory.stream()
- .map(IPath::toString)
- .collect(joining(", ")))));
- continue NEXT_FOLDER;
- }
-
- // create the "srcs" folder
- var virtualSourceFolder = getFileSystemMapper().getVirtualSourceFolder(project);
- if (virtualSourceFolder.exists() && virtualSourceFolder.isLinked()) {
- // delete it to ensure we start fresh
- virtualSourceFolder.delete(true, monitor.split(1));
- }
- createFolderAndParents(virtualSourceFolder, monitor.split(1));
-
- // build emulated Java package structure and link the directory
- var packageFolder = virtualSourceFolder.getFolder(packagePath);
- if (!packageFolder.getParent().exists()) {
- createFolderAndParents(packageFolder.getParent(), monitor.split(1));
- }
- if (packageFolder.exists() && !packageFolder.isLinked()) {
- packageFolder.delete(true, monitor.split(1));
- }
- packageFolder.createLink(
- javaInfo.getBazelPackage().getLocation(),
- IResource.REPLACE,
- monitor.split(1));
-
- // remove all files not created as part of this loop
- deleteAllFilesNotInFolderList(virtualSourceFolder, packageFolder, monitor.split(1));
-
- // done
+ // check for existing folder
+ sourceFolder = project.getProject().getFolder(dir);
+ if (sourceFolder.exists() && !sourceFolder.isLinked()) {
+ // check if there is any linked parent we can remove
+ var parent = sourceFolder.getParent();
+ while ((parent != null) && (parent.getType() != IResource.PROJECT)) {
+ if (parent.isLinked()) {
+ parent.delete(true, monitor.split(1));
break;
}
+ parent = parent.getParent();
+ }
+ if (sourceFolder.exists()) {
+ // TODO create problem marker
+ LOG.warn(
+ "Impossible to support project '{}' - found existing source directoy which cannot be deleted!",
+ project);
+ continue NEXT_FOLDER;
+ }
+ }
- // check for existing folder
- sourceFolder = project.getProject().getFolder(dir);
- if (sourceFolder.exists() && !sourceFolder.isLinked()) {
- // check if there is any linked parent we can remove
- var parent = sourceFolder.getParent();
- while ((parent != null) && (parent.getType() != IResource.PROJECT)) {
- if (parent.isLinked()) {
- parent.delete(true, monitor.split(1));
- break;
- }
- parent = parent.getParent();
- }
- if (sourceFolder.exists()) {
- // TODO create problem marker
- LOG.warn(
- "Impossible to support project '{}' - found existing source directoy which cannot be deleted!",
- project);
- continue NEXT_FOLDER;
- }
- }
+ // ensure the parent exists
+ if (!sourceFolder.getParent().exists()) {
+ createFolderAndParents(sourceFolder.getParent(), monitor.split(1));
+ }
- // ensure the parent exists
- if (!sourceFolder.getParent().exists()) {
- createFolderAndParents(sourceFolder.getParent(), monitor.split(1));
- }
+ // create link to folder
+ sourceFolder.createLink(
+ javaInfo.getBazelPackage().getLocation().append(dir),
+ IResource.REPLACE,
+ monitor.split(1));
+ }
+ }
- // create link to folder
- sourceFolder.createLink(
- javaInfo.getBazelPackage().getLocation().append(dir),
- IResource.REPLACE,
- monitor.split(1));
- }
+ private void linkSourceFilesWithoutCommonRoot(JavaSourceInfo sourceInfo, IFolder virtualSourceFolder,
+ SubMonitor monitor) throws CoreException {
+ if (!sourceInfo.hasSourceFilesWithoutCommonRoot()) {
+ return;
+ }
+
+ // create the "srcs" folder
+ createFolderAndParents(virtualSourceFolder, monitor.split(1));
+ // build emulated Java package structure and link files
+ var files = sourceInfo.getSourceFilesWithoutCommonRoot();
+ Set linkedFiles = new HashSet<>();
+ for (JavaSourceEntry fileEntry : files) {
+ // peek at Java package to find proper "root"
+ var packagePath = fileEntry.getDetectedPackagePath();
+ var packageFolder = virtualSourceFolder.getFolder(packagePath);
+ if (!packageFolder.exists()) {
+ createFolderAndParents(packageFolder, monitor.split(1));
}
+ // create link to file
+ var file = packageFolder.getFile(fileEntry.getPath().lastSegment());
+ file.createLink(fileEntry.getLocation(), IResource.REPLACE, monitor.split(1));
+
+ // remember for cleanup
+ linkedFiles.add(file);
+ }
+
+ // remove all files not created as part of this loop
+ deleteAllFilesNotInAllowList(virtualSourceFolder, linkedFiles, monitor.split(1));
+ }
+
+ /**
+ * Creates Eclipse virtual files/folders for sources collected in the {@link JavaSourceInfo}.
+ *
+ * @param project
+ * @param sourceInfo
+ * @param progress
+ * @throws CoreException
+ */
+ protected void linkSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo, IProgressMonitor progress)
+ throws CoreException {
+ var monitor = SubMonitor.convert(progress, 100);
+ try {
+ linkSourceFilesWithoutCommonRoot(
+ javaInfo.getSourceInfo(),
+ getFileSystemMapper().getVirtualSourceFolder(project),
+ monitor);
+ linkSourceDirectories(
+ project,
+ javaInfo,
+ javaInfo.getSourceInfo(),
+ getFileSystemMapper().getVirtualSourceFolder(project),
+ getFileSystemMapper().getGeneratedSourcesFolder(project),
+ monitor);
+
+ linkSourceFilesWithoutCommonRoot(
+ javaInfo.getSourceInfo(),
+ getFileSystemMapper().getVirtualSourceFolderForTests(project),
+ monitor);
+ linkSourceDirectories(
+ project,
+ javaInfo,
+ javaInfo.getTestSourceInfo(),
+ getFileSystemMapper().getVirtualSourceFolderForTests(project),
+ getFileSystemMapper().getGeneratedSourcesFolderForTests(project),
+ monitor);
+
// ensure the BUILD file is linked
var buildFileLocation = javaInfo.getBazelPackage().getBuildFileLocation();
var buildFile = project.getProject().getFile(buildFileLocation.lastSegment());
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java
index 381a208e..7cd609da 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java
@@ -471,6 +471,9 @@ protected List doProvisionProjects(Collection targets
.collect(joining(", ")))));
}
+ // configure links
+ linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1));
+
// configure classpath
configureRawClasspath(project, javaInfo, monitor.split(1));
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java
index 63ba559b..7120788f 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java
@@ -256,6 +256,9 @@ protected List doProvisionProjects(Collection targets
.collect(joining(", ")))));
}
+ // configure links
+ linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1));
+
// configure classpath
configureRawClasspath(project, javaInfo, monitor.split(1));
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java
index ede12905..a42dcb7a 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java
@@ -197,6 +197,7 @@ protected BazelProject provisionJavaLibraryProject(BazelTarget target, SubMonito
// configure links
linkSourcesIntoProject(project, javaInfo, monitor.split(1));
+ linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1));
// configure classpath
configureRawClasspath(project, javaInfo, monitor.split(1));
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java
index 17e1e1d0..df09616f 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java
@@ -179,13 +179,13 @@ private void addToSrc(Collection srcs, String srcFileOrLabel) throws Core
public IStatus analyzeProjectRecommendations(IProgressMonitor monitor) throws CoreException {
var result = new MultiStatus(JavaProjectInfo.class, 0, "Java Analysis Result");
- sourceInfo = new JavaSourceInfo(this.srcs, bazelPackage.getLocation());
+ sourceInfo = new JavaSourceInfo(this.srcs, bazelPackage);
sourceInfo.analyzeSourceDirectories(result);
resourceInfo = new JavaResourceInfo(resources, bazelPackage);
resourceInfo.analyzeResourceDirectories(result);
- testSourceInfo = new JavaSourceInfo(this.testSrcs, bazelPackage.getLocation(), sourceInfo);
+ testSourceInfo = new JavaSourceInfo(this.testSrcs, bazelPackage, sourceInfo);
testSourceInfo.analyzeSourceDirectories(result);
testResourceInfo = new JavaResourceInfo(testResources, bazelPackage);
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java
index aa92c50d..ada9da65 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java
@@ -7,12 +7,12 @@
import org.eclipse.core.runtime.IPath;
/**
- * A source entry points to exactly one .java
source file. It contains additional logic for extracting
- * the package path from the location.
+ * A source entry points to exactly one .java
source file. It contains additional logic for extracting the
+ * package path from the location.
*/
public class JavaSourceEntry implements Entry {
- private static boolean endsWith(IPath path, IPath lastSegments) {
+ static boolean endsWith(IPath path, IPath lastSegments) {
if (path.segmentCount() < lastSegments.segmentCount()) {
return false;
}
@@ -60,6 +60,13 @@ public boolean equals(Object obj) {
&& Objects.equals(relativePath, other.relativePath);
}
+ /**
+ * @return the Bazel package location this entry is contained in
+ */
+ public IPath getBazelPackageLocation() {
+ return bazelPackageLocation;
+ }
+
/**
* {@return absolute location of of the container of this path entry}
*/
@@ -93,8 +100,8 @@ public IPath getPathParent() {
}
/**
- * @return first few segments of {@link #getPathParent()} which could be the source directory, or
- * null
if unlikely
+ * @return first few segments of {@link #getPathParent()} (relative to {@link #getBazelPackageLocation()}) which
+ * could be the source directory, or null
if unlikely
*/
public IPath getPotentialSourceDirectoryRoot() {
var detectedPackagePath = getDetectedPackagePath();
@@ -113,6 +120,14 @@ public int hashCode() {
return Objects.hash(bazelPackageLocation, relativePath);
}
+ /**
+ * {@return true
if the entry is generated, i.e. located outside a Bazel package, false
+ * otherwise}
+ */
+ public boolean isExternalOrGenerated() {
+ return false;
+ }
+
@Override
public String toString() {
return relativePath + " (relativePathParent=" + relativePathParent + ", bazelPackageLocation="
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java
index 9c8eda94..42ab9e0f 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java
@@ -1,18 +1,27 @@
package com.salesforce.bazel.eclipse.core.model.discovery.projects;
import static java.lang.String.format;
+import static java.nio.file.Files.copy;
+import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.find;
import static java.nio.file.Files.isRegularFile;
import static java.nio.file.Files.readString;
+import static java.nio.file.Files.walkFileTree;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -21,16 +30,22 @@
import java.util.Set;
import java.util.StringTokenizer;
import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.MultiStatus;
-import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
+import com.salesforce.bazel.eclipse.core.model.BazelPackage;
+import com.salesforce.bazel.eclipse.core.model.BazelTarget;
+import com.salesforce.bazel.eclipse.core.model.BazelWorkspace;
+
/**
* Source information used by {@link JavaProjectInfo} to analyze the srcs
information in order to identify
* root directories or split packages and recommend a layout.
@@ -38,7 +53,7 @@
public class JavaSourceInfo {
private static final IPath NOT_FOLLOWING_JAVA_PACKAGE_STRUCTURE =
- new Path("_not_following_java_package_structure_");
+ IPath.forPosix("_not_following_java_package_structure_");
private static boolean isJavaFile(java.nio.file.Path file) {
return isRegularFile(file) && file.getFileName().toString().endsWith(".java");
@@ -48,6 +63,7 @@ private static boolean isJavaFile(java.nio.file.Path file) {
private final Collection srcs;
private final IPath bazelPackageLocation;
private final JavaSourceInfo sharedSourceInfo;
+ private final BazelWorkspace bazelWorkspace;
/**
* A list of all source files impossible to identify a common root directory
@@ -60,10 +76,11 @@ private static boolean isJavaFile(java.nio.file.Path file) {
*/
private Map sourceDirectoriesWithFilesOrGlobs;
- public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation) {
+ public JavaSourceInfo(Collection srcs, BazelPackage bazelPackage) {
this.srcs = srcs;
- this.bazelPackageLocation = bazelPackageLocation;
+ this.bazelPackageLocation = bazelPackage.getLocation();
this.sharedSourceInfo = null;
+ this.bazelWorkspace = bazelPackage.getBazelWorkspace();
}
/**
@@ -79,19 +96,20 @@ public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation) {
* @param bazelPackageLocation
* @param sharedSourceInfo
*/
- public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation, JavaSourceInfo sharedSourceInfo) {
+ public JavaSourceInfo(Collection srcs, BazelPackage bazelPackage, JavaSourceInfo sharedSourceInfo) {
this.srcs = srcs;
- this.bazelPackageLocation = bazelPackageLocation;
+ this.bazelPackageLocation = bazelPackage.getLocation();
this.sharedSourceInfo = sharedSourceInfo;
+ this.bazelWorkspace = bazelPackage.getBazelWorkspace();
}
@SuppressWarnings("unchecked")
public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
// build an index of all source files and their parent directories (does not need to maintain order)
- Map> sourceEntriesByParentFolder = new HashMap<>();
+ Map> sourceEntriesByParentFolder = new HashMap<>();
// group by potential source roots
- Function groupingByPotentialSourceRoots = fileEntry -> {
+ Function super JavaSourceEntry, IPath> groupingByPotentialSourceRoots = fileEntry -> {
// detect package if necessary
if (fileEntry.detectedPackagePath == null) {
fileEntry.detectedPackagePath = detectPackagePath(fileEntry);
@@ -109,37 +127,44 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
return NOT_FOLLOWING_JAVA_PACKAGE_STRUCTURE;
}
- // build second index of parent for all entries with a potential source root
- // this is needed in order to identify split packages later
- sourceEntriesByParentFolder.putIfAbsent(fileEntry.getPathParent(), new ArrayList<>());
- sourceEntriesByParentFolder.get(fileEntry.getPathParent()).add(fileEntry);
-
- // return the potential source root (relative)
- return potentialSourceDirectoryRoot.makeRelative().removeTrailingSeparator();
+ // return the potential source root
+ return potentialSourceDirectoryRoot;
};
// collect the potential list of source directories
var sourceEntriesBySourceRoot = new LinkedHashMap();
- for (Entry srcEntry : srcs) {
- if (srcEntry instanceof JavaSourceEntry javaSourceFile) {
- var sourceDirectory = groupingByPotentialSourceRoots.apply(javaSourceFile);
- if (!sourceEntriesBySourceRoot.containsKey(sourceDirectory)) {
- var list = new ArrayList<>();
+
+ // define a function for JavaSourceEntry so we can reuse it for sources and extracted srcjars
+ Function javaSourceEntryCollector = javaSourceFile -> {
+ var sourceDirectory = groupingByPotentialSourceRoots.apply(javaSourceFile);
+ if (!sourceEntriesBySourceRoot.containsKey(sourceDirectory)) {
+ var list = new ArrayList<>();
+ list.add(javaSourceFile);
+ sourceEntriesBySourceRoot.put(sourceDirectory, list);
+ } else {
+ var maybeList = sourceEntriesBySourceRoot.get(sourceDirectory);
+ if (maybeList instanceof List list) {
list.add(javaSourceFile);
- sourceEntriesBySourceRoot.put(sourceDirectory, list);
} else {
- var maybeList = sourceEntriesBySourceRoot.get(sourceDirectory);
- if (maybeList instanceof List list) {
- list.add(javaSourceFile);
- } else {
- result.add(
- Status.error(
- format(
- "It looks like source root '%s' is already mapped to a glob pattern. Please split into a separate targets. We cannot support this in the IDE.",
- sourceDirectory)));
- }
+ result.add(
+ Status.error(
+ format(
+ "It looks like source root '%s' is already mapped to a glob pattern. Please split into a separate targets. We cannot support this in the IDE.",
+ sourceDirectory)));
}
+ }
+ return null; // not relevant
+ };
+
+ for (Entry srcEntry : srcs) {
+ if (srcEntry instanceof JavaSourceEntry javaSourceFile) {
+ javaSourceEntryCollector.apply(javaSourceFile);
+
+ // build second index of parent for all entries with a potential source root
+ // this is needed in order to identify split packages (in same directory) later
+ sourceEntriesByParentFolder.putIfAbsent(javaSourceFile.getPathParent(), new ArrayList<>());
+ sourceEntriesByParentFolder.get(javaSourceFile.getPathParent()).add(javaSourceFile);
} else if (srcEntry instanceof GlobEntry globEntry) {
if (sourceEntriesByParentFolder.containsKey(globEntry.getRelativeDirectoryPath())) {
result.add(
@@ -150,20 +175,25 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
} else {
sourceEntriesBySourceRoot.put(globEntry.getRelativeDirectoryPath(), globEntry);
}
+ } else if (srcEntry instanceof LabelEntry labelEntry) {
+ var bazelTarget = bazelWorkspace.getBazelTarget(labelEntry.getLabel());
+ var srcJars = bazelTarget.getRuleOutput()
+ .stream()
+ .filter(p -> "srcjar".equals(p.getFileExtension()))
+ .collect(toList());
+ for (IPath srcjar : srcJars) {
+ var srcjarFolder = extractSrcJar(bazelTarget, srcjar);
+ collectJavaSourcesInFolder(srcjarFolder).forEach(javaSourceEntryCollector::apply);
+ }
} else {
- // check if the source has label dependencies
- result.add(
- Status.warning(
- format(
- "Found source label reference '%s'. The project may not be fully supported in the IDE.",
- srcEntry)));
+ throw new CoreException(Status.error(format("Unexpected source '%s'!", srcEntry)));
}
}
// discover folders that contain more .java files then declared in srcs
// (this is a strong split-package indication)
Set potentialSplitPackageOrSubsetFolders = new HashSet<>();
- for (Map.Entry> entry : sourceEntriesByParentFolder.entrySet()) {
+ for (Map.Entry> entry : sourceEntriesByParentFolder.entrySet()) {
var potentialSourceRoot = entry.getKey();
if (isContainedInSharedSourceDirectories(potentialSourceRoot)) {
// don't check for split packages for stuff covered in shared sources already
@@ -208,6 +238,10 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
// don't check for split packages for stuff covered in shared sources already
continue;
}
+ if (potentialSourceRoot.isAbsolute()) {
+ // don't check for split packages in srcjars (generated code)
+ continue;
+ }
var potentialSourceRootPath = bazelPackageLocation.append(potentialSourceRoot).toPath();
try {
@@ -231,7 +265,8 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
}
}
- // don't issue split packages warning nfor stuff covered in shared sources already
+ // don't issue split packages warning for stuff covered in shared sources already
+ // (test code is allowed to have same package)
if ((sharedSourceInfo != null) && sharedSourceInfo.hasSourceDirectories()) {
potentialSplitPackageOrSubsetFolders.removeIf(this::isContainedInSharedSourceDirectories);
}
@@ -257,6 +292,25 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException {
}
}
+ private Collection collectJavaSourcesInFolder(IPath directory) throws CoreException {
+ try {
+ List result = new ArrayList<>();
+ walkFileTree(directory.toPath(), new SimpleFileVisitor<>() {
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ var path = IPath.fromPath(file);
+ if ("java".equals(path.getFileExtension())) {
+ result.add(new JavaSrcJarEntry(path.removeFirstSegments(directory.segmentCount()), directory));
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ return result;
+ } catch (IOException e) {
+ throw new CoreException(
+ Status.error(format("Unable to scan directory '%s' for .java source files.", directory), e));
+ }
+ }
+
private IPath detectPackagePath(JavaSourceEntry fileEntry) {
// we inspect at most one file per directory (anything else is too weird to support)
var previouslyDetectedPackagePath = detectedPackagePathsByFileEntryPathParent.get(fileEntry.getPathParent());
@@ -265,7 +319,7 @@ private IPath detectPackagePath(JavaSourceEntry fileEntry) {
}
// assume empty by default
- IPath packagePath = Path.EMPTY;
+ var packagePath = IPath.EMPTY;
var packageName = readPackageName(fileEntry);
if (packageName.length() > 0) {
var packageNameSegments = new StringTokenizer(new String(packageName), ".");
@@ -280,6 +334,50 @@ private IPath detectPackagePath(JavaSourceEntry fileEntry) {
return packagePath;
}
+ /**
+ * Extract the source jar (typically found in the bazel-bin directory of the package) into a directory for
+ * consumption as source folder in an Eclipse project.
+ *
+ * The srcjar will be extracted into a directory inside bazel-bin.
+ *
+ *
+ * @param bazelTarget
+ * the target producing the source jar
+ * @param srcjar
+ * the path to the source jar
+ * @return absolute file system path to the directory containing the extracted sources
+ * @throws CoreException
+ */
+ private IPath extractSrcJar(BazelTarget bazelTarget, IPath srcjar) throws CoreException {
+ var jarFile = bazelWorkspace.getBazelBinLocation()
+ .append(bazelTarget.getBazelPackage().getWorkspaceRelativePath())
+ .append(srcjar);
+ var targetDirectory = jarFile.removeLastSegments(1).append("_eclipse").append(srcjar.lastSegment());
+
+ // TODO: need a more sensitive delta/diff (including removal) for Eclipse resource refresh
+
+ var destination = targetDirectory.toPath();
+ try (var archive = new ZipFile(jarFile.toFile())) {
+ // sort entries by name to always create folders first
+ List extends ZipEntry> entries =
+ archive.stream().sorted(Comparator.comparing(ZipEntry::getName)).collect(toList());
+ for (ZipEntry entry : entries) {
+ var entryDest = destination.resolve(entry.getName());
+ if (entry.isDirectory()) {
+ createDirectories(entryDest);
+ } else {
+ try (var is = archive.getInputStream(entry)) {
+ copy(is, entryDest, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new CoreException(Status.error(format("Error extracting srcjar '%s'", srcjar), e));
+ }
+
+ return targetDirectory;
+ }
+
public IPath getBazelPackageLocation() {
return bazelPackageLocation;
}
@@ -330,7 +428,7 @@ public IPath[] getExclutionPatternsForSourceDirectory(IPath sourceDirectory) {
if (excludePatterns != null) {
var exclusionPatterns = new IPath[excludePatterns.size()];
for (var i = 0; i < exclusionPatterns.length; i++) {
- exclusionPatterns[i] = Path.forPosix(excludePatterns.get(i));
+ exclusionPatterns[i] = IPath.forPosix(excludePatterns.get(i));
}
return exclusionPatterns;
}
@@ -355,7 +453,7 @@ public IPath[] getInclusionPatternsForSourceDirectory(IPath sourceDirectory) {
if (includePatterns != null) {
var exclusionPatterns = new IPath[includePatterns.size()];
for (var i = 0; i < exclusionPatterns.length; i++) {
- exclusionPatterns[i] = Path.forPosix(includePatterns.get(i));
+ exclusionPatterns[i] = IPath.forPosix(includePatterns.get(i));
}
return exclusionPatterns;
}
@@ -401,6 +499,29 @@ private boolean isContainedInSharedSourceDirectories(IPath potentialSourceRoot)
|| sharedSourceDirectories.stream().anyMatch(p -> p.isPrefixOf(potentialSourceRoot));
}
+ /**
+ * Performs a check of all entries discovered for a source directory to match a given predicate.
+ *
+ * @param sourceDirectory
+ * the source directory (must be contained in {@link #getSourceDirectories()})
+ * @param predicate
+ * the predicate that all entries for the specified source directory must match
+ * @return true
if all match, false
otherwise
+ */
+ public boolean matchAllSourceDirectoryEntries(IPath sourceDirectory, Predicate super Entry> predicate) {
+ var fileOrGlob = requireNonNull(
+ requireNonNull(sourceDirectoriesWithFilesOrGlobs, "no source directories discovered").get(sourceDirectory),
+ () -> format("source directory '%s' unknown", sourceDirectory));
+ if (fileOrGlob instanceof GlobEntry globEntry) {
+ return predicate.test(globEntry);
+ }
+ if (fileOrGlob instanceof List> listOfEntries) {
+ // the case is save assuming no programming mistakes in this class
+ return listOfEntries.stream().map(JavaSourceEntry.class::cast).allMatch(predicate);
+ }
+ return false;
+ }
+
@SuppressWarnings("deprecation") // use of TokenNameIdentifier is ok here
private String readPackageName(JavaSourceEntry fileEntry) {
var packageName = new StringBuilder();
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java
new file mode 100644
index 00000000..2ffc4e55
--- /dev/null
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java
@@ -0,0 +1,70 @@
+/*-
+ * Copyright (c) 2023 Salesforce and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Salesforce - adapted from M2E, JDT or other Eclipse project
+ */
+package com.salesforce.bazel.eclipse.core.model.discovery.projects;
+
+import org.eclipse.core.runtime.IPath;
+
+/**
+ * An entry inside a .srcjar
.
+ *
+ * In contrast to a standard {@link JavaSourceEntry} this specialization overrides the contract of
+ * {@link #getPotentialSourceDirectoryRoot()} to be an absolute path to a directory somewhere on the file system.
+ * Additional, the {@link #getBazelPackageLocation()} is also not really a Bazel package but the directory of the
+ * explosed srcjar.
+ *
+ */
+public class JavaSrcJarEntry extends JavaSourceEntry {
+
+ public JavaSrcJarEntry(IPath relativePath, IPath srcJarDirectory) {
+ super(relativePath, srcJarDirectory);
+ }
+
+ /**
+ * {@retun the location to the exploded srcjar}
+ */
+ @Override
+ public IPath getBazelPackageLocation() {
+ return super.getBazelPackageLocation();
+ }
+
+ /**
+ * @return first few segments of {@link #getPathParent()} (within {@link #getSrcJarDirectoryLocation()}) which could
+ * be the source directory, or null
if unlikely
+ */
+ @Override
+ public IPath getPotentialSourceDirectoryRoot() {
+ var detectedPackagePath = getDetectedPackagePath();
+
+ // note, we check the full path because we *want* to identify files from targets defined within a Java package
+ var absolutePathToJavaFileDirectory = getSrcJarDirectoryLocation().append(getPathParent());
+ if (endsWith(absolutePathToJavaFileDirectory, detectedPackagePath)) {
+ return absolutePathToJavaFileDirectory.removeLastSegments(detectedPackagePath.segmentCount());
+ }
+
+ return getSrcJarDirectoryLocation(); // assume srcjar directory
+ }
+
+ /**
+ * {@retun the location to the exploded srcjar}
+ */
+ public IPath getSrcJarDirectoryLocation() {
+ // the package location is the src jar directory
+ return getBazelPackageLocation();
+ }
+
+ @Override
+ public boolean isExternalOrGenerated() {
+ return true;
+ }
+}
diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java
index 0b3558b2..e2bbbd24 100644
--- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java
+++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java
@@ -30,6 +30,13 @@ public boolean equals(Object obj) {
return Objects.equals(label, other.label);
}
+ /**
+ * @return the label
+ */
+ public BazelLabel getLabel() {
+ return label;
+ }
+
@Override
public int hashCode() {
return Objects.hash(label);