All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.gradle.NbGradleProjectImpl Maven / Gradle / Ivy

The newest version!
/*
 * 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.nio.file.Files;
import java.time.Instant;
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.api.NbGradleProject.LoadOptions;
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;
    // @GuardedBy(this)
    private Instant timeLoaded;

    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(GradleReport.Severity severity, String errorClass, String location, int line, String message, 
                GradleReport causedBy, String[] traceLines);

        public abstract void setProblems(GradleBaseProject baseProject, Set 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 getFiles() {
                        GradleFiles gf = getGradleFiles();
                        Set 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;
    }
    
    /**
     * 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}
     * 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 (! force && 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.
     * 

* 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. *

* Implementation note: project reload events are dispatched synchronously * in the calling 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 */ @Deprecated public CompletableFuture 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. *

* 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. *

* Implementation note: project reload events are dispatched synchronously * in the calling thread. *
* @param options requirements and optiosn for the load operation * @return Future that completes with the project instance */ public CompletableFuture projectWithQualityTask(LoadOptions options) { boolean force = options.isForce(); synchronized (this) { GradleProject c = project; 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 (attemptedQuality.atLeast(options.getAim())) { return CompletableFuture.completedFuture(c); } } } CompletableFuture toRet = new CompletableFuture<>(); final boolean ff = force; RELOAD_RP.post(() -> loadOwnProject0(options.setForce(ff), false) .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. *
* Implementation note: project reload events are dispatched synchronously * in the calling thread. *
* @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 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 { private final LoadOptions options; private final boolean sync; private final List args; private ThreadLocal ownThreadCompletion = new ThreadLocal<>(); public LoadingCF(LoadOptions options, boolean sync, List args) { this.options = options; this.sync = sync; this.args = args; } public boolean satisifes(LoadingCF other) { if (options.getAim().worseThan(other.options.getAim())) { return false; } if (options.isIgnoreCache() != other.options.isIgnoreCache() || options.isInteractive() != other.options.isInteractive() || 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(); } } 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 * listeners in RELOAD_RP complete. *

* 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 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 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(options, sync, Arrays.asList(args)); synchronized (this) { if (this.loading != null && this.loading.satisifes(f)) { 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.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, 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 = options.getAim(); boolean replace = project == null || options.isForce(); 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, options.getAim() }); 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 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 RELOAD_RP.post(() -> loadOwnProject(reloadReason, true, 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 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}. *

* Note: Priming build makes the project trusted * * @return future that produces the result. */ @NbBundle.Messages({ "# {0} - project name", "ACT_PrimingProject=Preparing project {0}" }) CompletableFuture primeProject() { CompletableFuture 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 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 watcherRef; // @GuardedBy(this) private Map pluginLookups = Collections.emptyMap(); // @GuardedBy(this) private List 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 "<GRADLE_PLUGINS_ROOT>/<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 orderedPaths = new ArrayList<>(); orderedPaths.add(GRADLE_DEFAULT_LOOKUP); if (watcher.isGradleProjectLoaded()) { GradleBaseProject prj = watcher.projectLookup(GradleBaseProject.class); // plugins are unordered initially Set 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 remaining = new ArrayList<>(currentPlugins); Collections.sort(remaining); remaining.forEach(r -> orderedPaths.add(GRADLE_PLUGINS_ROOT + "/" + r + "/Lookup")); } orderedPaths.add(GRADLE_ANY_PLUGIN_LOOKUP); Map newLookups; Map 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 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"; } } } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy