blob: e8f76d83ce02e493216f82e2ac4ee83c3d4ca3d9 [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.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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
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 java.util.concurrent.ConcurrentHashMap;
import javax.swing.JLabel;
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 String INFO_CACHE_FILE_NAME = "project-info.ser"; //NOI18N
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 Map<File, Set<File>> SUB_PROJECT_DIR_CACHE = new ConcurrentHashMap<>();
// Increase this number if new info is gathered from the projects.
private static final int COMPATIBLE_CACHE_VERSION = 12;
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 || GradleSettings.getDefault().isCacheDisabled())
&& (prev.getQuality() == FALLBACK)) {
ProjectCacheEntry cacheEntry = loadCachedProject(files);
if (cacheEntry != null) {
if (cacheEntry.isCompatible()) {
prev = createGradleProject(cacheEntry.quality, cacheEntry.data);
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();
GradleConnector gconn = GradleConnector.newConnector();
File gradleInstall = RunUtils.evaluateGradleDistribution(ctx.project, true);
if (gradleInstall == null) {
GradleDistributionManager gdm = GradleDistributionManager.get(GradleSettings.getDefault().getGradleUserHome());
GradleDistributionManager.NbGradleVersion version = gdm.createVersion(GradleSettings.getDefault().getGradleVersion());
gradleInstall = gdm.install(version);
}
if (gradleInstall == null) {
return ctx.previous;
}
gconn.useInstallation(gradleInstall);
gconn.useGradleUserHomeDir(GradleSettings.getDefault().getGradleUserHome());
ProjectConnection pconn = gconn.forProjectDirectory(base.getProjectDir()).connect();
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} {1}", new Object[] {base.getProjectDir(), lines});
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 {
try {
pconn.close();
} catch (NullPointerException ex) {
}
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());
}
GradleProject ret = createGradleProject(quality, info);
GradleArtifactStore.getDefault().processProject(ret);
if (info.getMiscOnly()) {
ret = ctx.previous;
} else {
saveCachedProjectInfo(info, 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 (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 ProjectCacheEntry loadCachedProject(GradleFiles gf) {
File cacheFile = new File(getCacheDir(gf), INFO_CACHE_FILE_NAME);
ProjectCacheEntry ret = null;
if (cacheFile.canRead()) {
try (ObjectInputStream is = new ObjectInputStream(new FileInputStream(cacheFile))) {
try {
ret = (ProjectCacheEntry) is.readObject();
} catch (ClassNotFoundException ex) {
LOG.log(FINE, "Invalid cache entry.", ex);
}
} catch (IOException ex) {
LOG.log(FINE, "Could no load project info from " + cacheFile, ex);
}
}
return ret;
}
private static GradleProject createGradleProject(Quality quality, NbProjectInfo 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(quality, problems, results.values());
}
private static void updateSubDirectoryCache(GradleProject gp) {
if (gp.getQuality().atLeast(EVALUATED)) {
GradleBaseProject baseProject = gp.getBaseProject();
if (baseProject.isRoot()) {
SUB_PROJECT_DIR_CACHE.put(baseProject.getProjectDir(), new HashSet<File>(baseProject.getSubProjects().values()));
}
}
}
static Boolean isKnownSubProject(File rootDir, File subProjectDir) {
Set<File> cache = SUB_PROJECT_DIR_CACHE.get(rootDir);
return (cache != null) ? cache.contains(subProjectDir) : null;
}
private static void saveCachedProjectInfo(NbProjectInfo data, GradleProject gp) {
assert gp.getQuality().betterThan(FALLBACK) : "Never attempt to cache FALLBACK projects."; //NOi18N
//TODO: Make it possible to handle external file set as cache.
GradleFiles gf = new GradleFiles(gp.getBaseProject().getProjectDir(), true);
ProjectCacheEntry entry = new ProjectCacheEntry(new StoredProjectInfo(data), gp, gf.getProjectFiles());
File cacheFile = new File(getCacheDir(gp), INFO_CACHE_FILE_NAME);
if (!cacheFile.exists()) {
cacheFile.getParentFile().mkdirs();
}
try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(cacheFile))) {
os.writeObject(entry);
} catch (IOException ex) {
LOG.log(FINE, "Failed to persist project info to" + cacheFile, ex);
}
}
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;
}
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;
}
}
private static class ProjectCacheEntry implements Serializable {
int version;
long timestamp;
Set<File> sourceFiles;
Quality quality;
NbProjectInfo data;
protected ProjectCacheEntry() {
}
public ProjectCacheEntry(NbProjectInfo data, GradleProject gp, Set<File> sourceFiles) {
this.sourceFiles = sourceFiles;
this.data = data;
this.quality = gp.getQuality();
this.timestamp = gp.getEvaluationTime();
this.version = COMPATIBLE_CACHE_VERSION;
}
public boolean isCompatible() {
return version == COMPATIBLE_CACHE_VERSION;
}
public boolean isValid() {
boolean ret = isCompatible();
if (ret && (sourceFiles != null)) {
for (File f : sourceFiles) {
if (!f.exists() || (f.lastModified() > timestamp)) {
ret = false;
break;
}
}
}
return ret;
}
}
private static class StoredProjectInfo implements NbProjectInfo {
private final Map<String, Object> info;
private final Set<String> problems;
private final String gradleException;
public StoredProjectInfo(NbProjectInfo pinfo) {
info = new LinkedHashMap<>(pinfo.getInfo());
problems = new LinkedHashSet<>(pinfo.getProblems());
gradleException = pinfo.getGradleException();
}
@Override
public Map<String, Object> getInfo() {
return info;
}
@Override
public Map<String, Object> getExt() {
return Collections.emptyMap();
}
@Override
public Set<String> getProblems() {
return problems;
}
@Override
public String getGradleException() {
return gradleException;
}
@Override
public boolean hasException() {
return gradleException != null;
}
@Override
public boolean getMiscOnly() {
return false;
}
}
}