| /* |
| * 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; |
| } |
| |
| } |
| } |