View Javadoc
1   package com.jsql.util;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.model.exception.JSqlRuntimeException;
5   import org.apache.commons.lang3.StringUtils;
6   import org.apache.commons.lang3.SystemUtils;
7   import org.apache.commons.lang3.exception.ExceptionUtils;
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  import org.json.JSONException;
11  import org.json.JSONObject;
12  
13  import javax.swing.*;
14  import java.awt.*;
15  import java.io.IOException;
16  import java.net.URI;
17  import java.net.http.HttpRequest;
18  import java.net.http.HttpRequest.BodyPublishers;
19  import java.net.http.HttpResponse;
20  import java.net.http.HttpResponse.BodyHandlers;
21  import java.time.Duration;
22  import java.util.Locale;
23  
24  /**
25   * Utility class used to connect to GitHub Rest webservices.
26   * It uses jsql-robot profile to post data to GitHub.
27   */
28  public class GitUtil {
29      
30      /**
31       * Log4j logger sent to view.
32       */
33      private static final Logger LOGGER = LogManager.getRootLogger();
34      
35      /**
36       * Application useful information as json object from GitHub repository.
37       * Used to get current development version and community news.
38       */
39      private JSONObject jsonObject;
40      
41      /**
42       * Define explicit labels to declare method parameters.
43       * Used for code readability only.
44       */
45      public enum ShowOnConsole { YES, NO }
46  
47      private final InjectionModel injectionModel;
48      
49      public GitUtil(InjectionModel injectionModel) {
50          this.injectionModel = injectionModel;
51      }
52  
53      /**
54       * Verify if application is up-to-date against the version on GitHub.
55       * @param displayUpdateMessage YES for manual update verification, hidden otherwise
56       */
57      public void checkUpdate(ShowOnConsole displayUpdateMessage) {
58          if (displayUpdateMessage == ShowOnConsole.YES) {
59              LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("UPDATE_LOADING"));
60          }
61          try {
62              var versionGit = Float.parseFloat(this.callService().getString("version"));
63              if (versionGit > Float.parseFloat(this.injectionModel.getPropertiesUtil().getVersionJsql())) {
64                  LOGGER.log(LogLevelUtil.CONSOLE_ERROR, () -> I18nUtil.valueByKey("UPDATE_NEW_VERSION"));
65              } else if (displayUpdateMessage == ShowOnConsole.YES) {
66                  LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, () -> I18nUtil.valueByKey("UPDATE_UPTODATE"));
67              }
68          } catch (NumberFormatException | JSONException e) {
69              LOGGER.log(LogLevelUtil.CONSOLE_ERROR, I18nUtil.valueByKey("UPDATE_EXCEPTION"));
70          }
71      }
72  
73      /**
74       * Define the body of an issue to send to GitHub for an unhandled exception.
75       * It adds different system data to the body and remove sensible data like
76       * injection URL.
77       * @param threadName name of thread where the exception occurred
78       * @param throwable unhandled exception to report to GitHub
79       */
80      public void sendUnhandledException(String threadName, Throwable throwable) {
81          Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
82          int width = (int) screenSize.getWidth();
83          int height = (int) screenSize.getHeight();
84  
85          var osMetadata = String.join(
86              "\n",
87              String.format(
88                  "jSQL: v%s",
89                  this.injectionModel.getPropertiesUtil().getVersionJsql()
90              ),
91              String.format(
92                  "Java: v%s-%s-%s on %s%s",
93                  SystemUtils.JAVA_VERSION,
94                  SystemUtils.OS_ARCH,
95                  SystemUtils.USER_LANGUAGE,
96                  SystemUtils.JAVA_VENDOR,
97                  SystemUtils.JAVA_RUNTIME_NAME
98              ),
99              String.format(
100                 "OS: %s (v%s)",
101                 SystemUtils.OS_NAME, SystemUtils.OS_VERSION
102             ),
103             String.format(
104                 "Display: %s (%sx%s, %s)",
105                 UIManager.getLookAndFeel().getName(),
106                 width,
107                 height,
108                 Locale.getDefault().getLanguage()
109             ),
110             String.format(
111                 "Strategy: %s",
112                 this.injectionModel.getMediatorStrategy().getStrategy() != null
113                 ? this.injectionModel.getMediatorStrategy().getStrategy().getName()
114                 : "undefined"
115             ),
116             String.format(
117                 "Db engine: %s",
118                 this.injectionModel.getMediatorVendor().getVendor().toString()
119             )
120         );
121         
122         var exceptionText = String.format("Exception on %s%n%s%n", threadName, ExceptionUtils.getStackTrace(throwable).trim());
123         var clientDescription = String.format("```yaml%n%s%n```%n```java%n%s```", osMetadata, exceptionText);
124         
125         clientDescription = clientDescription.replaceAll("(https?://[.a-zA-Z_0-9]*)+", StringUtils.EMPTY);
126         this.sendReport(clientDescription, ShowOnConsole.NO, "Unhandled "+ throwable.getClass().getSimpleName());
127     }
128     
129     /**
130      * Connect to GitHub webservices and create an Issue on the repository.
131      * Used by translation protocol, unhandled exception detection and manual Issue reporting.
132      * @param reportBody text of the Issue
133      * @param showOnConsole in case of manual Issue reporting. Hidden in case of automatic reporting of unhandled exception.
134      * @param reportTitle title of the Issue
135      */
136     public void sendReport(String reportBody, ShowOnConsole showOnConsole, String reportTitle) {
137         if (this.injectionModel.getMediatorUtils().getProxyUtil().isNotLive(showOnConsole)) {
138             return;
139         }
140 
141         String token;
142         try {
143             token = StringUtil.fromHexZip(
144                 this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperty("github.token")
145             );
146         } catch (IOException e) {
147             throw new JSqlRuntimeException(e);
148         }
149 
150         var httpRequest = HttpRequest.newBuilder()
151             .uri(URI.create(this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperty("github.issues.url")))
152             .setHeader("Authorization", "token "+ token)
153             .POST(BodyPublishers.ofString(
154                 new JSONObject()
155                 .put("title", reportTitle)
156                 .put("body", reportBody)
157                 .toString()
158             ))
159             .timeout(Duration.ofSeconds(15))
160             .build();
161             
162         try {
163             HttpResponse<String> response = this.injectionModel.getMediatorUtils().getConnectionUtil().getHttpClient().build().send(httpRequest, BodyHandlers.ofString());
164             this.readGithubResponse(response, showOnConsole);
165         } catch (InterruptedException | IOException e) {
166             if (showOnConsole == ShowOnConsole.YES) {
167                 LOGGER.log(LogLevelUtil.CONSOLE_ERROR, String.format("Error during GitHub report connection: %s", e.getMessage()));
168             }
169             if (e instanceof InterruptedException) {
170                 Thread.currentThread().interrupt();
171             }
172         }
173     }
174     
175     private void readGithubResponse(HttpResponse<String> response, ShowOnConsole showOnConsole) throws IOException {
176         try {
177             if (showOnConsole == ShowOnConsole.YES) {
178                 var jsonObjectResponse = new JSONObject(response.body());
179                 var urlIssue = jsonObjectResponse.getString("html_url");
180                 LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Sent to GitHub: {}", urlIssue);
181             }
182         } catch (Exception e) {
183             throw new IOException("Connection to the GitHub API failed, check your connection or update jSQL");
184         }
185     }
186     
187     /**
188      * Displays news information on the console from GitHub web service.
189      * Infos concern the general roadmap for the application, current development status
190      * and other useful statements for the community.
191      */
192     public void showNews() {
193         try {
194             var news = this.callService().getJSONArray("news");
195             for (var index = 0 ; index < news.length() ; index++) {
196                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, news.get(index));
197             }
198         } catch (JSONException e) {
199             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Connection to the GitHub API failed");
200         }
201     }
202     
203     /**
204      * Instantiate the jsonObject from json data if not already set.
205      * @return jsonObject describing json data
206      */
207     public JSONObject callService() {
208         if (this.jsonObject == null) {
209             String json = this.injectionModel.getMediatorUtils().getConnectionUtil().getSource(
210                 this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperty("github.webservice.url")
211             );
212             // Fix #45349: JSONException on new JSONObject(json)
213             try {
214                 this.jsonObject = new JSONObject(json);
215             } catch (JSONException e) {
216                 try {
217                     this.jsonObject = new JSONObject("{\"version\": \"0\", \"news\": []}");
218                 } catch (JSONException eInner) {
219                     LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Fetching default JSON failed", eInner);
220                 }
221                 LOGGER.log(
222                     LogLevelUtil.CONSOLE_ERROR,
223                     "Fetching configuration from GitHub failed. Wait for service to be available, check your connection or update jSQL"
224                 );
225             }
226         }
227         return this.jsonObject;
228     }
229 }