blob: a64ba1761af52eca3ddd5c9c49d504c917b82aed [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.cache.ProjectInfoDiskCache;
import org.netbeans.modules.gradle.cache.SubProjectDiskCache;
import org.netbeans.modules.gradle.spi.GradleFiles;
import org.netbeans.modules.gradle.api.GradleBaseProject;
import org.netbeans.modules.gradle.api.NbGradleProject.Quality;
import static org.netbeans.modules.gradle.api.NbGradleProject.Quality.*;
import org.netbeans.modules.gradle.api.NbProjectInfo;
import org.netbeans.modules.gradle.spi.GradleSettings;
import org.netbeans.modules.gradle.spi.ProjectInfoExtractor;
import java.io.File;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import org.gradle.tooling.BuildAction;
import org.gradle.tooling.BuildActionExecuter;
import org.gradle.tooling.BuildController;
import org.gradle.tooling.GradleConnectionException;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;
import static java.util.logging.Level.*;
import javax.swing.SwingUtilities;
import org.gradle.tooling.CancellationToken;
import org.gradle.tooling.CancellationTokenSource;
import org.gradle.tooling.ProgressEvent;
import org.gradle.tooling.ProgressListener;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import static org.netbeans.modules.gradle.GradleDaemon.*;
import org.netbeans.modules.gradle.api.NbGradleProject;
import org.netbeans.modules.gradle.api.execute.GradleCommandLine;
import java.util.WeakHashMap;
import javax.swing.JLabel;
import org.netbeans.modules.gradle.cache.AbstractDiskCache.CacheEntry;
import org.netbeans.modules.gradle.cache.ProjectInfoDiskCache.QualifiedProjectInfo;
import org.netbeans.modules.gradle.api.execute.RunUtils;
import org.openide.awt.Notification;
import org.openide.awt.NotificationDisplayer;
import org.openide.awt.NotificationDisplayer.Category;
import org.openide.awt.NotificationDisplayer.Priority;
/**
*
* @author Laszlo Kishalmi
*/
@SuppressWarnings("rawtypes")
public final class GradleProjectCache {
private enum GoOnline { NEVER, ON_DEMAND, ALWAYS }
private static final Logger LOG = Logger.getLogger(GradleProjectCache.class.getName());
private static final Map<File, List<Notification>> NOTIFICATIONS = new WeakHashMap<>();
private static AtomicLong timeInLoad = new AtomicLong();
private static AtomicInteger loadedProjects = new AtomicInteger();
private static final boolean DEBUG_GRADLE_INFO_ACTION = Boolean.getBoolean("netbeans.debug.gradle.info.action"); //NOI18N
private GradleProjectCache() {
}
/**
* Loads a physical GradleProject either from Gradle or Cache. As project
* retrieval can be time consuming using Gradle sometimes it's just enough
* to shoot for FALLBACK information. Aiming for FALLBACK quality either
* retrieves the GradleProject form cache if it's valid or returns the
* fallback Project implementation.
*
* @param files The project to load.
* @param requestedQuality The project information quality to aim for.
* @return The retrievable GradleProject
*/
public static GradleProject loadProject(final NbGradleProjectImpl project, Quality aim, boolean ignoreCache, boolean interactive, String... args) {
final GradleFiles files = project.getGradleFiles();
if (aim == FALLBACK) {
return fallbackProject(files);
}
GradleProject prev = project.project != null ? project.project : fallbackProject(files);
// Try to turn to the cache
if (!ignoreCache && (prev.getQuality() == FALLBACK)) {
CacheEntry<QualifiedProjectInfo> cacheEntry = new ProjectInfoDiskCache(files).loadEntry();
if (cacheEntry != null) {
if (cacheEntry.isCompatible()) {
prev = createGradleProject(cacheEntry.getData());
if (cacheEntry.isValid()) {
updateSubDirectoryCache(prev);
return prev;
}
}
}
}
final ReloadContext ctx = new ReloadContext(project, prev, aim);
ctx.args = args;
GradleProject ret;
try {
if (RunUtils.isProjectTrusted(project, interactive)) {
ret = GRADLE_LOADER_RP.submit(new ProjectLoaderTask(ctx)).get();
updateSubDirectoryCache(ret);
} else {
ret = prev.invalidate();
}
} catch (InterruptedException | ExecutionException ex) {
ret = fallbackProject(files);
}
return ret;
}
@Messages({
"# {0} - project directory",
"TIT_LOAD_FAILED=Cannot load: {0}",
"# {0} - project name",
"TIT_LOAD_ISSUES={0} has some issues"
})
private static GradleProject loadGradleProject(ReloadContext ctx, CancellationToken token, ProgressListener pl) {
long start = System.currentTimeMillis();
NbProjectInfo info = null;
Quality quality = ctx.aim;
GradleBaseProject base = ctx.previous.getBaseProject();
ProjectConnection pconn = ctx.project.getLookup().lookup(ProjectConnection.class);
GradleCommandLine cmd = new GradleCommandLine(ctx.args);
cmd.setFlag(GradleCommandLine.Flag.CONFIGURE_ON_DEMAND, GradleSettings.getDefault().isConfigureOnDemand());
cmd.addParameter(GradleCommandLine.Parameter.INIT_SCRIPT, INIT_SCRIPT);
cmd.setStackTrace(GradleCommandLine.StackTrace.SHORT);
cmd.addSystemProperty(GradleDaemon.PROP_TOOLING_JAR, TOOLING_JAR);
cmd.addProjectProperty("nbSerializeCheck", "true");
GoOnline goOnline;
if (GradleSettings.getDefault().isOffline()) {
goOnline = GoOnline.NEVER;
} else if (ctx.aim == FULL_ONLINE) {
goOnline = GoOnline.ALWAYS;
} else {
switch (GradleSettings.getDefault().getDownloadLibs()) {
case NEVER:
goOnline = GoOnline.NEVER;
break;
case ALWAYS:
goOnline = GoOnline.ALWAYS;
break;
default:
goOnline = GoOnline.ON_DEMAND;
}
}
try {
info = retrieveProjectInfo(goOnline, pconn, cmd, token, pl);
List<Notification> nlist = NOTIFICATIONS.get(base.getProjectDir());
if (nlist != null) {
NOTIFICATIONS.remove(base.getProjectDir());
for (Notification notification : nlist) {
notification.clear();
}
}
if (!info.hasException()) {
if (!info.getProblems().isEmpty()) {
// If we do not have exception, but seen some problems the we mark the quality as SIMPLE
quality = SIMPLE;
openNotification(base.getProjectDir(),
Bundle.TIT_LOAD_ISSUES(base.getProjectDir().getName()),
Bundle.TIT_LOAD_ISSUES(base.getProjectDir().getName()),
bulletedList(info.getProblems()));
} else {
quality = ctx.aim;
}
} else {
String problem = info.getGradleException();
String[] lines = problem.split("\n");
LOG.log(INFO, "Failed to retrieve project information for: {0}\nReason: {1}", new Object[] {base.getProjectDir(), problem}); //NOI18N
openNotification(base.getProjectDir(), Bundle.TIT_LOAD_FAILED(base.getProjectDir().getName()), lines[0], problem);
return ctx.previous.invalidate(problem);
}
} catch (GradleConnectionException | IllegalStateException ex) {
LOG.log(FINE, "Failed to retrieve project information for: " + base.getProjectDir(), ex);
StringBuilder sb = new StringBuilder();
Throwable th = ex;
String separator = "";
while (th != null) {
sb.insert(0, separator);
sb.insert(0, th.getMessage());
th = th.getCause();
separator = "<br/>";
}
openNotification(base.getProjectDir(), Bundle.TIT_LOAD_FAILED(base.getProjectDir()), ex.getMessage(), sb.toString());
return ctx.previous.invalidate(sb.toString());
} finally {
loadedProjects.incrementAndGet();
}
long finish = System.currentTimeMillis();
timeInLoad.getAndAdd(finish - start);
LOG.log(FINE, "Loaded project {0} in {1} msec", new Object[]{base.getProjectDir(), finish - start});
if (SwingUtilities.isEventDispatchThread()) {
LOG.log(FINE, "Load happened on AWT event dispatcher", new RuntimeException());
}
QualifiedProjectInfo qinfo = new QualifiedProjectInfo(quality, info);
GradleProject ret = createGradleProject(qinfo);
GradleArtifactStore.getDefault().processProject(ret);
if (info.getMiscOnly()) {
ret = ctx.previous;
} else {
saveCachedProjectInfo(qinfo, ret);
}
return ret;
}
private static BuildActionExecuter<NbProjectInfo> createInfoAction(ProjectConnection pconn, GradleCommandLine cmd, CancellationToken token, ProgressListener pl) {
BuildActionExecuter<NbProjectInfo> ret = pconn.action(new NbProjectInfoAction());
cmd.configure(ret);
if (DEBUG_GRADLE_INFO_ACTION) {
// This would start the Gradle Daemon in Debug Mode, so the Tooling API can be debugged as well
ret.addJvmArguments("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5006");
}
if (token != null) {
ret.withCancellationToken(token);
}
if (pl != null) {
ret.addProgressListener(pl);
}
return ret;
}
private static NbProjectInfo retrieveProjectInfo(GoOnline goOnline, ProjectConnection pconn, GradleCommandLine cmd, CancellationToken token, ProgressListener pl) throws GradleConnectionException, IllegalStateException {
NbProjectInfo ret;
GradleSettings settings = GradleSettings.getDefault();
GradleCommandLine online = new GradleCommandLine(cmd);
GradleCommandLine offline = new GradleCommandLine(cmd);
if (goOnline != GoOnline.ALWAYS) {
if (settings.getDownloadSources() == GradleSettings.DownloadMiscRule.ALWAYS) {
//online.addProjectProperty("downloadSources", "ALL"); //NOI18N
}
if (settings.getDownloadJavadoc() == GradleSettings.DownloadMiscRule.ALWAYS) {
//online.addProjectProperty("downloadJavadoc", "ALL"); //NOI18N
}
offline.addFlag(GradleCommandLine.Flag.OFFLINE);
}
if (goOnline == GoOnline.NEVER || goOnline == GoOnline.ON_DEMAND) {
BuildActionExecuter<NbProjectInfo> action = createInfoAction(pconn, offline, token, pl);
try {
ret = action.run();
if (goOnline == GoOnline.NEVER || !ret.hasException()) {
return ret;
}
} catch (GradleConnectionException | IllegalStateException ex) {
if (goOnline == GoOnline.NEVER) {
throw ex;
}
}
}
BuildActionExecuter<NbProjectInfo> action = createInfoAction(pconn, online, token, pl);
ret = action.run();
return ret;
}
private static class NbProjectInfoAction implements Serializable, BuildAction<NbProjectInfo> {
@Override
public NbProjectInfo execute(BuildController bc) {
return bc.getModel(NbProjectInfo.class);
}
}
private static class ProjectLoaderTask implements Callable<GradleProject>, Cancellable {
private final ReloadContext ctx;
private CancellationTokenSource tokenSource;
public ProjectLoaderTask(ReloadContext ctx) {
this.ctx = ctx;
}
@Messages({
"# {0} - The project name",
"LBL_Loading=Loading {0}"
})
@Override
public GradleProject call() throws Exception {
tokenSource = GradleConnector.newCancellationTokenSource();
final ProgressHandle handle = ProgressHandle.createHandle(Bundle.LBL_Loading(ctx.previous.getBaseProject().getName()), this);
ProgressListener pl = (ProgressEvent pe) -> {
handle.progress(pe.getDescription());
};
handle.start();
try {
return loadGradleProject(ctx, tokenSource.token(), pl);
} catch (Throwable ex) {
LOG.log(WARNING, ex.getMessage(), ex);
throw ex;
} finally {
handle.finish();
}
}
@Override
public boolean cancel() {
if (tokenSource != null) {
tokenSource.cancel();
}
return true;
}
}
private static void openNotification(File projectDir, String title, String problem, String details) {
StringBuilder sb = new StringBuilder(details.length());
sb.append("<html>");
String[] lines = details.split("\n");
for (String line : lines) {
sb.append(line).append("<br/>");
}
Notification notify = NotificationDisplayer.getDefault().notify(title,
NbGradleProject.getWarningIcon(),
new JLabel(problem),
new JLabel(sb.toString()),
Priority.LOW, Category.WARNING);
List<Notification> nlist = NOTIFICATIONS.get(projectDir);
if (nlist == null) {
nlist = new LinkedList<>();
NOTIFICATIONS.put(projectDir, nlist);
}
nlist.add(notify);
}
private static String bulletedList(Collection<? extends Object> elements) {
StringBuilder sb = new StringBuilder();
sb.append("<ul>");
for (Object element : elements) {
sb.append("<li>");
String[] lines = element.toString().split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
sb.append(line);
if (i < lines.length - 1) {
sb.append("<br/>");
}
}
sb.append("</li>");
}
sb.append("</ul>");
return sb.toString();
}
private static GradleProject createGradleProject(QualifiedProjectInfo info) {
Collection<? extends ProjectInfoExtractor> extractors = Lookup.getDefault().lookupAll(ProjectInfoExtractor.class);
Map<Class, Object> results = new HashMap<>();
Set<String> problems = new LinkedHashSet<>(info.getProblems());
Map<String, Object> projectInfo = new HashMap<>(info.getInfo());
projectInfo.putAll(info.getExt());
for (ProjectInfoExtractor extractor : extractors) {
ProjectInfoExtractor.Result result = extractor.extract(projectInfo, Collections.unmodifiableMap(results));
problems.addAll(result.getProblems());
for (Object extract : result.getExtract()) {
results.put(extract.getClass(), extract);
}
}
return new GradleProject(info.getQuality(), problems, results.values());
}
private static void updateSubDirectoryCache(GradleProject gp) {
if (gp.getQuality().atLeast(EVALUATED)) {
GradleBaseProject baseProject = gp.getBaseProject();
if (baseProject.isRoot()) {
SubProjectDiskCache spCache = SubProjectDiskCache.get(baseProject.getRootDir());
spCache.storeData(new SubProjectDiskCache.SubProjectInfo(baseProject));
}
}
}
private static void saveCachedProjectInfo(QualifiedProjectInfo data, GradleProject gp) {
assert gp.getQuality().betterThan(FALLBACK) : "Never attempt to cache FALLBACK projects."; //NOi18N
GradleFiles gf = new GradleFiles(gp.getBaseProject().getProjectDir(), true);
new ProjectInfoDiskCache(gf).storeData(data);
}
private static GradleProject fallbackProject(GradleFiles files) {
return createFallbackProject(FALLBACK, files, Collections.<String>emptyList());
}
private static GradleProject createFallbackProject(Quality quality, GradleFiles files, Collection<String> probs) {
Collection<? extends ProjectInfoExtractor> extractors = Lookup.getDefault().lookupAll(ProjectInfoExtractor.class);
Map<Class, Object> infos = new HashMap<>();
Set<String> problems = new LinkedHashSet<>(probs);
for (ProjectInfoExtractor extractor : extractors) {
ProjectInfoExtractor.Result result = extractor.fallback(files);
problems.addAll(result.getProblems());
for (Object extract : result.getExtract()) {
infos.put(extract.getClass(), extract);
}
}
return new GradleProject(quality, problems, infos.values());
}
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 static final class ReloadContext {
final NbGradleProjectImpl project;
final GradleProject previous;
final Quality aim;
String[] args = new String[0];
public ReloadContext(NbGradleProjectImpl project, GradleProject previous, Quality aim) {
this.project = project;
this.previous = previous;
this.aim = aim;
}
public GradleProject getPrevious() {
return previous;
}
public Quality getAim() {
return aim;
}
}
}