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