Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LoadOptions introduced to control project loading. #7646

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions extide/gradle/apichanges.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ is the proper place.
<!-- ACTUAL CHANGES BEGIN HERE: -->

<changes>
<change id="load-options-lookup">
<api name="general"/>
<summary>LoadOptions object replaces growing number of argumetns to project load APIs. Access to current Lookup added.</summary>
<version major="2" minor="43"/>
<date day="9" month="8" year="2024"/>
<author login="sdedic"/>
<compatibility semantic="compatible" addition="yes" deprecation="yes"/>
<description>
<p>
To avoid growing number of parameters to <a href="@TOP@/org/netbeans/modules/gradle/api/NbGradleProject.html#toQuality-java.lang.String-org.netbeans.modules.gradle.api.NbGradleProject.Quality-boolean-">toQuality()</a>,
a <a href="@TOP@/org/netbeans/modules/gradle/api/NbGradleProject.LoadOptions.html">LoadOptions</a> structure was created that can be used to provide details on
how the project should be loaded.
</p>
<p>
The time the project data was actually loaded is now available using <a href="@TOP@/org/netbeans/modules/gradle/api/NbGradleProject.html#getEvaluateTime--">NbGradleProject.getEvaluateTime()</a>.
The time can be used for timestamp checking against project files.
</p>
<p>
Access to the current metadata information/services snapshot is newly available from <a href="@TOP@/org/netbeans/modules/gradle/api/NbGradleProject.html#curretLookup--">NbGradleProject.currentLookup()</a>.
</p>
</description>
<class package="org.netbeans.modules.gradle.api" name="NbGradleProject"/>
</change>
<change id="gradle-init-javaversion">
<api name="general"/>
<summary>Gradle InitOperation now Supports --java-version and --comments flags</summary>
Expand Down
2 changes: 1 addition & 1 deletion extide/gradle/manifest.mf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ OpenIDE-Module: org.netbeans.modules.gradle/2
OpenIDE-Module-Layer: org/netbeans/modules/gradle/layer.xml
OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/gradle/Bundle.properties
OpenIDE-Module-Java-Dependencies: Java > 17
OpenIDE-Module-Specification-Version: 2.42
OpenIDE-Module-Specification-Version: 2.43
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@
*/
package org.netbeans.modules.gradle;

import org.netbeans.modules.gradle.api.NbGradleProject;
import org.netbeans.modules.gradle.api.NbGradleProject.LoadOptions;

