This is an automated email from the ASF dual-hosted git repository. sdedic pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push: new f98f5e0e36 Detect proxy issues and offer fixes. (#4043) f98f5e0e36 is described below commit f98f5e0e369ed66b882c9b4820a37b513a3b9188 Author: Svatopluk Dedic <svatopluk.de...@oracle.com> AuthorDate: Mon Jul 11 10:57:42 2022 +0200 Detect proxy issues and offer fixes. (#4043) Detect proxy issues and offer fixes --- extide/gradle/apichanges.xml | 14 + .../modules/gradle/api/NbGradleProject.java | 14 +- .../problems/ChangeOrRemovePropertyResolver.java | 176 ++++++++ .../gradle/problems/GradlePropertiesEditor.java | 200 +++++++++ .../modules/gradle/problems/PropertiesEditor.java | 141 +++++++ .../gradle/problems/ProxyAlertProvider.java | 463 +++++++++++++++++++++ 6 files changed, 1007 insertions(+), 1 deletion(-) diff --git a/extide/gradle/apichanges.xml b/extide/gradle/apichanges.xml index 5ce8c29166..72dd83e5d4 100644 --- a/extide/gradle/apichanges.xml +++ b/extide/gradle/apichanges.xml @@ -83,6 +83,20 @@ is the proper place. <!-- ACTUAL CHANGES BEGIN HERE: --> <changes> + <change id="gradleproject-files"> + <api name="general"/> + <summary>GradleFiles can be obtained from API</summary> + <version major="2" minor="24"/> + <date day="30" month="4" year="2022"/> + <author login="sdedic"/> + <compatibility semantic="compatible" addition="yes"/> + <description> + <a href="@TOP@/org/netbeans/modules/gradle/spi/GradleFiles.html">GradleFiles</a> can be obtained from + <a href="@TOP@/org/netbeans/modules/gradle/api/NbGradleProject.html">NbGradleProject</a> instance. Gradle project + users can use <code>GradleFiles</code> to get important files or pathnames in the project. + </description> + <class package="org.netbeans.modules.gradle.api" name="NbGradleProject"/> + </change> <change id="gradle-reports"> <api name="general"/> <summary>Rich exception reports are exported from Gradle process.</summary> diff --git a/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java b/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java index f62db6da94..ba3ace1b43 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java @@ -38,6 +38,7 @@ import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.modules.gradle.spi.GradleFiles; import org.openide.filesystems.FileAttributeEvent; import org.openide.filesystems.FileChangeListener; import org.openide.filesystems.FileEvent; @@ -307,7 +308,18 @@ public final class NbGradleProject { public static NbGradleProject get(Project project) { return project instanceof NbGradleProjectImpl ? ((NbGradleProjectImpl) project).getProjectWatcher() : null; } - + + /** + * Returns accessor for Gradle project files. Note that the returned instance is immutable, possibly lazy-initialized. + * A change (creation, removal) to project files will not be reflected by the {@link GradleFiles} instance, but this method + * may return a new instance. + * @return files accessor. + * @since 2.24 + */ + public GradleFiles getGradleFiles() { + return project.getGradleFiles(); + } + @Override public String toString() { return "Watcher for " + project.toString(); //NOI18N diff --git a/extide/gradle/src/org/netbeans/modules/gradle/problems/ChangeOrRemovePropertyResolver.java b/extide/gradle/src/org/netbeans/modules/gradle/problems/ChangeOrRemovePropertyResolver.java new file mode 100644 index 0000000000..c41031ef4b --- /dev/null +++ b/extide/gradle/src/org/netbeans/modules/gradle/problems/ChangeOrRemovePropertyResolver.java @@ -0,0 +1,176 @@ +/* + * 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.problems; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.api.project.Project; +import org.netbeans.modules.gradle.api.NbGradleProject; +import org.netbeans.spi.project.ui.ProjectProblemResolver; +import org.netbeans.spi.project.ui.ProjectProblemsProvider; +import org.netbeans.spi.project.ui.ProjectProblemsProvider.Result; +import org.openide.util.EditableProperties; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; + +/** + * Adds, modifies or removes proxy properties from gradle's settings file. If found in some of the existing files, the + * Resolver modifies that property file. If the properties are nowehere to be found (and are necessary), the Resolver + * adds them to the user properties file ({@code ~/.gradle/gradle.properties}). + * + * @author sdedic + */ +public class ChangeOrRemovePropertyResolver implements ProjectProblemResolver { + private static final Logger LOG = Logger.getLogger(ChangeOrRemovePropertyResolver.class.getName()); + private static final RequestProcessor EDIT_RP = new RequestProcessor(ChangeOrRemovePropertyResolver.class); + + private final Project project; + private final String proxyHost; + private final PropertiesEditor editor; + private final int proxyPort; + private final CompletableFuture<ProjectProblemsProvider.Result> future = new CompletableFuture<>(); + private final NbGradleProject gp; + + public ChangeOrRemovePropertyResolver(Project project, PropertiesEditor editor, String proxyHost, int proxyPort) { + this.project = project; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.editor = editor; + gp = NbGradleProject.get(project); + } + + @Override + public Future<ProjectProblemsProvider.Result> resolve() { + EDIT_RP.post(() -> run()); + return future; + } + + @NbBundle.Messages({ + "# {0} - properties file name", + "# {1} - reported error message", + "Error_UpdatePropertiesError=Error updating properties file {0}: {1}", + "Error_ProxySettingNotFound=Proxy settings not found.", + "# {0} - properties file name", + "Error_LoadingUserProperties=Could not load user properties file ({0}).", + "ReasonProxyChanged=Gradle proxies have changed" + }) + public void run() { + boolean reload = doRun(); + if (reload) { + gp.toQuality(Bundle.ReasonProxyChanged(), NbGradleProject.Quality.FULL, true); + } + } + + boolean doRun() { + if (editor == null) { + // should not happen + return false; + } + EditableProperties p; + try { + p = editor.open(); + } catch (IOException ex) { + LOG.log(Level.WARNING, "Could not load properties: {0}", editor.getFilePath()); + LOG.log(Level.WARNING, "Error reported", ex); + + future.complete(Result.create(ProjectProblemsProvider.Status.UNRESOLVED, Bundle.Error_UpdatePropertiesError(editor.getFilePath(), ex.getLocalizedMessage()))); + return false; + } + + boolean updated = updateProperties(p); + if (!updated) { + if (proxyHost == null) { + // there were no properties, still the error happened ?? + future.complete(Result.create(ProjectProblemsProvider.Status.UNRESOLVED, Bundle.Error_ProxySettingNotFound())); + return false; + } + + p.setProperty("systemProp.http.proxyHost", proxyHost); + p.setProperty("systemProp.http.proxyPort", Integer.toString(proxyPort)); + p.setProperty("systemProp.https.proxyHost", proxyHost); + p.setProperty("systemProp.https.proxyPort", Integer.toString(proxyPort)); + } + try { + editor.save(); + } catch (IOException ex) { + LOG.log(Level.WARNING, "Could not save updated properties: {0}", editor.getFilePath()); + LOG.log(Level.WARNING, "Error reported", ex); + future.complete(Result.create(ProjectProblemsProvider.Status.UNRESOLVED, Bundle.Error_UpdatePropertiesError(editor.getFilePath(), ex.getLocalizedMessage()))); + return false; + } + future.complete(Result.create(ProjectProblemsProvider.Status.RESOLVED)); + return true; + } + + private boolean updateProperties(EditableProperties props) { + boolean b = changeProperty(props, "systemProp.http.proxyHost"); // NOI18N + b |= changeProperty(props, "systemProp.https.proxyHost"); // NOI18N + b |= changeProperty(props, "systemProp.socks.proxyHost"); // NOI18N + return b; + } + + private boolean changeProperty(EditableProperties props, String k) { + String s = props.getProperty(k); + if (s == null) { + return false; + } + if (proxyHost != null) { + props.setProperty(k, proxyHost); + props.setProperty(k.replace("Host", "Port"), Integer.toString(proxyPort)); + } else { + props.remove(k); + } + return true; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 83 * hash + Objects.hashCode(this.project); + hash = 83 * hash + Objects.hashCode(this.proxyHost); + hash = 83 * hash + this.proxyPort; + hash = 83 * hash + this.editor.getFilePath().hashCode(); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ChangeOrRemovePropertyResolver other = (ChangeOrRemovePropertyResolver) obj; + if (this.proxyPort != other.proxyPort) { + return false; + } + if (!Objects.equals(this.proxyHost, other.proxyHost)) { + return false; + } + return Objects.equals(this.project, other.project); + } +} diff --git a/extide/gradle/src/org/netbeans/modules/gradle/problems/GradlePropertiesEditor.java b/extide/gradle/src/org/netbeans/modules/gradle/problems/GradlePropertiesEditor.java new file mode 100644 index 0000000000..d4f819e7d1 --- /dev/null +++ b/extide/gradle/src/org/netbeans/modules/gradle/problems/GradlePropertiesEditor.java @@ -0,0 +1,200 @@ +/* + * 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.problems; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.api.project.Project; +import org.netbeans.modules.gradle.api.NbGradleProject; +import org.netbeans.modules.gradle.spi.GradleFiles; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.EditableProperties; + +/** + * A helper class that loads + merges property files. It also tracks which property comes + * from which file (project properties, settings, user properties). The loaded properties + * are cached, and the cache is checked on each call to {@link #loadProperties}. + * <p> + * {@link #ensureGetProperties()} only ensures the properties are loaded, not that the + * current state is not stale. Make sure that {@link #loadProperties} are called after + * each suspected change to refresh the state. + * + * @author sdedic + */ +public class GradlePropertiesEditor { + private static final Logger LOG = Logger.getLogger(GradlePropertiesEditor.class.getName()); + + private final Project project; + + public GradlePropertiesEditor(Project project) { + this.project = project; + } + + // @GuardedBy(this) + private CachedProperties loadedProperties = new CachedProperties(Collections.emptyMap()); + + static class CachedProperties extends Properties { + private final Map<File, Long> timestamps; + private final Map<String, FileObject> origins = new HashMap<>(); + private final Map<Path, PropertiesEditor> editables = new HashMap<>(); + + public CachedProperties(Map<File, Long> timestamps) { + this.timestamps = timestamps; + } + + boolean valid(Collection<File> files) { + if (!timestamps.keySet().containsAll(files) || timestamps.size() != files.size()) { + return false; + } + for (File k : files) { + Long l = timestamps.get(k); + if (l == null || l.longValue() != k.lastModified()) { + return false; + } + } + return true; + } + } + + public FileObject getPropertyOrigin(String propName) { + return ensureGetProperties().origins.get(propName); + } + + public PropertiesEditor getEditorFor(String property) { + FileObject fo = getPropertyOrigin(property); + if (fo == null) { + return null; + } else { + return getEditor(fo, null); + } + } + + public PropertiesEditor getEditor(FileObject origin, GradleFiles.Kind kind) { + GradleFiles gf = NbGradleProject.get(project).getGradleFiles(); + if (origin != null) { + File f = FileUtil.toFile(origin); + if (f != null) { + synchronized (this) { + PropertiesEditor ep = ensureGetProperties().editables.get(f.toPath()); + if (ep != null) { + return ep; + } + } + } + } + if (kind == null) { + return null; + } + File f = gf.getFile(kind); + if (f == null) { + return null; + } + Map<Path, PropertiesEditor> editables = ensureGetProperties().editables; + + PropertiesEditor ed; + synchronized (this) { + ed = editables.get(f.toPath()); + if (ed != null) { + return ed; + } + ed = new PropertiesEditor(f.toPath()); + editables.put(f.toPath(), ed); + } + return ed; + } + + /** + * Loads properties from project and global files. + * @throws IOException + */ + public Properties loadGradleProperties() { + return loadGradleProperties0(); + } + + CachedProperties loadGradleProperties0() { + GradleFiles gf = NbGradleProject.get(project).getGradleFiles(); + List<File> files = gf.getPropertyFiles(); + CachedProperties cached; + synchronized (this) { + cached = loadedProperties; + if (cached.valid(files)) { + return cached; + } + } + return doLoadProperties(cached, files); + } + + CachedProperties ensureGetProperties() { + synchronized (this) { + if (loadedProperties != null) { + return loadedProperties; + } + } + return loadGradleProperties0(); + } + + private CachedProperties doLoadProperties(CachedProperties cached, List<File> files) { + Map<File, Long> stamps = new HashMap<>(); + for (File f : files) { + stamps.put(f, f.lastModified()); + } + CachedProperties merged = new CachedProperties(stamps); + Map<Path, PropertiesEditor> propertyMap = new HashMap<>(); + for (int i = files.size() - 1; i >= 0; i--) { + File f = files.get(i); + Path path = f.toPath(); + if (propertyMap.containsKey(path)) { + continue; + } + FileObject fo = FileUtil.toFileObject(f); + + try { + PropertiesEditor pe = fo == null ? new PropertiesEditor(path) : new PropertiesEditor(fo); + propertyMap.put(path, pe); + EditableProperties p = pe.open(); + for (Object k : p.keySet()) { + String ks = k.toString(); + if (!merged.containsKey(ks)) { + merged.put(ks, p.getProperty(ks)); + merged.origins.put(ks, fo); + } + } + merged.editables.put(path, pe); + } catch (IOException ex) { + LOG.log(Level.INFO, "Could not read properties file {0}", f); + } + } + synchronized (this) { + if (this.loadedProperties == cached) { + this.loadedProperties = merged; + } + } + return merged; + } +} diff --git a/extide/gradle/src/org/netbeans/modules/gradle/problems/PropertiesEditor.java b/extide/gradle/src/org/netbeans/modules/gradle/problems/PropertiesEditor.java new file mode 100644 index 0000000000..3bc8d7fa2f --- /dev/null +++ b/extide/gradle/src/org/netbeans/modules/gradle/problems/PropertiesEditor.java @@ -0,0 +1,141 @@ +/* + * 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.problems; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.netbeans.api.actions.Savable; +import org.netbeans.api.editor.document.AtomicLockDocument; +import org.netbeans.api.editor.document.LineDocumentUtils; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.EditableProperties; + +/** + * + * @author sdedic + */ +public class PropertiesEditor { + private static final Logger LOG = Logger.getLogger(PropertiesEditor.class.getName()); + + private FileObject file; + private final Path filePath; + private Document openedDocument; + private EditableProperties properties; + + public PropertiesEditor(FileObject file) { + this.file = file; + File f = FileUtil.toFile(file); + this.filePath = f != null ? f.toPath() : null; + } + + public PropertiesEditor(Path path) { + this.filePath = path; + } + + public Path getFilePath() { + return filePath; + } + + public EditableProperties open() throws IOException { + if (properties != null) { + return properties; + } + if (file == null) { + return properties = new EditableProperties(false); + } + EditorCookie cake = file.getLookup().lookup(EditorCookie.class); + if (cake != null) { + openedDocument = cake.getDocument(); + } + + EditableProperties p; + if (openedDocument == null) { + try (InputStream istm = file.getInputStream()) { + p = new EditableProperties(false); + p.load(istm); + } + this.properties = p; + return p; + } + IOException[] err = new IOException[1]; + AtomicReference<ByteArrayInputStream> ref = new AtomicReference<>(); + openedDocument.render(() -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintWriter pw = new PrintWriter(baos)) { + pw.print( + openedDocument.getText(0, openedDocument.getLength()) + ); + ref.set(new ByteArrayInputStream(baos.toByteArray())); + } catch (BadLocationException ex) { + err[0] = new IOException(ex); + } + }); + p = new EditableProperties(false); + p.load(ref.get()); + this.properties = p; + return p; + } + + public void save() throws IOException { + if (file != null) { + EditorCookie cake = file.getLookup().lookup(EditorCookie.class); + if (cake != null) { + openedDocument = cake.getDocument(); + } + + if (openedDocument != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + properties.store(baos); + String str = new String(baos.toByteArray(), StandardCharsets.ISO_8859_1); + AtomicLockDocument ald = LineDocumentUtils.asRequired(openedDocument, AtomicLockDocument.class); + BadLocationException[] err = new BadLocationException[1]; + Runnable edit = () -> { + int curLen = openedDocument.getLength(); + try { + openedDocument.insertString(0, str, null); + openedDocument.remove(str.length(), curLen); + } catch (BadLocationException ex) { + err[0] = ex; + } + }; + ald.runAtomicAsUser(edit); + file.getLookup().lookup(Savable.class).save(); + return; + } + } + try (OutputStream fos = Files.newOutputStream(filePath, StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING)) { + properties.store(fos); + } + } +} diff --git a/extide/gradle/src/org/netbeans/modules/gradle/problems/ProxyAlertProvider.java b/extide/gradle/src/org/netbeans/modules/gradle/problems/ProxyAlertProvider.java new file mode 100644 index 0000000000..18b245a89a --- /dev/null +++ b/extide/gradle/src/org/netbeans/modules/gradle/problems/ProxyAlertProvider.java @@ -0,0 +1,463 @@ +/* + * 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.problems; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.api.project.Project; +import org.netbeans.modules.gradle.GradleReport; +import org.netbeans.modules.gradle.NbGradleProjectImpl; +import org.netbeans.modules.gradle.api.NbGradleProject; +import org.netbeans.modules.gradle.spi.GradleFiles; +import org.netbeans.spi.project.ProjectServiceProvider; +import org.netbeans.spi.project.ui.ProjectProblemResolver; +import org.netbeans.spi.project.ui.ProjectProblemsProvider; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; + +/** + * The ProblemsProvider impl could be simpler, but the extraction of system proxies using PAC may be quite time-consuming, + * so it should not be done after each query for project problems. Rather the impl monitors project reloads / info changes + * and processes reports. Known reports are recorded at the start of the processing, so subsequent project reload with the + * same reports will not trigger another round. + * + * @author sdedic + */ +@ProjectServiceProvider(service = ProjectProblemsProvider.class, projectType = NbGradleProject.GRADLE_PROJECT_TYPE) +public class ProxyAlertProvider implements ProjectProblemsProvider, PropertyChangeListener { + private static final Logger LOG = Logger.getLogger(ProxyAlertProvider.class.getName()); + private static final RequestProcessor CHECKER_RP = new RequestProcessor(ProxyAlertProvider.class); + + private final Project owner; + private final RequestProcessor.Task checkTask = CHECKER_RP.create(new ReportChecker(), true); + private final GradlePropertiesEditor gradlePropertiesEditor; + private final PropertyChangeSupport propSupport = new PropertyChangeSupport(this); + + // @GuardedBy(this) + private Collection<GradleReport> knownReports = Collections.emptySet(); + // @GuardedBy(this) + private List<ProjectProblem> problems = null; + + public ProxyAlertProvider(Project owner) { + this.owner = owner; + NbGradleProject.addPropertyChangeListener(owner, this); + this.gradlePropertiesEditor = new GradlePropertiesEditor(owner); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + propSupport.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + propSupport.removePropertyChangeListener(listener); + } + + @Override + public Collection<? extends ProjectProblem> getProblems() { + synchronized (this) { + if (problems == null) { + checkTask.schedule(0); + this.problems = Collections.emptyList(); + return Collections.emptyList(); + } else { + return new ArrayList<>(this.problems); + } + } + } + + void updateProblemList(List<ProjectProblem> newProblems) { + List<ProjectProblem> old; + + synchronized (this) { + if (this.problems != null && this.problems.equals(newProblems)) { + return; + } + old = this.problems; + if (old == null) { + old = Collections.emptyList(); + } + this.problems = newProblems; + } + propSupport.firePropertyChange(PROP_PROBLEMS, old, newProblems); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!NbGradleProject.PROP_PROJECT_INFO.equals(evt.getPropertyName())) { + return; + } + synchronized (this) { + if (reports().equals(this.knownReports)) { + return; + } + } + checkTask.schedule(0); + } + + private Set<GradleReport> reports() { + NbGradleProjectImpl gp = (NbGradleProjectImpl)owner; + return gp.getGradleProject().getProblems(); + } + + private static final String CLASS_UNKNOWN_HOST = "java.net.UnknownHostException"; // NOI18N + private static final String CLASS_CONNECT_TIMEOUT = "org.apache.http.conn.ConnectTimeoutException"; // NOI18N + + /** + * Checks the proxy settings asynchronously. + */ + @NbBundle.Messages({ + "Title_GradleProxyNeeded=Proxy is missing", + "# {0} - proxy host", + "ProxyProblemMissing=Gradle is missing proxy settings, but it seems that {0} should be used as a proxy." + }) + class ReportChecker implements Runnable { + private Set<GradleReport> processReports; + private List<String> systemProxies; + + @Override + public void run() { + systemProxies = null; + gradlePropertiesEditor.loadGradleProperties(); + processReports = reports(); + synchronized (ProxyAlertProvider.this) { + knownReports = processReports; + } + Set<String> gradleProxies = findProxyHostNames(); + AtomicReference<String> proxyName = new AtomicReference<>(); + + findReport(processReports, (r) -> { + if (CLASS_UNKNOWN_HOST.equals(r.getErrorClass()) || + CLASS_CONNECT_TIMEOUT.equals(r.getErrorClass())) { + for (String s : gradleProxies) { + if (r.getMessage().contains(s + ":")) { + proxyName.set(s); + return true; + } + } + } + return false; + }); + List<ProjectProblem> problems = new ArrayList<>(); + String offendingProxy = proxyName.get(); + + // configured proxy reported as unreachable, make a report, suggest a fix + // to remove the proxy name: + if (offendingProxy != null) { + maybeChangeProxyFix(offendingProxy, problems); + } + + if (gradleProxies.isEmpty()) { + GradleReport connectTimeout = findReport(processReports, (r) -> + CLASS_CONNECT_TIMEOUT.equals(r.getErrorClass()) + ); + List<String> proxies = findSystemProxies(); + if (connectTimeout != null && !proxies.isEmpty()) { + // the proxy seems to be missing, but we know there are some proxies: + String suggestion = proxies.get(0); + PropertiesEditor editor = gradlePropertiesEditor.getEditor(null, GradleFiles.Kind.USER_PROPERTIES); + problems.add(ProjectProblem.createError(Bundle.Title_GradleProxyNeeded(), + Bundle.ProxyProblemMissing(suggestion), + changeOrRemoveResolver(editor, suggestion) + )); + } + } + + updateProblemList(problems); + } + + List<String> findSystemProxies() { + if (systemProxies != null) { + return systemProxies; + } + return systemProxies = findSystemProxyHosts(); + } + + @NbBundle.Messages({ + "Title_ProxyNotNeeded=Proxy not needed for Gradle.", + "# {0} - proxy name", + "ProxyProblemRemoveProxy=The proxy {0} is unusable, and it seems that no proxies are needed to access the global network. The proxy setting should be removed.", + "ProxyProblemRemoveProxy2=The configured proxy is unusable, and it seems that no proxies are needed to access the global network. The proxy setting should be removed.", + "Title_ProxyMisconfigured=Gradle proxy misconfigured", + "# {0} - offending proxy name", + "# {1} - suggested proxy name", + "ProxyProblemMisconfigured=The gradle proxy {0} is unusable, and a different proxy {1} is in use in the system. The proxy setting should be updated.", + "# {0} - suggested proxy name", + "ProxyProblemMisconfigured2=The configured gradle proxy is unusable, and a different proxy {0} is in use in the system. The proxy setting should be updated.", + "ProxyProblemMisconfigured3=The configured gradle proxy is unusable. Please check project or gradle user settings." + }) + private void maybeChangeProxyFix(String offendingProxy, Collection<ProjectProblem> problems) { + + PropertiesEditor editor = null; + for (String s : ProxyAlertProvider.GRADLE_PROXY_PROPERTIES) { + PropertiesEditor candidate = gradlePropertiesEditor.getEditorFor(s); + if (candidate != null) { + if (editor == null) { + editor = candidate; + } else { + if (candidate != editor) { + LOG.log(Level.FINE, "Multiple property definition sources found: {0}, {1}", new Object[] { + candidate.getFilePath(), editor.getFilePath() + }); + // too complex to handle - issue a generic warning and stop + problems.add( + ProjectProblem.createError(Bundle.Title_ProxyMisconfigured(), Bundle.ProxyProblemMisconfigured3()) + ); + return; + } + } + } + } + + Set<String> hosts = new HashSet<>(); + for (String pn : SYSTEM_PROXY_PROPERTIES) { + String v = System.getProperty(pn); + if (v == null) { + continue; + } + String host = proxyHost(v.trim()); + if (host != null) { + String portNo = System.getProperty(pn.replace("Host", "Port")); // MOI18N + if (portNo != null) { + try { + host = host + ":" + Integer.parseInt(portNo); // MOI18N + } catch (NumberFormatException ex) { + // expected + } + } + hosts.add(host); + } + } + if (hosts.isEmpty()) { + List<String> systemHosts = findSystemProxies(); + if (!systemHosts.isEmpty()) { + hosts.add(systemHosts.iterator().next()); + } + } + + if (hosts.isEmpty()) { + // no proxies are probably required; suggest to remove the proxy + if (editor == null) { + // but the proxy setting is nowhere to be found: just report. + problems.add( + ProjectProblem.createError(Bundle.Title_ProxyMisconfigured(), Bundle.ProxyProblemMisconfigured3()) + ); + } else { + if (editor == null) { + // obtain user properties, possibly not existing + editor = gradlePropertiesEditor.getEditor(null, GradleFiles.Kind.USER_PROPERTIES); + } + problems.add( + ProjectProblem.createError(Bundle.Title_ProxyNotNeeded(), + offendingProxy != null ? + Bundle.ProxyProblemRemoveProxy(offendingProxy) : + Bundle.ProxyProblemRemoveProxy2(), + new ChangeOrRemovePropertyResolver(owner, editor, null, -1)) + ); + } + return; + } + if (hosts.size() == 1) { + if (editor == null) { + // get the editor for user properties - even in the case the file does not exist at all + editor = gradlePropertiesEditor.getEditor(null, GradleFiles.Kind.USER_PROPERTIES); + } + // if there's !1 host, we can define gradle properties to that host + // otherwise, we do not know what proxy host to use + String suggestion = hosts.iterator().next(); + problems.add( + ProjectProblem.createError(Bundle.Title_ProxyMisconfigured(), + offendingProxy != null ? + Bundle.ProxyProblemMisconfigured(offendingProxy, suggestion) : + Bundle.ProxyProblemMisconfigured2(suggestion), + changeOrRemoveResolver(editor, suggestion) + ) + ); + return; + } else { + // we can just offer to open the properties file so the user can use + // his brain. + } + } + + private ProjectProblemResolver changeOrRemoveResolver(PropertiesEditor editor, String suggestion) { + if (suggestion == null) { + // remove + return new ChangeOrRemovePropertyResolver(owner, editor, null, -1); + } + int i = suggestion.indexOf(':'); + String h; + int port = -1; + if (i > 0) { + h = suggestion.substring(0, i); + port = Integer.parseInt(suggestion.substring(i + 1)); + } else { + h = suggestion; + } + return new ChangeOrRemovePropertyResolver(owner, editor, h, port); + } + } + + /** + * Uses ProxySelector to guess a proxy for an external network. If the selector uses PAC, + * it may take significant time to initialize & run the JS scripts, so the method should not + * be run in EDT or some time-bound thread. + * @return list of proxy hosts. + */ + static List<String> findSystemProxyHosts() { + List<String> hosts = new ArrayList<>(); + try { + // try to detect the proxy from ProxySelector. Use well-known root DNS address + // to increase the chance to get to an 'external' network. + List<Proxy> proxies = ProxySelector.getDefault().select(new URI("https", "8.8.8.8", "/", null)); // MOI18N + for (Proxy p : proxies) { + SocketAddress ad = p.address(); + if (ad instanceof InetSocketAddress) { + InetSocketAddress ipv4 = (InetSocketAddress)ad; + if (ipv4.getPort() > 0) { + hosts.add(ipv4.getHostString() + ":" + ipv4.getPort()); // MOI18N + } else { + hosts.add(ipv4.getHostString()); + } + // PENDING: if the failure repeats, maybe try a different proxy ? + break; + } + } + } catch (URISyntaxException ex) { + LOG.log(Level.WARNING, "Unexpected syntax ex", ex); + } + return hosts; + } + + private static GradleReport findReport(Collection<GradleReport> reports, Predicate<GradleReport> predicate) { + for (GradleReport root : reports) { + GradleReport cause = root; + GradleReport previous; + do { + previous = cause; + if (predicate.test(previous)) { + return previous; + } + cause = previous.getCause(); + } while (cause != null && cause != previous); + } + return null; + } + + private Set<String> findProxyHostNames() { + Properties props = gradlePropertiesEditor.ensureGetProperties(); + return findProxyHostNames(props); + } + + private Set<String> findProxyHostNames(Properties props) { + Set<String> hosts = new HashSet<>(); + for (String pn : GRADLE_PROXY_PROPERTIES) { + String v = props.getProperty(pn); + if (v == null) { + continue; + } + String host = proxyHost(v.trim()); + if (host != null) { + hosts.add(host); + } + } + return hosts; + } + + private static String proxyHost(String value) { + if (value == null || value.length() == 0) { + return null; + } + try { + URI u = new URI(value); + String h = u.getHost(); + if (h != null) { + return h; + } + } catch (URISyntaxException ex) { + // expected + } + int s = value.indexOf('/'); // NOI18N + int e = value.indexOf(':'); // NOI18N + if (e == -1) { + e = value.length(); + } + if (s == -1 && e == value.length()) { + return value; + } else { + return value.substring(s + 1, e); + } + } + + private static final String SOCKS_PROXY_HOST = "systemProp.socks.proxyHost"; // NOI18N + + static final String[] GRADLE_PROXY_PROPERTIES = { + "systemProp.http.proxyHost", // NOI18N + "systemProp.https.proxyHost", // NOI18N + SOCKS_PROXY_HOST + }; + + static final String[] SYSTEM_PROXY_PROPERTIES = { + "http.proxyHost", // NOI18N + "https.proxyHost", // NOI18N + "socks.proxyHost", // NOI18N + }; + + static class CachedProperties extends Properties { + private final Map<File, Long> timestamps; + + public CachedProperties(Map<File, Long> timestamps) { + this.timestamps = timestamps; + } + + boolean valid(Collection<File> files) { + if (!timestamps.keySet().containsAll(files) || timestamps.size() != files.size()) { + return false; + } + for (File k : files) { + Long l = timestamps.get(k); + if (l == null || l.longValue() != k.lastModified()) { + return false; + } + } + return true; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists