package org.netbeans.gradle.project.java; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import org.jtrim.event.ListenerRef; import org.jtrim.property.MutableProperty; import org.jtrim.property.PropertyFactory; import org.jtrim.property.PropertySource; import org.jtrim.property.swing.SwingForwarderFactory; import org.jtrim.property.swing.SwingProperties; import org.jtrim.property.swing.SwingPropertySource; import org.jtrim.swing.concurrent.SwingTaskExecutor; import org.jtrim.utils.ExceptionHelper; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.classpath.GlobalPathRegistry; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.gradle.model.java.JavaOutputDirs; import org.netbeans.gradle.model.java.JavaSourceGroup; import org.netbeans.gradle.model.java.JavaSourceSet; import org.netbeans.gradle.project.NbGradleProjectFactory; import org.netbeans.gradle.project.ProjectInitListener; import org.netbeans.gradle.project.ProjectIssue; import org.netbeans.gradle.project.ProjectIssueManager; import org.netbeans.gradle.project.ProjectIssueRef; import org.netbeans.gradle.project.api.config.ProjectSettingsProvider; import org.netbeans.gradle.project.api.entry.GradleProjectExtension2; import org.netbeans.gradle.project.coverage.GradleCoverageProvider; import org.netbeans.gradle.project.event.ChangeListenerManager; import org.netbeans.gradle.project.event.GenericChangeListenerManager; import org.netbeans.gradle.project.java.model.JavaParsingUtils; import org.netbeans.gradle.project.java.model.JavaProjectDependencies; import org.netbeans.gradle.project.java.model.JavaSourceDirHandler; import org.netbeans.gradle.project.java.model.NbJavaModel; import org.netbeans.gradle.project.java.model.NbJavaModule; import org.netbeans.gradle.project.java.nodes.JavaExtensionNodes; import org.netbeans.gradle.project.java.nodes.JavaProjectContextActions; import org.netbeans.gradle.project.java.properties.JavaDebuggingPanel; import org.netbeans.gradle.project.java.properties.JavaProjectProperties; import org.netbeans.gradle.project.java.query.GradleAnnotationProcessingQuery; import org.netbeans.gradle.project.java.query.GradleBinaryForSourceQuery; import org.netbeans.gradle.project.java.query.GradleClassPathProvider; import org.netbeans.gradle.project.java.query.GradleProjectSources; import org.netbeans.gradle.project.java.query.GradleProjectTemplates; import org.netbeans.gradle.project.java.query.GradleSourceForBinaryQuery; import org.netbeans.gradle.project.java.query.GradleSourceLevelQueryImplementation; import org.netbeans.gradle.project.java.query.GradleUnitTestFinder; import org.netbeans.gradle.project.java.query.J2SEPlatformFromScriptQueryImpl; import org.netbeans.gradle.project.java.query.JavaInitScriptQuery; import org.netbeans.gradle.project.java.tasks.GradleJavaBuiltInCommands; import org.netbeans.gradle.project.java.tasks.JavaGradleTaskVariableQuery; import org.netbeans.gradle.project.model.issue.DependencyResolutionIssue; import org.netbeans.gradle.project.model.issue.ModelLoadIssueReporter; import org.netbeans.gradle.project.properties.NbProperties; import org.netbeans.gradle.project.script.ScriptFileProvider; import org.netbeans.gradle.project.util.CloseableAction; import org.netbeans.gradle.project.util.CloseableActionContainer; import org.netbeans.gradle.project.util.NbFunction; import org.netbeans.spi.project.support.LookupProviderSupport; import org.netbeans.spi.project.ui.ProjectOpenedHook; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Lookup; import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; public final class JavaExtension implements GradleProjectExtension2<NbJavaModel> { private static final Logger LOGGER = Logger.getLogger(JavaExtension.class.getName()); private final Project project; private final File projectDirectoryAsFile; private final MutableProperty<NbJavaModel> currentModel; private volatile boolean hasEverBeenLoaded; private final GradleClassPathProvider cpProvider; private final AtomicReference<JavaSourceDirHandler> sourceDirsHandlerRef; private final ProjectIssueRef dependencyResolutionFailureRef; private final JavaProjectDependencies projectDependencies; private final AtomicReference<Lookup> projectLookupRef; private final AtomicReference<Lookup> permanentLookupRef; private final AtomicReference<Lookup> extensionLookupRef; private final AtomicReference<Lookup> combinedLookupRef; private final ChangeListenerManager modelChangeListeners; private final AtomicReference<JavaProjectProperties> projectPropertiesRef; private final AtomicReference<ProjectSettingsProvider.ExtensionSettings> extensionSettingsRef; private JavaExtension(Project project) throws IOException { ExceptionHelper.checkNotNullArgument(project, "project"); this.projectDirectoryAsFile = FileUtil.toFile(project.getProjectDirectory()); if (projectDirectoryAsFile == null) { throw new IOException("Project directory does not exist: " + project.getProjectDirectory()); } this.project = project; NbJavaModel defaultModel = JavaParsingUtils.createEmptyModel( project.getProjectDirectory(), project.getLookup().lookup(ScriptFileProvider.class)); this.currentModel = PropertyFactory.memPropertyConcurrent(defaultModel, SwingTaskExecutor.getStrictExecutor(false)); this.cpProvider = new GradleClassPathProvider(this); this.projectDependencies = new JavaProjectDependencies(this); this.projectLookupRef = new AtomicReference<>(null); this.permanentLookupRef = new AtomicReference<>(null); this.extensionLookupRef = new AtomicReference<>(null); this.combinedLookupRef = new AtomicReference<>(null); this.hasEverBeenLoaded = false; this.sourceDirsHandlerRef = new AtomicReference<>(null); this.dependencyResolutionFailureRef = getProjectInfoManager(project).createIssueRef(); this.modelChangeListeners = new GenericChangeListenerManager(); this.projectPropertiesRef = new AtomicReference<>(null); this.extensionSettingsRef = new AtomicReference<>(null); } public static PropertySource<JavaExtension> extensionOfProject(Project project) { ExceptionHelper.checkNotNullArgument(project, "project"); PropertySource<JavaExtension> extRef = NbProperties.lookupProperty(project.getLookup(), JavaExtension.class); return NbProperties.cacheFirstNonNull(extRef); } public static PropertySource<NbJavaModel> javaModelOfProject(Project project) { PropertySource<JavaExtension> extRef = extensionOfProject(project); return NbProperties.propertyOfProperty(extRef, new NbFunction<JavaExtension, PropertySource<NbJavaModel>>() { @Override public PropertySource<NbJavaModel> apply(JavaExtension ext) { return ext != null ? ext.currentModel : PropertyFactory.<NbJavaModel>constSource(null); } }); } public ProjectSettingsProvider.ExtensionSettings getExtensionSettings() { ProjectSettingsProvider.ExtensionSettings result = extensionSettingsRef.get(); if (result == null) { ProjectSettingsProvider settingsProvider = project.getLookup().lookup(ProjectSettingsProvider.class); if (settingsProvider == null) { throw new IllegalArgumentException("Not a Gradle project."); } result = settingsProvider.getExtensionSettings(JavaExtensionDef.EXTENSION_NAME); if (!extensionSettingsRef.compareAndSet(null, result)) { result = extensionSettingsRef.get(); } } return result; } public JavaProjectProperties getProjectProperties() { JavaProjectProperties result = projectPropertiesRef.get(); if (result == null) { ProjectSettingsProvider.ExtensionSettings extensionSettings = getExtensionSettings(); result = new JavaProjectProperties(extensionSettings.getActiveSettings()); if (!projectPropertiesRef.compareAndSet(null, result)) { result = projectPropertiesRef.get(); } } return result; } public static JavaExtension getJavaExtensionOfProject(Project project) { JavaExtension result = project.getLookup().lookup(JavaExtension.class); if (result != null) { return result; } else { LOGGER.log(Level.WARNING, "JavaExtension cannot be found the project''s lookup: {0}", project.getProjectDirectory()); try { return create(project); } catch (IOException ex) { throw new RuntimeException(ex); } } } public ListenerRef addModelChangeListener(Runnable listener) { return modelChangeListeners.registerListener(listener); } public JavaSourceDirHandler getSourceDirsHandler() { JavaSourceDirHandler result = sourceDirsHandlerRef.get(); if (result == null) { sourceDirsHandlerRef.compareAndSet(null, new JavaSourceDirHandler(this)); result = sourceDirsHandlerRef.get(); } return result; } public static JavaExtension create(Project project) throws IOException { return new JavaExtension(project); } public boolean isOwnerProject(File file) { FileObject fileObj; File currentFile = file; fileObj = FileUtil.toFileObject(currentFile); while (fileObj == null) { currentFile = currentFile.getParentFile(); if (currentFile == null) { return false; } fileObj = FileUtil.toFileObject(currentFile); } return isOwnerProject(fileObj); } public boolean isOwnerProject(FileObject file) { Project owner = FileOwnerQuery.getOwner(file); if (owner == null) { return false; } return project.getProjectDirectory().equals(owner.getProjectDirectory()); } public JavaProjectDependencies getProjectDependencies() { return projectDependencies; } public PropertySource<NbJavaModel> currentModel() { return currentModel; } public NbJavaModel getCurrentModel() { return currentModel().getValue(); } private void initLookup(Lookup lookup) { for (ProjectInitListener listener: lookup.lookupAll(ProjectInitListener.class)) { listener.onInitProject(); } } // These classes are on the lookup always. @Override public Lookup getPermanentProjectLookup() { Lookup lookup = permanentLookupRef.get(); if (lookup == null) { lookup = Lookups.fixed(this, new OpenHook(this)); if (permanentLookupRef.compareAndSet(null, lookup)) { initLookup(lookup); } lookup = permanentLookupRef.get(); } return lookup; } @Override public Lookup getProjectLookup() { Lookup lookup = projectLookupRef.get(); if (lookup == null) { lookup = Lookups.fixed( LookupProviderSupport.createSourcesMerger(), new GradleProjectSources(this), cpProvider, new GradleCoverageProvider(this), new GradleSourceLevelQueryImplementation(this), new GradleUnitTestFinder(this), new GradleAnnotationProcessingQuery(), new GradleSourceForBinaryQuery(this), new GradleBinaryForSourceQuery(this), new GradleProjectTemplates(), new JavaGradleTaskVariableQuery(this), new J2SEPlatformFromScriptQueryImpl(this) // internal use only ); if (projectLookupRef.compareAndSet(null, lookup)) { initLookup(lookup); } lookup = projectLookupRef.get(); } return lookup; } @Override public Lookup getExtensionLookup() { Lookup lookup = extensionLookupRef.get(); if (lookup == null) { lookup = Lookups.fixed(new JavaExtensionNodes(this), new JavaProjectContextActions(this), new GradleJavaBuiltInCommands(this), new JavaInitScriptQuery(), JavaDebuggingPanel.createDebuggingCustomizer(true)); if (extensionLookupRef.compareAndSet(null, lookup)) { initLookup(lookup); } lookup = extensionLookupRef.get(); } return lookup; } public Project getProject() { return project; } public Lookup getOwnerProjectLookup() { return project.getLookup(); } public FileObject getProjectDirectory() { return project.getProjectDirectory(); } public File getProjectDirectoryAsFile() { return projectDirectoryAsFile; } public String getName() { return getCurrentModel().getMainModule().getShortName(); } public boolean hasEverBeenLoaded() { return hasEverBeenLoaded; } private Lookup getCombinedLookup() { Lookup lookup = combinedLookupRef.get(); if (lookup == null) { lookup = new ProxyLookup( getPermanentProjectLookup(), getProjectLookup(), getExtensionLookup()); combinedLookupRef.compareAndSet(null, lookup); lookup = combinedLookupRef.get(); } return lookup; } private void fireModelChange() { for (JavaModelChangeListener listener: getCombinedLookup().lookupAll(JavaModelChangeListener.class)) { listener.onModelChange(); } projectDependencies.updateDependencies(); modelChangeListeners.fireEventually(); } private static ProjectIssueManager getProjectInfoManager(Project project) { // TODO: In the future this should be a public API. return NbGradleProjectFactory.getGradleProject(project).getProjectIssueManager(); } private void checkDependencyResolveProblems(NbJavaModule module) { String projectName = module.getProperties().getProjectName(); List<DependencyResolutionIssue> issues = new ArrayList<>(); for (JavaSourceSet sourceSet: module.getSources()) { String sourceSetName = sourceSet.getName(); Throwable compileProblems = sourceSet.getCompileClassPathProblem(); if (compileProblems != null) { issues.add(DependencyResolutionIssue.compileIssue(projectName, sourceSetName, compileProblems)); } Throwable runtimeProblems = sourceSet.getRuntimeClassPathProblem(); if (runtimeProblems != null) { issues.add(DependencyResolutionIssue.runtimeIssue(projectName, sourceSetName, runtimeProblems)); } } if (!issues.isEmpty()) { List<ProjectIssue.Entry> entries = new ArrayList<>(issues.size()); for (DependencyResolutionIssue issue: issues) { entries.add(new ProjectIssue.Entry( ProjectIssue.Kind.ERROR, issue.getMessage(), getIssueDescription(issue))); } dependencyResolutionFailureRef.setInfo(new ProjectIssue(entries)); ModelLoadIssueReporter.reportDependencyResolutionFailures(issues); } else { dependencyResolutionFailureRef.setInfo(null); } } private static String getIssueDescription(DependencyResolutionIssue issue) { StringBuilder result = new StringBuilder(1024); result.append(issue.getMessage()); result.append("\n"); // TODO: I18N result.append("Project: "); result.append(issue.getProjectName()); result.append("\n"); result.append("Source set: "); result.append(issue.getSourceSetName()); result.append("\n"); result.append("Dependecy type: "); result.append(issue.getDependencyKind().toString()); result.append("\n"); result.append("Exception messages: "); for (String message: getExceptionMessages(issue.getStackTrace())) { result.append("\n - "); result.append(message); } return result.toString(); } private static List<String> getExceptionMessages(Throwable ex) { List<String> result = new ArrayList<>(); Throwable current = ex; while (current != null) { for (Throwable suppressed: current.getSuppressed()) { result.add(suppressed.getMessage()); } result.add(current.getMessage()); current = current.getCause(); } Collections.reverse(result); return result; } private void markOwnerIfNecessary(Path projectDir, File dir) { if (!dir.toPath().startsWith(projectDir)) { URI dirUri = Utilities.toURI(dir); FileOwnerQuery.markExternalOwner(dirUri, project, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); } } private void markOwnerIfNecessary(Path projectDir, Collection<? extends File> dirs) { for (File dir: dirs) { markOwnerIfNecessary(projectDir, dir); } } private void markOwnedOutputDirs(Path projectDir, JavaSourceSet sourceSet) { JavaOutputDirs outputDirs = sourceSet.getOutputDirs(); markOwnerIfNecessary(projectDir, outputDirs.getClassesDir()); markOwnerIfNecessary(projectDir, outputDirs.getResourcesDir()); markOwnerIfNecessary(projectDir, outputDirs.getOtherDirs()); } private void markOwnedDirs(NbJavaModule mainModule) { Path projectDir = getProjectDirectoryAsFile().toPath(); for (JavaSourceSet sourceSet: mainModule.getSources()) { markOwnedOutputDirs(projectDir, sourceSet); for (JavaSourceGroup sourceGroup: sourceSet.getSourceGroups()) { markOwnerIfNecessary(projectDir, sourceGroup.getSourceRoots()); } } } @Override public void activateExtension(NbJavaModel parsedModel) { ExceptionHelper.checkNotNullArgument(parsedModel, "parsedModel"); currentModel.setValue(parsedModel); hasEverBeenLoaded = true; NbJavaModule mainModule = parsedModel.getMainModule(); checkDependencyResolveProblems(mainModule); markOwnedDirs(mainModule); fireModelChange(); } @Override public void deactivateExtension() { } private static PropertySource<CloseableAction> classPathProviderProperty( JavaExtension javaExt, String... classPathTypes) { ClassPathProviderProperty src = new ClassPathProviderProperty(javaExt, classPathTypes); return SwingProperties.fromSwingSource(src, new SwingForwarderFactory<PropertyChangeListener>() { @Override public PropertyChangeListener createForwarder(final Runnable listener) { return new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { listener.run(); } }; } }); } private static final class ClassPathProviderProperty implements SwingPropertySource<CloseableAction, PropertyChangeListener> { private final GradleClassPathProvider cpProvider; private final CloseableAction pathRegAction; public ClassPathProviderProperty( JavaExtension javaExt, String... classPathTypes) { this.cpProvider = javaExt.cpProvider; GlobalPathReg[] pathRegs = new GlobalPathReg[classPathTypes.length]; for (int i = 0; i < classPathTypes.length; i++) { pathRegs[i] = new GlobalPathReg(javaExt, classPathTypes[i]); } this.pathRegAction = CloseableActionContainer.mergeActions(pathRegs); } @Override public CloseableAction getValue() { return pathRegAction; } @Override public void addChangeListener(PropertyChangeListener listener) { cpProvider.addPropertyChangeListener(listener); } @Override public void removeChangeListener(PropertyChangeListener listener) { cpProvider.removePropertyChangeListener(listener); } } // OpenHook is important for debugging because the debugger relies on the // globally registered source class paths for source stepping. private static class OpenHook extends ProjectOpenedHook { private final CloseableActionContainer closeableActions; public OpenHook(JavaExtension javaExt) { this.closeableActions = new CloseableActionContainer(); closeableActions.defineAction(classPathProviderProperty(javaExt, ClassPath.SOURCE, ClassPath.BOOT, ClassPath.COMPILE, ClassPath.EXECUTE)); } @Override protected void projectOpened() { closeableActions.open(); } @Override protected void projectClosed() { closeableActions.close(); } } private static class GlobalPathReg implements CloseableAction { private final JavaExtension javaExt; private final String type; public GlobalPathReg(JavaExtension javaExt, String type) { this.javaExt = javaExt; this.type = type; } @Override public Ref open() { final GlobalPathRegistry registry = GlobalPathRegistry.getDefault(); final ClassPath[] paths = new ClassPath[]{javaExt.cpProvider.getClassPaths(type)}; LOGGER.log(Level.FINE, "Registering ClassPath ({0}) for project: {1}", new Object[]{type, javaExt.getProjectDirectoryAsFile()}); registry.register(type, paths); return new Ref() { @Override public void close() { registry.unregister(type, paths); LOGGER.log(Level.FINE, "Unregistered ClassPath ({0}) for project: {1}", new Object[]{type, javaExt.getProjectDirectoryAsFile()}); } }; } } }