/**
*
* @author lkishalmi
*/
public interface GradleProjectLoader {

GradleProject loadProject(NbGradleProject.Quality aim, String descriptionOpt, boolean ignoreCache, boolean interactive, String... args);
public GradleProject loadProject(LoadOptions options, String... args);
}
123 changes: 96 additions & 27 deletions extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -65,6 +67,7 @@
import org.netbeans.api.project.ui.ProjectProblems;
import org.netbeans.modules.gradle.api.GradleBaseProject;
import org.netbeans.modules.gradle.api.GradleReport;
import org.netbeans.modules.gradle.api.NbGradleProject.LoadOptions;
import org.netbeans.modules.gradle.options.GradleExperimentalSettings;
import org.netbeans.spi.project.CacheDirectoryProvider;
import org.netbeans.spi.project.support.LookupProviderSupport;
Expand Down Expand Up @@ -106,6 +109,8 @@ public void run() {
private volatile GradleProject project;
// @GuardedBy(this)
private Quality attemptedQuality;
// @GuardedBy(this)
private Instant timeLoaded;

static {
// invokes static initializer of ModelHandle.class
Expand Down Expand Up @@ -257,6 +262,19 @@ public NbGradleProject getProjectWatcher() {
return watcher;
}

/**
* Time when the gradle project was evaluated.
* @return evaluation time.
*/
public long getEvaluationTime() {
GradleProject gp = this.project;
if (gp == null) {
return -1;
} else {
return gp.getEvaluationTime();
}
}

/**
* Obtains a project attempting at least the defined quality, without setting
* that quality level for subsequent loads. Same as {@link #projectWithQualityTask}
Expand Down Expand Up @@ -308,23 +326,55 @@ public GradleProject projectWithQuality(String desc, Quality aim, boolean intera
* @param force to force load even though the quality does not change.
* @return project instance
*/
@Deprecated
public CompletableFuture<GradleProject> projectWithQualityTask(String desc, Quality aim, boolean interactive, boolean force) {
return projectWithQualityTask(NbGradleProject.loadOptions(aim).
setDescription(desc).
setInteractive(interactive).
setForce(force)
);
}

/**
* Obtains a project attempting at least the defined quality, without setting
* that quality level for subsequent loads. Note that the returned project's quality
* must be checked. If the currently loaded project declares the desired quality,
* no load is performed.
* <p>
* This method should be used in preference to {@link #loadProject()} or {@link #loadOWnProject},
* unless it's desired to force refresh the project contents to the current disk state.
* <div class="nonnormative">
* Implementation note: project reload events are dispatched <b>synchronously</b>
* in the calling thread.
* </div>
* @param options requirements and optiosn for the load operation
* @return Future that completes with the project instance
*/
public CompletableFuture<GradleProject> projectWithQualityTask(LoadOptions options) {
boolean force = options.isForce();
synchronized (this) {
GradleProject c = project;
if (c != null) {
if (!force && c.getQuality().atLeast(aim)) {
if (options.isCheckFiles()) {
Instant newest = newestProjectFiletime();
if (newest.isAfter(Instant.ofEpochMilli(c.getEvaluationTime()))) {
force = true;
}
}
if (!force && c != null) {
if (c.getQuality().atLeast(options.getAim())) {
return CompletableFuture.completedFuture(c);
}
if (!force && attemptedQuality.atLeast(aim)) {
if (attemptedQuality.atLeast(options.getAim())) {
return CompletableFuture.completedFuture(c);
}
}
}
CompletableFuture<GradleProject> toRet = new CompletableFuture<>();
final boolean ff = force;
RELOAD_RP.post(() ->
loadOwnProject0(desc, false, interactive, aim, false, force)
loadOwnProject0(options.setForce(ff), false)
.handle((p, e) -> {
if (e == null) {
if (e == null) {
toRet.complete(p);
} else {
toRet.completeExceptionally(e);
Expand Down Expand Up @@ -388,26 +438,22 @@ CompletableFuture<GradleProject> loadOwnProject(String desc, boolean ignoreCache
private LoadingCF loading;

private static class LoadingCF extends CompletableFuture<GradleProject> {
private final Quality aim;
private final boolean ignoreCache;
private final boolean interactive;
private final LoadOptions options;
private final boolean sync;
private final List<String> args;
private ThreadLocal<GradleProject> ownThreadCompletion = new ThreadLocal<>();

public LoadingCF(Quality aim, boolean ignoreCache, boolean interactive, boolean sync, List<String> args) {
this.aim = aim;
this.ignoreCache = ignoreCache;
this.interactive = interactive;
public LoadingCF(LoadOptions options, boolean sync, List<String> args) {
this.options = options;
this.sync = sync;
this.args = args;
}

public boolean satisifes(LoadingCF other) {
if (aim.worseThan(other.aim)) {
if (options.getAim().worseThan(other.options.getAim())) {
return false;
}
if (ignoreCache != other.ignoreCache || interactive != other.interactive || sync != other.sync) {
if (options.isIgnoreCache() != other.options.isIgnoreCache() || options.isInteractive() != other.options.isInteractive() || sync != other.sync) {
return false;
}
return args.equals(other.args);
Expand Down Expand Up @@ -438,6 +484,17 @@ public GradleProject get() throws InterruptedException, ExecutionException {
}
}

Instant newestProjectFiletime() {
return getGradleFiles().getProjectFiles().stream().map(f -> {
try {
return Files.getLastModifiedTime(f.toPath()).toInstant();
} catch (IOException ex) {
// no op
return Instant.now();
}
}).reduce((a, b) -> a.isAfter(b) ? a : b).orElse(Instant.now());
}

/**
* Loads a project. After load, dispatches reload events. If "sync" is false (= asynchronous), dispatches events
* and does possible fixups in {@link #RELOAD_RP}. The returned future completes only after all the event
Expand All @@ -455,35 +512,51 @@ public GradleProject get() throws InterruptedException, ExecutionException {
* @return Future for the new GradleProject state. See notes about sync/async differences.
*/
/* nonprivate: tests only */CompletableFuture<GradleProject> loadOwnProject0(String desc, boolean ignoreCache, boolean interactive, Quality aim, boolean sync, boolean force, String... args) {
return loadOwnProject0(NbGradleProject.loadOptions(aim).
setDescription(desc).
setIgnoreCache(ignoreCache).
setInteractive(interactive).
setForce(force),
sync, args
);
}

// NOTE: the optional arguments are only used by ActionProviderImpl, to reload project before / after a project action. If there are more users,
// consider to expose the args... in the LoadOptions. Somehow need to solve the effect of different args to the project loaded data, as they may
// differ significantly and replace other-argumented state in the disk cache etc.
CompletableFuture<GradleProject> loadOwnProject0(LoadOptions options, boolean sync, String... args) {
GradleProjectLoader loader = getLookup().lookup(GradleProjectLoader.class);
if (loader == null) {
throw new IllegalStateException("No loader implementation is present!");
}
LoadingCF f = new LoadingCF(aim, ignoreCache, interactive, sync, Arrays.asList(args));
LoadingCF f = new LoadingCF(options, sync, Arrays.asList(args));
synchronized (this) {
if (this.loading != null && this.loading.satisifes(f)) {
if (!force) {
if (!options.isForce()) {
LOG.log(Level.FINER, "Project {2} is already loading to quality {0}, now attempted {1}, returning existing handle", new
Object[] { this.loading.aim, aim, this });
Object[] { this.loading.options.getAim(), options.getAim(), this });
return loading;
}
}
this.loading = f;
}
int s = currentSerial.incrementAndGet();
// do not block during project load.
LOG.log(Level.FINER, "Starting project {2} load, serial {0}, attempted quality {1}", new Object[] { s, aim, this });
GradleProject prj = loader.loadProject(aim, desc, ignoreCache, interactive, args);
LOG.log(Level.FINER, "Starting project {2} load, serial {0}, attempted quality {1}", new Object[] { s, options.getAim(), this });
if (options.isForce()) {
options.setIgnoreCache(true);
}
GradleProject prj = loader.loadProject(options, args);
synchronized (this) {
if (loadedProjectSerial > s && project != null) {
// the load started LATER than this one: return that project, and do not replace anything as this.project is newer
LOG.log(Level.FINER, "Future finished project load, returing {0} throwing away {1}", new Object[] { project, prj });
return CompletableFuture.completedFuture(this.project);
}
loadedProjectSerial = s;
this.attemptedQuality = aim;
this.attemptedQuality = options.getAim();

boolean replace = project == null || force;
boolean replace = project == null || options.isForce();
if (project != null) {
if (prj.getQuality().betterThan(project.getQuality())) {
replace = true;
Expand All @@ -497,7 +570,7 @@ public GradleProject get() throws InterruptedException, ExecutionException {
}
if (!replace) {
// avoid replacing a project when nothing has changed.
LOG.log(Level.FINER, "Current project {1} sufficient for attempted quality {0}", new Object[] { this.project, aim });
LOG.log(Level.FINER, "Current project {1} sufficient for attempted quality {0}", new Object[] { this.project, options.getAim() });
return CompletableFuture.completedFuture(this.project);
}
LOG.log(Level.FINER, "Replacing {0} with {1}, attempted quality {2}", new Object[] { this.project, prj, attemptedQuality });
Expand Down Expand Up @@ -562,11 +635,7 @@ private CompletableFuture<GradleProject> callAccessorReload(LoadingCF f, GradleP
* @return Task representing the reloading process
*/
RequestProcessor.Task forceReloadProject(String reloadReason, boolean interactive, final Quality aim, final String... args) {
return reloadProject(reloadReason, true, interactive, aim, args);
}

private RequestProcessor.Task reloadProject(String desc, final boolean ignoreCache, final boolean interactive, final Quality aim, final String... args) {
return RELOAD_RP.post(() -> loadOwnProject(desc, ignoreCache, interactive, aim, args));
return RELOAD_RP.post(() -> loadOwnProject(reloadReason, true, interactive, aim, args));
}

@Override
Expand Down
Loading
Loading