blob: 0dcd6d0c49d304a498aa94227a3a7d9740e958d9 [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.spi.GradleFiles;
import org.netbeans.modules.gradle.api.NbGradleProject;
import org.netbeans.modules.gradle.api.NbGradleProject.Quality;
import org.netbeans.modules.gradle.spi.GradleSettings;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
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.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.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 {
private 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() {
project = loadProject();
ACCESSOR.doFireReload(watcher);
}
});
private final FileObject projectDir;
private final ProjectState projectState;
private final Lookup lookup;
private final Lookup basicLookup;
private final Lookup completeLookup;
private Updater openedProjectUpdater;
private Quality aimedQuality = FALLBACK;
private final @NonNull NbGradleProject watcher;
@SuppressWarnings("MS_SHOULD_BE_FINAL")
public static WatcherAccessor ACCESSOR = null;
GradleProject project;
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 static abstract 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);
}
@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),
new GradleSharabilityQueryImpl(this),
UILookupMergerSupport.createProjectOpenHookMerger(new ProjectOpenedHookImpl()),
UILookupMergerSupport.createProjectProblemsProviderMerger(),
UILookupMergerSupport.createRecommendedTemplatesMerger(),
UILookupMergerSupport.createPrivilegedTemplatesMerger(),
state
);
}
public GradleProject getGradleProject() {
if (project == null) {
project = loadProject();
}
return project;
}
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) {
ret.add(gf.getFile(kind));
}
return ret;
}
}));
}
}
openedProjectUpdater.attachAll();
}
void detachAllUpdater() {
synchronized (this) {
if (openedProjectUpdater != null) {
openedProjectUpdater.detachAll();
}
}
}
void dumpProject() {
project = null;
}
public Quality getAimedQuality() {
return aimedQuality;
}
public NbGradleProject getProjectWatcher() {
return watcher;
}
public void setAimedQuality(Quality aim) {
//TODO: Shall we do some locking here?
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)) {
project = loadProject();
ACCESSOR.doFireReload(watcher);
}
}
private GradleProject loadProject() {
return loadProject(false, aimedQuality);
}
private GradleProject loadProject(boolean ignoreCache, Quality aim, String... args) {
GradleProject prj = GradleProjectCache.loadProject(this, aim, ignoreCache, false, args);
return prj;
}
void reloadProject(final boolean ignoreCache, final Quality aim, final String... args) {
RELOAD_RP.post(() -> {
project = loadProject(ignoreCache, aim, args);
ACCESSOR.doFireReload(watcher);
});
}
@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() {
if (isGradleProjectLoaded()) {
return "Gradle: " + project.getBaseProject().getName() + "[" + project.getQuality() + "]";
} else {
return "Unloaded Gradle Project: " + gradleFiles.toString();
}
}
private class ProjectOpenedHookImpl extends ProjectOpenedHook {
@Override
protected void projectOpened() {
Runnable open = () -> {
setAimedQuality(FULL);
attachAllUpdater();
if (ProjectProblems.isBroken(NbGradleProjectImpl.this)) {
ProjectProblems.showAlert(NbGradleProjectImpl.this);
}
};
if (GradleSettings.getDefault().isOpenLazy()) {
RELOAD_RP.post(open, 100);
} else {
open.run();
}
}
@Override
protected void projectClosed() {
setAimedQuality(Quality.FALLBACK);
detachAllUpdater();
dumpProject();
}
}
interface FileProvider {
Set<File> getFiles();
}
private class CacheDirProvider implements CacheDirectoryProvider {
@Override
public FileObject getCacheDirectory() throws IOException {
return FileUtil.createFolder(GradleProjectCache.getCacheDir(gradleFiles));
}
}
private static class PluginDependentLookup extends ProxyLookup implements PropertyChangeListener {
private static final String NB_GENERAL = "<nb-general>"; //NOI18N
private static final String NB_ROOT_PLUGIN = "root"; //NOI18N
private final WeakReference<NbGradleProject> watcherRef;
private final Map<String, Lookup> pluginLookups = new HashMap<>();
@java.lang.SuppressWarnings("LeakingThisInConstructor")
public PluginDependentLookup(NbGradleProject watcher) {
watcherRef = new WeakReference<>(watcher);
Lookup general = Lookups.forPath("Projects/" + NbGradleProject.GRADLE_PROJECT_TYPE + "/Lookup"); //NOI18N
pluginLookups.put(NB_GENERAL, general); //NOI18N
check();
watcher.addPropertyChangeListener(WeakListeners.propertyChange(this, watcher));
}
private void check() {
boolean lookupsChanged = false;
NbGradleProject watcher = watcherRef.get();
if (watcher != null) {
lookupsChanged = !watcher.isGradleProjectLoaded();
GradleBaseProject prj = watcher.projectLookup(GradleBaseProject.class);
Set<String> currentPlugins = new HashSet<>(prj.getPlugins());
if (prj.isRoot()) {
currentPlugins.add(NB_ROOT_PLUGIN);
}
for (String cp : currentPlugins) {
//Add Lookups for new plugins
if (!pluginLookups.containsKey(cp)) {
Lookup pluginLookup = Lookups.forPath("Projects/" + NbGradleProject.GRADLE_PLUGIN_TYPE + "/" + cp + "/Lookup"); //NOI18N
pluginLookups.put(cp, pluginLookup);
lookupsChanged = true;
}
}
Iterator<String> it = pluginLookups.keySet().iterator();
while (it.hasNext()) {
String oldPlugin = it.next();
if (!currentPlugins.contains(oldPlugin) && !NB_GENERAL.equals(oldPlugin)) {
it.remove();
lookupsChanged = true;
}
}
}
if (lookupsChanged) {
setLookups(pluginLookups.values().toArray(new Lookup[pluginLookups.size()]));
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (NbGradleProject.PROP_PROJECT_INFO.equals(evt.getPropertyName())) {
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) {
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) {
try {
FileUtil.removeFileChangeListener(this, f);
} catch (IllegalArgumentException ex) {
assert false : "Project closed twice in a row";
}
}
}
}
}
}