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

Reply via email to