blob: 0633e2b3a4df65f91d098f496e5830eb177ad6d5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.gradle;
import org.netbeans.modules.gradle.loaders.GradleProjectLoaderImpl;
import org.netbeans.modules.gradle.spi.GradleFiles;
import org.netbeans.modules.gradle.api.NbGradleProject;
import org.netbeans.modules.gradle.api.NbGradleProject.Quality;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.netbeans.api.project.Project;
import org.netbeans.spi.project.ProjectState;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileUtil;
import org.openide.util.*;
import org.openide.util.lookup.Lookups;
import static org.netbeans.modules.gradle.api.NbGradleProject.Quality.*;
import static java.util.logging.Level.*;
import java.util.logging.Logger;
import org.gradle.tooling.ProjectConnection;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.SuppressWarnings;
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.options.GradleExperimentalSettings;
import org.netbeans.spi.project.CacheDirectoryProvider;
import org.netbeans.spi.project.support.LookupProviderSupport;
import org.netbeans.spi.project.ui.ProjectOpenedHook;
import org.netbeans.spi.project.ui.support.UILookupMergerSupport;
import org.openide.util.lookup.ProxyLookup;
/**
*
* @author Laszlo Kishalmi
*/
public final class NbGradleProjectImpl implements Project {
static final Logger LOG = Logger.getLogger(NbGradleProjectImpl.class.getName());
public static final RequestProcessor RELOAD_RP = new RequestProcessor("Gradle project reloading", 1); //NOI18
private final RequestProcessor.Task reloadTask = RELOAD_RP.create(new Runnable() {
@Override
public void run() {
loadOwnProject(null, false, false, aimedQuality);
}
});
private final FileObject projectDir;
private final ProjectState projectState;
private final Lookup lookup;
private final Lookup basicLookup;
private final Lookup completeLookup;
private Updater openedProjectUpdater;
// @GuardedBy(this)
private volatile Quality aimedQuality = FALLBACK;
private final @NonNull NbGradleProject watcher;
@SuppressWarnings("MS_SHOULD_BE_FINAL")
public static WatcherAccessor ACCESSOR = null;
// @GuardedBy(this)
private volatile GradleProject project;
// @GuardedBy(this)
private Quality attemptedQuality;
static {
// invokes static initializer of ModelHandle.class
// that will assign value to the ACCESSOR field above
Class<?> c = NbGradleProject.class;
try {
Class.forName(c.getName(), true, c.getClassLoader());
} catch (ClassNotFoundException ex) {
LOG.log(SEVERE, "very wrong, very wrong, yes indeed", ex);
}
}
private final GradleFiles gradleFiles;
public boolean isGradleProjectLoaded() {
return project != null;
}
public abstract static class WatcherAccessor {
public abstract NbGradleProject createWatcher(NbGradleProjectImpl proj);
public abstract void doFireReload(NbGradleProject watcher);
public abstract void activate(NbGradleProject watcher);
public abstract void passivate(NbGradleProject watcher);
public abstract GradleReport createReport(String errorClass, String location, int line, String message, GradleReport causedBy);
public abstract void setProblems(GradleBaseProject baseProject, Set<GradleReport> problems);
}
@java.lang.SuppressWarnings("LeakingThisInConstructor")
public NbGradleProjectImpl(final FileObject projectDir, ProjectState projectState) {
this.projectDir = projectDir;
this.projectState = projectState;
this.gradleFiles = new GradleFiles(FileUtil.normalizeFile(FileUtil.toFile(projectDir)), true);
lookup = Lookups.proxy(new Lookup.Provider() {
@Override
public Lookup getLookup() {
if (completeLookup == null) {
//not fully initialized constructor
LOG.log(Level.FINE, "Accessing project's lookup before the instance is fully initialized at " + gradleFiles.getBuildScript(), new Exception());
assert basicLookup != null;
return basicLookup;
} else {
return completeLookup;
}
}
});
watcher = ACCESSOR.createWatcher(this);
GradleAuxiliaryConfigImpl aux = new GradleAuxiliaryConfigImpl(projectDir, true);
basicLookup = createBasicLookup(projectState, aux);
completeLookup = LookupProviderSupport.createCompositeLookup(basicLookup, new PluginDependentLookup(watcher));
}
public GradleFiles getGradleFiles() {
return gradleFiles;
}
@Override
public FileObject getProjectDirectory() {
return projectDir;
}
@Override
public Lookup getLookup() {
return lookup;
}
private Lookup createBasicLookup(ProjectState state, GradleAuxiliaryConfigImpl aux) {
return Lookups.fixed(this,
watcher,
new CacheDirProvider(),
aux,
aux.getProblemProvider(),
new GradleAuxiliaryPropertiesImpl(this),
UILookupMergerSupport.createProjectOpenHookMerger(new ProjectOpenedHookImpl()),
UILookupMergerSupport.createProjectProblemsProviderMerger(),
UILookupMergerSupport.createRecommendedTemplatesMerger(),
UILookupMergerSupport.createPrivilegedTemplatesMerger(),
LookupProviderSupport.createSourcesMerger(),
LookupProviderSupport.createSharabilityQueryMerger(),
new GradleProjectLoaderImpl(this),
new GradleProjectErrorNotifications(),
state
);
}
public GradleProject getGradleProject() {
return projectWithQuality(null, EVALUATED, false, false);
}
public void fireProjectReload(boolean wait) {
reloadTask.schedule(0);
if (wait) {
reloadTask.waitFinished();
}
}
void attachAllUpdater() {
synchronized (this) {
if (openedProjectUpdater == null) {
openedProjectUpdater = new Updater((new FileProvider() {
@Override
public Set<File> getFiles() {
GradleFiles gf = getGradleFiles();
Set<File> ret = new LinkedHashSet<>();
for (GradleFiles.Kind kind : GradleFiles.Kind.PROJECT_FILES) {
File f = gf.getFile(kind);
if (f != null) {
ret.add(f);
}
}
return ret;
}
}));
}
}
openedProjectUpdater.attachAll();
}
void detachAllUpdater() {
synchronized (this) {
if (openedProjectUpdater != null) {
openedProjectUpdater.detachAll();
}
}
}
synchronized void dumpProject() {
loading = null;
project = null;
attemptedQuality = null;
loadedProjectSerial = 0;
aimedQuality = FALLBACK;
}
public Quality getAimedQuality() {
return aimedQuality;
}
public NbGradleProject getProjectWatcher() {
return watcher;
}
/**
* Obtains a project attempting at least the defined quality, without setting
* that quality level for subsequent loads. Same as {@link #projectWithQualityTask}
* but synchronous (runs in this thread).
* @param desc optional description for the loading process, can be {@code null}.
* @param aim aimed quality
* @param interactive true, if user messages/confirmations can be displayed
* @param force to force load even though the quality does not change.
* @return project instance
*/
public GradleProject projectWithQuality(String desc, Quality aim, boolean interactive, boolean force) {
synchronized (this) {
GradleProject c = project;
if (c != null) {
if (c.getQuality().atLeast(aim)) {
LOG.log(Level.FINER, "Asked for {0}, got {1} already: ", new Object[] { aim, c.getQuality() });
return c;
}
if (!force && attemptedQuality.atLeast(aim)) {
LOG.log(Level.FINER, "Attempted quality was {0}, ignoring request to get {1}", new Object[] { attemptedQuality, aim });
return c;
}
}
}
try {
return loadOwnProject0(desc, false, interactive, aim, true, force).get();
} catch (InterruptedException | ExecutionException ex) {
// should not happen, the event dispatch + potential issues happen
// synchronously
return null;
}
}
/**
* 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 desc optional description for the loading process, can be {@code null}.
* @param aim aimed quality
* @param interactive true, if user messages/confirmations can be displayed
* @param force to force load even though the quality does not change.
* @return project instance
*/
public CompletableFuture<GradleProject> projectWithQualityTask(String desc, Quality aim, boolean interactive, boolean force) {
synchronized (this) {
GradleProject c = project;
if (c != null) {
if (c.getQuality().atLeast(aim)) {
return CompletableFuture.completedFuture(c);
}
if (!force && attemptedQuality.atLeast(aim)) {
return CompletableFuture.completedFuture(c);
}
}
}
CompletableFuture<GradleProject> toRet = new CompletableFuture<>();
RELOAD_RP.post(() ->
loadOwnProject0(desc, false, interactive, aim, false, force)
.handle((p, e) -> {
if (e == null) {
toRet.complete(p);
} else {
toRet.completeExceptionally(e);
}
return null;
})
);
return toRet;
}
/**
* Changes the aimed project's quality. Reloads the project, if the
* current quality is lower. If the aimed quality is better than {@link #FALLBACK}
* build script files are monitored and the project is eventually reloaded when a
* change is detected.
* <div class="nonnormative">
* Implementation note: project reload events are dispatched <b>synchronously</b>
* in the calling thread.
* </div>
* @param aim the aimed quality.
*/
public void setAimedQuality(Quality aim) {
// Locked so that watcher is active/inactive always in sync with aimedQuality
// FIXME: in the case the project _actually_ loads with a LOWER quality,
// the Watcher is still active.
synchronized (this) {
if ((aimedQuality == FALLBACK) && aim.betterThan(FALLBACK)) {
ACCESSOR.activate(watcher);
}
if ((aim == FALLBACK) && aimedQuality.betterThan(FALLBACK)) {
ACCESSOR.passivate(watcher);
}
this.aimedQuality = aim;
if (!((project == null) || project.getQuality().worseThan(aim))) {
return;
}
}
loadOwnProject0(null, false, false, aimedQuality, true, false);
}
/**
* Increasing stamp of load attempts.
*/
private final AtomicInteger currentSerial = new AtomicInteger();
/**
* Stamp of the currently loaded project.
*/
// @GuardedBy(this)
private int loadedProjectSerial;
CompletableFuture<GradleProject> loadOwnProject(String desc, boolean ignoreCache, boolean interactive, Quality aim, String... args) {
return loadOwnProject0(desc, ignoreCache, interactive, aim, false, true, args);
}
/**
* Future that is present during project load. Other load requests can be satisfied by this Future if they do not contain
* the 'force' flag.
*/
// @GuardedBy(this)
private LoadingCF loading;
private static class LoadingCF extends CompletableFuture<GradleProject> {
private final Quality aim;
private final boolean ignoreCache;
private final boolean interactive;
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;
this.sync = sync;
this.args = args;
}
public boolean satisifes(LoadingCF other) {
if (aim.worseThan(other.aim)) {
return false;
}
if (ignoreCache != other.ignoreCache || interactive != other.interactive || sync != other.sync) {
return false;
}
return args.equals(other.args);
}
@Override
public GradleProject getNow(GradleProject valueIfAbsent) {
GradleProject p = ownThreadCompletion.get();
return p != null ? p : super.getNow(valueIfAbsent);
}
@Override
public GradleProject join() {
GradleProject p = ownThreadCompletion.get();
return p != null ? p : super.join();
}
@Override
public GradleProject get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
GradleProject p = ownThreadCompletion.get();
return p != null ? p : super.get(timeout, unit);
}
@Override
public GradleProject get() throws InterruptedException, ExecutionException {
GradleProject p = ownThreadCompletion.get();
return p != null ? p : super.get();
}
}
/**
* 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
* listeners in RELOAD_RP complete.
* <p/>
* If "sync" is true, everything happens synchronously and the returned Future is always completed. Errors from the
* project load and reload listeners are thrown from this method.
*
* @param desc description of the reload, can be {@code null}.
* @param ignoreCache true to ignore cached data
* @param interactive true, if displaying UI is permitted (i.e. security questions)
* @param aim aimed quality
* @param sync true to run everything synchronously
* @param args optional arguments for the reload
* @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) {
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));
synchronized (this) {
if (this.loading != null && this.loading.satisifes(f)) {
if (!force) {
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 });
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);
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;
boolean replace = project == null || force;
if (project != null) {
if (prj.getQuality().betterThan(project.getQuality())) {
replace = true;
} else if (
project.getQuality().equals(prj.getQuality()) &&
!project.getProblems().equals(prj.getProblems()) &&
!prj.getProblems().isEmpty()) {
// exception: if the new project is the same quality fallback, but contains (different) problem info, use it
replace = true;
}
}
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 });
return CompletableFuture.completedFuture(this.project);
}
LOG.log(Level.FINER, "Replacing {0} with {1}, attempted quality {2}", new Object[] { this.project, prj, attemptedQuality });
this.project = prj;
}
// notify the project has been changed.
if (sync || RELOAD_RP.isRequestProcessorThread()) {
synchronized (this) {
if (this.loading == f) {
this.loading = null;
}
}
LOG.log(Level.FINER, "Firing changes/reload synchronously");
try {
f.ownThreadCompletion.set(prj);
ACCESSOR.doFireReload(watcher);
} finally {
f.ownThreadCompletion.remove();
f.complete(prj);
}
return f;
} else {
LOG.log(Level.FINER, "Firing changes/reload in RP");
RELOAD_RP.post(() -> callAccessorReload(f, prj));
return f;
}
}
private CompletableFuture<GradleProject> callAccessorReload(LoadingCF f, GradleProject prj) {
try {
synchronized (this) {
if (this.loading == f) {
this.loading = null;
}
}
try {
f.ownThreadCompletion.set(prj);
ACCESSOR.doFireReload(watcher);
} finally {
f.ownThreadCompletion.remove();
f.complete(prj);
}
} catch (ThreadDeath t) {
throw t;
} catch (RuntimeException | Error ex) {
f.completeExceptionally(ex);
throw ex;
} catch (Throwable t) {
f.completeExceptionally(t);
LOG.log(Level.WARNING, "Unexpected exception from project listeners", t);
}
return f;
}
/**
* Forces project reload with the given quality, ignoring caches. The 'aim' quality does not become the {@link #getAimedQuality() aimed one}, just
* forces appropriate load scope.
* @param reloadReason optional reason for the reload operation
* @param interactive true, if the originating action is interactive and UI can be displayed
* @param aim the aimed quality
* @param args optional argument for reload
* @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));
}
@Override
public int hashCode() {
return gradleFiles.hashCode() * 3;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Project) {
NbGradleProjectImpl impl = ((Project) obj).getLookup().lookup(NbGradleProjectImpl.class);
if (impl != null) {
return getGradleFiles().equals(impl.getGradleFiles());
}
}
return false;
}
@Override
public String toString() {
// synchronized was here, but is it may be called during Logger.log(), it may completely cause a deadlock
// between LogHandler (that calls this toString() and other thread that locked this and tries to use Logger).
GradleProject p = project;
if (p != null) {
return "Gradle: " + p.getBaseProject().getName() + "[" + p.getQuality() + "]";
} else {
return "Unloaded Gradle Project: " + gradleFiles.toString();
}
}
final RequestProcessor GRADLE_PRIMING_RP = new RequestProcessor("gradle-project-resolver", 1); //NOI18N
// @GuardedBy(this)
private CompletableFuture<GradleProject> primingBuild;
boolean isProjectPrimingRequired() {
return getPrimedProject() == null;
}
GradleProject getPrimedProject() {
GradleProject gp = projectWithQuality(null, EVALUATED, false, false);
return gp.getQuality().betterThan(EVALUATED) ? gp : null;
}
/**
* The core implementation is tied to project quality itself, so it is extracted here from
* {@link GradleProjectProblemProvider}.
* <p>
* <b>Note: Priming build makes the project trusted</b>
*
* @return future that produces the result.
*/
@NbBundle.Messages({
"# {0} - project name",
"ACT_PrimingProject=Preparing project {0}"
})
CompletableFuture<GradleProject> primeProject() {
CompletableFuture<GradleProject> ret;
synchronized (this) {
if (primingBuild != null && !primingBuild.isDone()) {
// avoid priming twice, piggyback on the old one
LOG.log(Level.FINER, "Priming build runs for {0}: {1}", new Object[] { this, primingBuild });
return primingBuild;
}
ret = new CompletableFuture<>();
primingBuild = ret;
}
LOG.log(Level.FINER, "Submitting priming build runs for {0}: {1}", new Object[] { this, ret });
GRADLE_PRIMING_RP.submit(() -> {
GradleProject gradleProject = null;
try {
// this was explicitly invoked as project action, or problem resolution. Same level as
// Build project, so trust the project.
ProjectTrust.getDefault().trustProject(this, true);
gradleProject = getPrimedProject();
if (gradleProject != null) {
ret.complete(gradleProject);
return;
}
// get at least something to extract project name from:
GradleProject fallback = projectWithQuality(null, FALLBACK, false, false);
loadOwnProject0(Bundle.ACT_PrimingProject(fallback.getBaseProject().getName()), true, true, FULL_ONLINE, false, true).
// wait until after reload event is fired.
thenApply(p -> ret.complete(p)).
exceptionally((e) -> ret.completeExceptionally(e));
LOG.log(Level.FINER, "Priming finished, reloaded {0}: {1}", gradleProject);
} catch (Throwable t) {
LOG.log(Level.FINER, t, () -> String.format("Priming errored for %s", project));
ret.completeExceptionally(t);
if (t instanceof ThreadDeath) {
throw t;
}
}
});
return ret;
}
public static File getCacheDir(GradleFiles gf) {
return getCacheDir(gf.getRootDir(), gf.getProjectDir());
}
public static File getCacheDir(GradleProject gp) {
GradleBaseProject base = gp.getBaseProject();
return getCacheDir(base.getRootDir(), base.getProjectDir());
}
private static File getCacheDir(File rootDir, File projectDir) {
int code = Math.abs(projectDir.getAbsolutePath().hashCode());
String dirName = projectDir.getName() + "-" + code; //NOI18N
File dir = new File(rootDir, ".gradle/nb-cache/" + dirName); //NOI18N
return dir;
}
private class ProjectOpenedHookImpl extends ProjectOpenedHook {
@Override
protected void projectOpened() {
Runnable open = () -> {
setAimedQuality(FULL);
attachAllUpdater();
if (ProjectProblems.isBroken(NbGradleProjectImpl.this)) {
ProjectProblems.showAlert(NbGradleProjectImpl.this);
}
};
if (GradleExperimentalSettings.getDefault().isOpenLazy()) {
RELOAD_RP.post(open, 100);
} else {
open.run();
}
}
@Override
protected void projectClosed() {
setAimedQuality(Quality.FALLBACK);
detachAllUpdater();
dumpProject();
getLookup().lookup(ProjectConnection.class).close();
getLookup().lookup(GradleProjectErrorNotifications.class).clear();
}
}
interface FileProvider {
Set<File> getFiles();
}
private class CacheDirProvider implements CacheDirectoryProvider {
@Override
public FileObject getCacheDirectory() throws IOException {
return FileUtil.createFolder(getCacheDir(gradleFiles));
}
}
private static class PluginDependentLookup extends ProxyLookup implements PropertyChangeListener {
private static final String NB_ROOT_PLUGIN = "root"; //NOI18N
private final WeakReference<NbGradleProject> watcherRef;
// @GuardedBy(this)
private Map<String, Lookup> pluginLookups = Collections.emptyMap();
// @GuardedBy(this)
private List<String> pluginOrder = Collections.emptyList();
@java.lang.SuppressWarnings("LeakingThisInConstructor")
public PluginDependentLookup(NbGradleProject watcher) {
// PENDING: is this ref really necessary ? If we added a strong PropertyChangeListener
// to the `watcher', it would keep this Lookup alive as long as the watcher itself is alive
watcherRef = new WeakReference<>(watcher);
check();
watcher.addPropertyChangeListener(WeakListeners.propertyChange(this, watcher));
}
/**
* Path for the default Gradle project lookup contents
*/
private static final String GRADLE_DEFAULT_LOOKUP = "Projects/" + NbGradleProject.GRADLE_PROJECT_TYPE + "/Lookup";
/**
* Path for the default Gradle project lookup contents
*/
private static final String GRADLE_ANY_PLUGIN_LOOKUP = "Projects/" + NbGradleProject.GRADLE_PLUGIN_TYPE + "/_any/Lookup";
/**
* Root for plugin lookup registrations. Individual Plugins must register in "&lt;GRADLE_PLUGINS_ROOT>/&lt;plugin-id>/Lookup".
*/
private static final String GRADLE_PLUGINS_ROOT = "Projects/" + NbGradleProject.GRADLE_PLUGIN_TYPE;
private void check() {
NbGradleProject watcher = watcherRef.get();
if (watcher == null) {
// shortcut
return;
}
List<String> orderedPaths = new ArrayList<>();
orderedPaths.add(GRADLE_DEFAULT_LOOKUP);
if (watcher.isGradleProjectLoaded()) {
GradleBaseProject prj = watcher.projectLookup(GradleBaseProject.class);
// plugins are unordered initially
Set<String> currentPlugins = new HashSet<>(prj.getPlugins());
if (prj.isRoot()) {
currentPlugins.add(NB_ROOT_PLUGIN);
}
FileObject pluginRoot = FileUtil.getConfigFile(GRADLE_PLUGINS_ROOT);
if (pluginRoot != null) {
// iterate in the file-system order to get at least SOME defined default order (according to module dependencies)
for (FileObject pl : pluginRoot.getChildren()) {
if (currentPlugins.remove(pl.getName())) {
orderedPaths.add(GRADLE_PLUGINS_ROOT + "/" + pl.getName() + "/Lookup");
}
}
}
// order the rest of plugins alphabetically
List<String> remaining = new ArrayList<>(currentPlugins);
Collections.sort(remaining);
remaining.forEach(r -> orderedPaths.add(GRADLE_PLUGINS_ROOT + "/" + r + "/Lookup"));
}
orderedPaths.add(GRADLE_ANY_PLUGIN_LOOKUP);
Map<String, Lookup> newLookups;
Map<String, Lookup> prevLookups;
synchronized (this) {
if (this.pluginOrder.equals(orderedPaths)) {
return;
}
prevLookups = this.pluginLookups;
}
newLookups = new HashMap<>(prevLookups);
newLookups.keySet().retainAll(orderedPaths);
Lookup[] lkps = new Lookup[orderedPaths.size()];
int i = 0;
for (String s : orderedPaths) {
Lookup l = newLookups.get(s);
if (l == null) {
newLookups.put(s, l = Lookups.forPath(s));
}
lkps[i++] = l;
}
synchronized (this) {
// double check: if a parallel execution took the pluginLookups (= later than us) and finished
// before -> more recent data.
if (pluginLookups != prevLookups) {
return;
}
pluginLookups = newLookups;
pluginOrder = orderedPaths;
}
setLookups(lkps);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (NbGradleProject.PROP_PROJECT_INFO.equals(evt.getPropertyName())) {
// PENDING: maybe the Lookup change should synchronize into RELOAD_RP
check();
}
}
}
private class Updater implements FileChangeListener {
final FileProvider fileProvider;
Set<File> filesToWatch;
long lastEventTime = 0;
Updater(FileProvider fp) {
fileProvider = fp;
}
@Override
public void fileFolderCreated(FileEvent fe) {
}
@Override
public void fileDataCreated(FileEvent fe) {
if (lastEventTime < fe.getTime()) {
lastEventTime = System.currentTimeMillis();
fireProjectReload(false);
}
}
@Override
public void fileChanged(FileEvent fe) {
if (lastEventTime < fe.getTime()) {
lastEventTime = System.currentTimeMillis();
fireProjectReload(false);
}
}
@Override
public void fileDeleted(FileEvent fe) {
lastEventTime = System.currentTimeMillis();
fireProjectReload(false);
}
@Override
public void fileRenamed(FileRenameEvent fe) {
}
@Override
public void fileAttributeChanged(FileAttributeEvent fe) {
}
synchronized void attachAll() {
filesToWatch = fileProvider.getFiles();
if (filesToWatch != null) {
for (File f : filesToWatch) {
if (f != null) {
try {
FileUtil.addFileChangeListener(this, f);
} catch (IllegalArgumentException ex) {
assert false : "Project opened twice in a row";
}
}
}
}
}
synchronized void detachAll() {
if (filesToWatch != null) {
for (File f : filesToWatch) {
if (f != null) {
try {
FileUtil.removeFileChangeListener(this, f);
} catch (IllegalArgumentException ex) {
assert false : "Project closed twice in a row";
}
}
}
}
}
}
}