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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]
For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists