Skip to content

Commit

Permalink
Merge pull request apache#7646 from sdedic/gradle/loadOptions
Browse files Browse the repository at this point in the history
LoadOptions introduced to control project loading.
  • Loading branch information
sdedic authored Aug 27, 2024
2 parents 99d110c + de5412c commit 3182e8e
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 58 deletions.
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

0 comments on commit 3182e8e

Please sign in to comment.