From 395631ba83347729ce01207cff2217d76732f235 Mon Sep 17 00:00:00 2001 From: Fred Bricon Date: Mon, 31 Jul 2023 14:13:28 +0200 Subject: [PATCH] perf: improve Quarkus wizard latency Signed-off-by: Fred Bricon --- .../intellij/quarkus/QuarkusConstants.java | 2 + .../QuarkusCodeEndpointChooserStep.java | 268 ++++++++++++++++-- .../intellij/quarkus/module/QuarkusModel.java | 20 +- .../quarkus/module/QuarkusModelRegistry.java | 129 ++++----- .../quarkus/module/QuarkusModuleBuilder.java | 20 ++ .../quarkus/module/QuarkusModuleInfoStep.java | 245 +++++++++++----- 6 files changed, 511 insertions(+), 173 deletions(-) diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java index 4b3227b3e..097a39ae8 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java @@ -12,6 +12,7 @@ import com.intellij.openapi.util.Key; import com.redhat.devtools.intellij.quarkus.module.QuarkusExtensionsModel; +import com.redhat.devtools.intellij.quarkus.module.QuarkusModel; import com.redhat.devtools.intellij.quarkus.tool.ToolDelegate; public class QuarkusConstants { @@ -24,6 +25,7 @@ public class QuarkusConstants { public final static Key WIZARD_PATH_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".path"); public final static Key WIZARD_EXTENSIONS_MODEL_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".model"); public final static Key WIZARD_ENDPOINT_URL_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".endpointURL"); + public final static Key WIZARD_QUARKUS_STREAMS = Key.create(QuarkusConstants.class.getPackage().getName() + ".streams"); public static final String CONFIG_ROOT_ANNOTATION = "io.quarkus.runtime.annotations.ConfigRoot"; public static final String CONFIG_GROUP_ANNOTATION = "io.quarkus.runtime.annotations.ConfigGroup"; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusCodeEndpointChooserStep.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusCodeEndpointChooserStep.java index 17d4a3220..369fc72e2 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusCodeEndpointChooserStep.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusCodeEndpointChooserStep.java @@ -15,33 +15,40 @@ import com.intellij.ide.util.PropertiesComponent; import com.intellij.ide.util.projectWizard.ModuleWizardStep; import com.intellij.ide.util.projectWizard.WizardContext; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.progress.EmptyProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.ui.ComponentWithBrowseButton; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Disposer; import com.intellij.ui.HyperlinkLabel; -import com.intellij.ui.IdeBorderFactory; import com.intellij.ui.TextFieldWithStoredHistory; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBRadioButton; +import com.intellij.util.ui.AsyncProcessIcon; import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.components.BorderLayoutPanel; import com.redhat.devtools.intellij.quarkus.QuarkusConstants; -import javax.swing.ButtonGroup; -import javax.swing.JComponent; -import javax.swing.JPanel; -import java.awt.BorderLayout; +import javax.swing.*; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.io.IOException; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.QUARKUS_CODE_URL; +import static com.redhat.devtools.intellij.quarkus.module.QuarkusModelRegistry.DEFAULT_TIMEOUT_IN_SEC; -public class QuarkusCodeEndpointChooserStep extends ModuleWizardStep { +public class QuarkusCodeEndpointChooserStep extends ModuleWizardStep implements Disposable { private static final String LAST_ENDPOINT_URL = "quarkus.code.endpoint.url.last"; private static final String ENDPOINT_URL_HISTORY = "quarkus.code.endpoint.url.history"; private final WizardContext wizardContext; @@ -49,8 +56,16 @@ public class QuarkusCodeEndpointChooserStep extends ModuleWizardStep { private final JBRadioButton customRadioButton = new JBRadioButton("Custom:", false); private final TextFieldWithStoredHistory endpointURL = new TextFieldWithStoredHistory(ENDPOINT_URL_HISTORY); private final ComponentWithBrowseButton customUrlWithBrowseButton; + private final JPanel component; + + private Future loadingRequest = null; + + private QuarkusModel streams = null; + + private AsyncProcessIcon spinner = new AsyncProcessIcon("Loading Quarkus data..."); QuarkusCodeEndpointChooserStep(WizardContext wizardContext) { + Disposer.register(wizardContext.getDisposable(), this); this.customUrlWithBrowseButton = new ComponentWithBrowseButton(this.endpointURL, new ActionListener() { public void actionPerformed(ActionEvent e) { try { @@ -62,6 +77,13 @@ public void actionPerformed(ActionEvent e) { } }); + + endpointURL.addActionListener(e -> { + if (customRadioButton.isSelected()) { + updateCustomUrl(); + } + }); + this.wizardContext = wizardContext; String lastServiceUrl = PropertiesComponent.getInstance().getValue(LAST_ENDPOINT_URL, QUARKUS_CODE_URL); if (!lastServiceUrl.equals(QUARKUS_CODE_URL)) { @@ -75,18 +97,27 @@ public void actionPerformed(ActionEvent e) { List history = this.endpointURL.getHistory(); history.remove(QUARKUS_CODE_URL); this.endpointURL.setHistory(history); - this.updateCustomUrl(); + this.component = createComponent(); + } + + @Override + public void _init() { + super._init(); + SwingUtilities.invokeLater(() -> { + this.updateCustomUrl(); + }); } + @Override public JComponent getComponent() { + return component; + } + + public JPanel createComponent() { ButtonGroup group = new ButtonGroup(); group.add(this.defaultRadioButton); group.add(this.customRadioButton); - ActionListener listener = new ActionListener() { - public void actionPerformed(ActionEvent e) { - QuarkusCodeEndpointChooserStep.this.updateCustomUrl(); - } - }; + ActionListener listener = e -> QuarkusCodeEndpointChooserStep.this.updateCustomUrl(); this.defaultRadioButton.addActionListener(listener); this.customRadioButton.addActionListener(listener); FormBuilder builder = new FormBuilder(); @@ -102,7 +133,8 @@ public void actionPerformed(ActionEvent e) { this.customUrlWithBrowseButton.setButtonIcon(AllIcons.Actions.Preview); customPanel.addToCenter(this.customUrlWithBrowseButton); builder.addComponent(customPanel); - builder.addTooltip("Make sure your network connection is active before continuing."); + builder.addTooltip("Make sure your network connection is active."); + builder.addComponentFillVertically(spinner, 10); JPanel panel = new JPanel(new BorderLayout()); panel.add(builder.getPanel(), "North"); panel.setBorder(JBUI.Borders.emptyLeft(20)); @@ -115,34 +147,218 @@ private void updateCustomUrl() { this.endpointURL.getTextEditor().setEditable(custom); this.endpointURL.setEnabled(custom); this.customUrlWithBrowseButton.setButtonEnabled(custom); + if (loadingRequest != null) { + loadingRequest.cancel(true); + } + loadingRequest = loadStreams(); + } + +// private HyperlinkLabel urlLabel; +// private TextFieldWithStoredHistory urlTextField; + +// protected JComponent createCenterPanel() { +// JPanel panel = new JPanel(new GridBagLayout()); +// GridBagConstraints constraints = new GridBagConstraints(); +// constraints.gridx = 0; +// constraints.gridy = 0; +// constraints.anchor = GridBagConstraints.WEST; +// +// // URL Label +// urlLabel = new HyperlinkLabel(QUARKUS_CODE_URL); +// urlLabel.setHyperlinkText(QUARKUS_CODE_URL); +// urlLabel.setHyperlinkTarget(QUARKUS_CODE_URL); +// +// // Modify Icon +// JLabel modifyIcon = new JLabel(AllIcons.General.Inline_edit); +// modifyIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); +// modifyIcon.setToolTipText("Modify"); +// modifyIcon.addMouseListener(new MouseAdapter() { +// @Override +// public void mouseClicked(MouseEvent e) { +// toggleEditing(); +// } +// }); +// +// // Server URL label +// JBLabel serverUrlLabel = new JBLabel("Quarkus Code URL:"); +// serverUrlLabel.setLabelFor(urlLabel); +// +// // Server URL text field with history +// urlTextField = new TextFieldWithStoredHistory(ENDPOINT_URL_HISTORY); +// urlTextField.setText(urlLabel.getText()); +// urlTextField.setToolTipText("Enter URL"); +// urlTextField.setMinimumAndPreferredWidth(200); +// +// urlTextField.addFocusListener(new FocusAdapter() { +// @Override +// public void focusLost(FocusEvent e) { +// if (!urlTextField.isVisible() || +// //ignore if we lost focus in favor of a subcomponent +// isChildOf(e.getOppositeComponent(), urlTextField)) { +// return; +// } +// toggleEditing(); +// urlLabel.setHyperlinkText(urlTextField.getText()); +// urlLabel.setHyperlinkTarget(urlTextField.getText()); +// } +// +// boolean isChildOf(Component component, Component parent) { +// Component currentComponent = component; +// while (currentComponent != null) { +// if (currentComponent == urlTextField) { +// return true; +// } +// currentComponent = currentComponent.getParent(); +// } +// return false; +// } +// }); +// urlTextField.addActionListener(new ActionListener() { +// @Override +// public void actionPerformed(ActionEvent e) { +// urlLabel.setHyperlinkText(urlTextField.getText()); +// urlLabel.setHyperlinkTarget(urlTextField.getText()); +// toggleEditing(); +// } +// }); +// toggleEditing(); +// +// panel.add(serverUrlLabel, constraints); +// constraints.gridx++; +// panel.add(urlLabel, constraints); +// panel.add(urlTextField, constraints); +// constraints.gridx++; +// panel.add(modifyIcon, constraints); +// constraints.gridx++; +// panel.add(spinner, constraints); +// constraints.gridx = 1; +// constraints.gridy++; +// +// return panel; +// } + + private Future loadStreams() { + final String endpoint = getSelectedEndpointUrl(); + if (endpoint == null || endpoint.isBlank()) { + return null; + } + showSpinner(); + return ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + return QuarkusModelRegistry.INSTANCE.loadStreams(endpoint, new EmptyProgressIndicator()); + } catch (Exception e) { + ApplicationManager.getApplication().invokeLater(() -> { + Messages.showErrorDialog("Failed to load Quarkus platform versions from "+endpoint+":\n"+ e.getMessage(), "Error loading Quarkus platform versions"); + }, ModalityState.stateForComponent(getComponent()) ); + } finally { + ApplicationManager.getApplication().invokeLater(() -> { + hideSpinner(); + }, ModalityState.stateForComponent(getComponent())); + } + return null; + }); + } + + private void showSpinner() { + spinner.setVisible(true); + spinner.resume(); + } + + private void hideSpinner() { + spinner.suspend(); + spinner.setVisible(false); } + +// private void toggleEditing() { +// if (urlTextField.isVisible()) { +// urlTextField.setEnabled(false); +// urlTextField.setVisible(false); +// urlLabel.setVisible(true); +// loadingRequest = loadStreams(); +// } else { +// urlTextField.setText(urlLabel.getText()); +// urlTextField.setVisible(true); +// urlTextField.setEnabled(true); +// urlTextField.requestFocus(); +// urlTextField.selectText(); +// urlLabel.setVisible(false); +// } +// } + + @Override public boolean validate() throws ConfigurationException { - if (this.defaultRadioButton.isSelected()) { - return true; - } else { + if (this.customRadioButton.isSelected()) { String serviceUrl = this.endpointURL.getText(); if (serviceUrl.isEmpty()) { throw new ConfigurationException("Quarkus Code endpoint URL must be set"); - } else if (!serviceUrl.startsWith("http://") && !serviceUrl.startsWith("https://")) { + } else if (!serviceUrl.matches("^https?://.*")) { throw new ConfigurationException("Invalid custom Quarkus Code endpoint URL"); - } else { - try { - QuarkusModelRegistry.INSTANCE.load(serviceUrl, new EmptyProgressIndicator()); - return true; - } catch (IOException var3) { - throw new ConfigurationException("Invalid Custom Quarkus Code endpoint URL"); - } } } + try { + boolean requestComplete = checkRequestComplete(); + if (!requestComplete) { + return false; + } + streams = loadingRequest.get(DEFAULT_TIMEOUT_IN_SEC, TimeUnit.SECONDS); + if (streams == null) { + throw new ConfigurationException("Failed to load Quarkus platform streams"); + } + } catch (Exception e) { + throw new ConfigurationException("Failed to load Quarkus platform streams", e, "Quarkus streams loading failure"); + } + return true; } + private boolean checkRequestComplete() throws ExecutionException, InterruptedException { + if (loadingRequest == null) { + loadingRequest = loadStreams(); + } + if (loadingRequest.isDone()){ + return true; + } + ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { + // Wait for the loading to finish + try { + loadingRequest.get(DEFAULT_TIMEOUT_IN_SEC, TimeUnit.SECONDS); + } catch (Exception e) { + ApplicationManager.getApplication().invokeLater(() -> { + String message = e.getMessage(); + if (e instanceof TimeoutException) { + message = "Process timeout (exceeded "+DEFAULT_TIMEOUT_IN_SEC+" seconds)"; + } + if (message == null) { + message = e.getClass().getName(); + } + Messages.showErrorDialog("Failed to load Quarkus platform versions from "+getSelectedEndpointUrl()+":\n"+ message, "Error loading Quarkus platform versions"); + }, ModalityState.stateForComponent(getComponent()) ); + } + }, "Loading Quarkus platform streams...", true, wizardContext.getProject()); + return false; + } + + @Override public void updateDataModel() { - String endpointURL = this.customRadioButton.isSelected() ? this.endpointURL.getText() : QUARKUS_CODE_URL; + String endpointURL = getSelectedEndpointUrl(); if (!Comparing.strEqual(this.wizardContext.getUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY), endpointURL)) { this.endpointURL.addCurrentTextToHistory(); this.wizardContext.putUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY, endpointURL); + this.wizardContext.putUserData(QuarkusConstants.WIZARD_QUARKUS_STREAMS, streams); PropertiesComponent.getInstance().setValue(LAST_ENDPOINT_URL, endpointURL); } } + + private String getSelectedEndpointUrl() { + return this.customRadioButton.isSelected() ? this.endpointURL.getText() : QUARKUS_CODE_URL; + } + + @Override + public void dispose() { + if (loadingRequest != null) { + loadingRequest.cancel(true); + loadingRequest = null; + } + streams = null; + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModel.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModel.java index b1338505f..4c38cb241 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModel.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModel.java @@ -10,14 +10,18 @@ ******************************************************************************/ package com.redhat.devtools.intellij.quarkus.module; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; -public class QuarkusModel { +public class QuarkusModel implements Disposable { private String baseURL; private List streams; @@ -32,7 +36,16 @@ public List getStreams() { return streams; } + //Used in Unit test public QuarkusExtensionsModel getExtensionsModel(String key, ProgressIndicator indicator) throws IOException { + try { + return ApplicationManager.getApplication().executeOnPooledThread(() -> loadExtensionsModel(key, indicator)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } + } + + public QuarkusExtensionsModel loadExtensionsModel(String key, ProgressIndicator indicator) throws IOException { QuarkusExtensionsModel extensionsModel = extensionsModelMap.get(key); if (extensionsModel == null) { extensionsModel = QuarkusModelRegistry.loadExtensionsModel(baseURL, key, indicator); @@ -40,4 +53,9 @@ public QuarkusExtensionsModel getExtensionsModel(String key, ProgressIndicator i } return extensionsModel; } + + @Override + public void dispose() { + extensionsModelMap.clear(); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModelRegistry.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModelRegistry.java index 8d3a7bae7..6ed34bd42 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModelRegistry.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModelRegistry.java @@ -22,6 +22,8 @@ import com.intellij.util.Urls; import com.intellij.util.io.HttpRequests; import com.intellij.util.io.RequestBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.zeroturnaround.zip.ZipUtil; import java.io.File; @@ -31,26 +33,19 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_ARTIFACT_ID_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_CLASSNAME_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_EXTENSIONS_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_GROUP_ID_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_NO_EXAMPLES_DEFAULT; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_NO_EXAMPLES_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_PATH_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_VALUE; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_QUARKUS_IO_CLIENT_NAME_HEADER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_QUARKUS_IO_CLIENT_NAME_HEADER_VALUE; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_STREAM_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_TOOL_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.CODE_VERSION_PARAMETER_NAME; -import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.PLATFORM_ONLY_PARAMETER; +import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.*; public class QuarkusModelRegistry { - private static final String EXTENSIONS_SUFFIX = "/api/extensions/stream/"; + private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusModelRegistry.class); + /** + * Default request timeout in seconds + */ + public static final int DEFAULT_TIMEOUT_IN_SEC = 10; + private static final String EXTENSIONS_SUFFIX = "/api/extensions/stream/"; private static final String STREAMS_SUFFIX = "/api/streams"; public static final QuarkusModelRegistry INSTANCE = new QuarkusModelRegistry(); @@ -72,33 +67,40 @@ private static String computeUserAgent() { } public QuarkusModel load(String endPointURL, ProgressIndicator indicator) throws IOException { + try { + return ApplicationManager.getApplication().executeOnPooledThread(() -> loadStreams(endPointURL, indicator)).get(DEFAULT_TIMEOUT_IN_SEC, TimeUnit.SECONDS); + } catch (InterruptedException|ExecutionException|TimeoutException e) { + throw new IOException(e); + } + } + + public QuarkusModel loadStreams(String endPointURL, ProgressIndicator indicator) throws IOException { + long start = System.currentTimeMillis(); String normalizedEndPointURL = normalizeURL(endPointURL); - indicator.setText("Looking up Quarkus model from endpoint " + endPointURL); - QuarkusModel model = models.get(endPointURL); - if (model == null) { - indicator.setText("Loading Quarkus model from endpoint " + endPointURL); - try { - model = ApplicationManager.getApplication().executeOnPooledThread(() -> HttpRequests.request(normalizedEndPointURL + STREAMS_SUFFIX).userAgent(USER_AGENT).tuner(request -> { + indicator.setText("Looking up Quarkus streams from endpoint " + endPointURL); + QuarkusModel streamModel = models.get(endPointURL); + if (streamModel != null) { + return streamModel; + } + indicator.setText("Loading Quarkus streams from endpoint " + endPointURL); + streamModel = HttpRequests.request(normalizedEndPointURL + STREAMS_SUFFIX) + .userAgent(USER_AGENT) + .tuner(request -> { request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_NAME_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_NAME_HEADER_VALUE); request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_VALUE); - }).connect(request -> { - try (Reader reader = request.getReader(indicator)) { - List streams = mapper.readValue(reader, new TypeReference>() { - }); - QuarkusModel newModel = new QuarkusModel(normalizedEndPointURL, streams); - return newModel; - } catch (IOException e) { - throw new ProcessCanceledException(e); - } - })).get(); - } catch (InterruptedException|ExecutionException e) { - throw new IOException(e); - } - } - if (model == null) { - throw new IOException(); - } - return model; + }) + .connect(request -> { + try (Reader reader = request.getReader(indicator)) { + List streams = mapper.readValue(reader, new TypeReference>() { + }); + QuarkusModel model = new QuarkusModel(normalizedEndPointURL, streams); + long elapsed = System.currentTimeMillis() - start; + LOGGER.info("Loaded Quarkus streams in {} ms", elapsed); + return model; + } + }); + models.put(endPointURL, streamModel); + return streamModel; } private static String normalizeURL(String endPointURL) { @@ -110,30 +112,25 @@ private static String normalizeURL(String endPointURL) { } public static QuarkusExtensionsModel loadExtensionsModel(String endPointURL, String key, ProgressIndicator indicator) throws IOException { + long start = System.currentTimeMillis(); String normalizedEndPointURL = normalizeURL(endPointURL); indicator.setText("Looking up Quarkus extensions from endpoint " + endPointURL + " and key " + key); - QuarkusExtensionsModel model = null; - try { - model = ApplicationManager.getApplication().executeOnPooledThread(() -> HttpRequests.request(normalizedEndPointURL + EXTENSIONS_SUFFIX + key + "?" + PLATFORM_ONLY_PARAMETER + "=false").userAgent(USER_AGENT).tuner(request -> { - request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_NAME_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_NAME_HEADER_VALUE); - request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_VALUE); - }).connect(request -> { - try (Reader reader = request.getReader(indicator)) { - List extensions = mapper.readValue(reader, new TypeReference>() { - }); - QuarkusExtensionsModel newModel = new QuarkusExtensionsModel(key, extensions); - return newModel; - } catch (IOException e) { - throw new ProcessCanceledException(e); - } - })).get(); - } catch (InterruptedException|ExecutionException e) { - throw new IOException(e); - } - if (model == null) { - throw new IOException(); - } - return model; + String query = normalizedEndPointURL + EXTENSIONS_SUFFIX + key + "?" + PLATFORM_ONLY_PARAMETER + "=false"; + return HttpRequests.request(query).userAgent(USER_AGENT).tuner(request -> { + request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_NAME_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_NAME_HEADER_VALUE); + request.setRequestProperty(CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_VALUE); + }).connect(request -> { + try (Reader reader = request.getReader(indicator)) { + List extensions = mapper.readValue(reader, new TypeReference>() { + }); + QuarkusExtensionsModel newModel = new QuarkusExtensionsModel(key, extensions); + long elapsed = System.currentTimeMillis() - start; + LOGGER.info("Loaded Quarkus extensions in {} ms", elapsed); + return newModel; + } catch (IOException e) { + throw new ProcessCanceledException(e); + } + }); } public static void zip(String endpoint, String tool, String groupId, String artifactId, String version, @@ -182,10 +179,4 @@ private static String buildParameters(String tool, String groupId, String artifa json.addProperty(CODE_STREAM_PARAMETER_NAME, model.getKey()); return json.toString(); } - - public static void zip(String endpoint, String tool, String groupId, String artifactId, String version, - String className, String path, QuarkusExtensionsModel model, File output) throws IOException { - zip(endpoint, tool, groupId, artifactId, version, className, path, model, output, true); - } - - } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleBuilder.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleBuilder.java index 61b5abb1a..9c772068b 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleBuilder.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleBuilder.java @@ -20,6 +20,10 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleWithNameAlreadyExists; import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.JavaSdkVersion; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.ui.configuration.ModulesProvider; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.InvalidDataException; @@ -36,6 +40,7 @@ import javax.swing.Icon; import java.io.File; import java.io.IOException; +import java.util.concurrent.ExecutionException; public class QuarkusModuleBuilder extends JavaModuleBuilder { @@ -103,6 +108,21 @@ public Module createModule(@NotNull ModifiableModuleModel moduleModel) throws In } } + + @Override + public void setupRootModel(@NotNull ModifiableRootModel rootModel) throws ConfigurationException { + // Set the minimum JDK version to 11 + var sdk = rootModel.getSdk(); + if (sdk != null) { + @Nullable JavaSdkVersion minJDK = JavaSdkVersion.fromVersionString("11"); + JavaSdkVersion selectedVersion = JavaSdk.getInstance().getVersion(sdk); + if (minJDK != null && !selectedVersion.isAtLeast(minJDK)) { + //rootModel.setSdk(JavaSdk.getInstance().); + } + } + super.setupRootModel(rootModel); + } + private void processDownload() throws IOException { File moduleFile = new File(getContentEntryPath()); QuarkusModelRegistry.zip(wizardContext.getUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY), diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleInfoStep.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleInfoStep.java index 0499c6b4c..c0c5b7a93 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleInfoStep.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusModuleInfoStep.java @@ -13,10 +13,15 @@ import com.intellij.ide.util.projectWizard.ModuleWizardStep; import com.intellij.ide.util.projectWizard.WizardContext; import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Disposer; import com.intellij.ui.CollectionComboBoxModel; import com.intellij.ui.ColoredListCellRenderer; import com.intellij.ui.ScrollPaneFactory; @@ -24,6 +29,7 @@ import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLoadingPanel; import com.intellij.ui.components.JBTextField; +import com.intellij.util.ui.AsyncProcessIcon; import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.redhat.devtools.intellij.quarkus.QuarkusConstants; @@ -32,14 +38,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.JComponent; -import javax.swing.JList; -import javax.swing.SwingUtilities; +import javax.swing.*; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; -import java.awt.BorderLayout; -import java.io.IOException; +import java.awt.*; import java.util.Arrays; +import java.util.concurrent.*; + +import static com.redhat.devtools.intellij.quarkus.module.QuarkusModelRegistry.DEFAULT_TIMEOUT_IN_SEC; public class QuarkusModuleInfoStep extends ModuleWizardStep implements Disposable { private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusModuleInfoStep.class); @@ -62,13 +68,20 @@ public class QuarkusModuleInfoStep extends ModuleWizardStep implements Disposabl private JBTextField pathField; + private AsyncProcessIcon spinner = new AsyncProcessIcon("Loading Quarkus data..."); + private final WizardContext context; private QuarkusModel model; + private Future extensionsModelRequest; + private QuarkusExtensionsModel extensionsModel; + private CollectionComboBoxModel streamModel; + private EmptyProgressIndicator indicator; public QuarkusModuleInfoStep(WizardContext context) { + Disposer.register(context.getDisposable(), this); this.context = context; } @@ -86,97 +99,129 @@ public void updateDataModel() { context.putUserData(QuarkusConstants.WIZARD_VERSION_KEY, versionField.getText()); context.putUserData(QuarkusConstants.WIZARD_CLASSNAME_KEY, classNameField.getText()); context.putUserData(QuarkusConstants.WIZARD_PATH_KEY, pathField.getText()); - if (extensionsModel != null) { - context.putUserData(QuarkusConstants.WIZARD_EXTENSIONS_MODEL_KEY, extensionsModel); - } else { - throw new RuntimeException("Unable to get extensions"); - } + context.putUserData(QuarkusConstants.WIZARD_EXTENSIONS_MODEL_KEY, extensionsModel); } @Override public void dispose() { + if (model != null) { + model.dispose(); + } + if (extensionsModelRequest != null) { + extensionsModelRequest.cancel(true); + } + model = null; + } + @Override + public void disposeUIResources() { + super.disposeUIResources(); } @Override public void _init() { panel.setBorder(JBUI.Borders.empty(20)); - ProgressIndicator indicator = new EmptyProgressIndicator() { + indicator = new EmptyProgressIndicator() { @Override public void setText(String text) { - SwingUtilities.invokeLater(() -> panel.setLoadingText(text)); + SwingUtilities.invokeLater(() -> panel.setLoadingText(text)); } }; - try { - model = QuarkusModelRegistry.INSTANCE.load(context.getUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY), indicator); - final FormBuilder formBuilder = new FormBuilder(); - final CollectionComboBoxModel streamModel = new CollectionComboBoxModel<>(model.getStreams()); - streamModel.setSelectedItem(model.getStreams().stream().filter(QuarkusStream::isRecommended).findFirst().orElse(model.getStreams().get(0))); - streamModel.addListDataListener(new ListDataListener() { - @Override - public void intervalAdded(ListDataEvent e) { - } + model = context.getUserData(QuarkusConstants.WIZARD_QUARKUS_STREAMS); + final FormBuilder formBuilder = new FormBuilder(); + streamModel = new CollectionComboBoxModel<>(model.getStreams()); + streamModel.setSelectedItem(model.getStreams().stream().filter(QuarkusStream::isRecommended).findFirst().orElse(model.getStreams().get(0))); + streamModel.addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { + } - @Override - public void intervalRemoved(ListDataEvent e) { - } + @Override + public void intervalRemoved(ListDataEvent e) { + } - @Override - public void contentsChanged(ListDataEvent e) { - try { - loadExtensionsModel(streamModel, indicator); - } catch (IOException ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - } - } - }); - loadExtensionsModel(streamModel, indicator); - streamComboBox = new ComboBox<>(streamModel); - streamComboBox.setRenderer(new ColoredListCellRenderer() { - @Override - protected void customizeCellRenderer(@NotNull JList list, QuarkusStream stream, int index, boolean selected, boolean hasFocus) { - if (stream.isRecommended()) { - this.append(stream.getPlatformVersion(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, true); - } else { - this.append(stream.getPlatformVersion(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true); - } - if (stream.getStatus() != null) { - this.append(" ").append(stream.getStatus()); - } + @Override + public void contentsChanged(ListDataEvent e) { + loadExtensionsModel(streamModel, indicator); + } + }); + + streamComboBox = new ComboBox<>(streamModel); + streamComboBox.setRenderer(new ColoredListCellRenderer() { + @Override + protected void customizeCellRenderer(@NotNull JList list, QuarkusStream stream, int index, boolean selected, boolean hasFocus) { + if (stream.isRecommended()) { + this.append(stream.getPlatformVersion(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, true); + } else { + this.append(stream.getPlatformVersion(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true); } - }); - formBuilder.addLabeledComponent("Quarkus stream:", streamComboBox); - final CollectionComboBoxModel toolModel = new CollectionComboBoxModel<>(Arrays.asList(ToolDelegate.getDelegates())); - toolComboBox = new ComboBox<>(toolModel); - toolComboBox.setRenderer(new ColoredListCellRenderer() { - @Override - protected void customizeCellRenderer(@NotNull JList list, ToolDelegate toolDelegate, int index, boolean selected, boolean hasFocus) { - this.append(toolDelegate.getDisplay()); + if (stream.getStatus() != null) { + this.append(" ").append(stream.getStatus()); } - }); - formBuilder.addLabeledComponent("Tool:", toolComboBox); - exampleField = new JBCheckBox("If selected, project will contain sample code from extensions that suppport codestarts.", true); - formBuilder.addLabeledComponent("Example code:", exampleField); - groupIdField = new JBTextField("org.acme"); - formBuilder.addLabeledComponent("Group:", groupIdField); - artifactIdField = new JBTextField("code-with-quarkus"); - formBuilder.addLabeledComponent("Artifact:", artifactIdField); - versionField = new JBTextField("1.0.0-SNAPSHOT"); - formBuilder.addLabeledComponent("Version:", versionField); - classNameField = new JBTextField("org.acme.ExampleResource"); - formBuilder.addLabeledComponent("Class name:", classNameField); - pathField = new JBTextField("/hello"); - formBuilder.addLabeledComponent("Path:", pathField); - panel.add(ScrollPaneFactory.createScrollPane(formBuilder.getPanel(), true), "North"); - } catch (IOException e) { - LOGGER.error(e.getLocalizedMessage(), e); - throw new RuntimeException(e); - } + } + }); + //formBuilder.addLabeledComponent("Quarkus stream:", streamComboBox); +// JPanel streamPanel = new JPanel(new BorderLayout()); +// streamPanel.add(streamComboBox, BorderLayout.WEST); +// streamPanel.add(spinner); + JComponent streamComponent = new JComponent() {}; + streamComponent.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + streamComponent.add(streamComboBox); + streamComponent.add(spinner); + streamComponent.add(Box.createHorizontalGlue()); + + //streamComponent.add(new JLabel("Quarkus stream:"), BorderLayout.WEST); + //streamComponent.add(streamPanel, BorderLayout.CENTER); + + formBuilder.addLabeledComponent("Quarkus stream:", streamComponent); + + final CollectionComboBoxModel toolModel = new CollectionComboBoxModel<>(Arrays.asList(ToolDelegate.getDelegates())); + toolComboBox = new ComboBox<>(toolModel); + toolComboBox.setRenderer(new ColoredListCellRenderer() { + @Override + protected void customizeCellRenderer(@NotNull JList list, ToolDelegate toolDelegate, int index, boolean selected, boolean hasFocus) { + this.append(toolDelegate.getDisplay()); + } + }); + formBuilder.addLabeledComponent("Tool:", toolComboBox); + exampleField = new JBCheckBox("If selected, project will contain sample code from extensions that suppport codestarts.", true); + formBuilder.addLabeledComponent("Example code:", exampleField); + groupIdField = new JBTextField("org.acme"); + formBuilder.addLabeledComponent("Group:", groupIdField); + artifactIdField = new JBTextField("code-with-quarkus"); + formBuilder.addLabeledComponent("Artifact:", artifactIdField); + versionField = new JBTextField("1.0.0-SNAPSHOT"); + formBuilder.addLabeledComponent("Version:", versionField); + classNameField = new JBTextField("org.acme.ExampleResource"); + formBuilder.addLabeledComponent("Class name:", classNameField); + pathField = new JBTextField("/hello"); + formBuilder.addLabeledComponent("Path:", pathField); + panel.add(ScrollPaneFactory.createScrollPane(formBuilder.getPanel(), true), "North"); + hideSpinner(); + extensionsModelRequest = loadExtensionsModel(streamModel, indicator); } - private void loadExtensionsModel(CollectionComboBoxModel streamModel, ProgressIndicator indicator) throws IOException { - extensionsModel = model.getExtensionsModel(((QuarkusStream) streamModel.getSelectedItem()).getKey(), indicator); + private Future loadExtensionsModel(CollectionComboBoxModel streamModel, ProgressIndicator indicator) { + String key = ((QuarkusStream) streamModel.getSelectedItem()).getKey(); + if (key == null) { + return null; + } + showSpinner(); + return ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + return model.loadExtensionsModel(key, indicator); + } catch (Exception e) { + ApplicationManager.getApplication().invokeLater(() -> { + Messages.showErrorDialog("Failed to load extensions for Quarkus "+key + ":\n"+ e.getMessage(), "Error loading Quarkus extensions"); + }, ModalityState.stateForComponent(getComponent()) ); + } finally { + ApplicationManager.getApplication().invokeLater(() -> { + hideSpinner(); + }, ModalityState.stateForComponent(getComponent())); + } + return null; + }); } @Override @@ -190,9 +235,55 @@ public boolean validate() throws ConfigurationException { if (versionField.getText().isEmpty()) { throw new ConfigurationException("Version must be specified"); } - if (extensionsModel == null) { - throw new ConfigurationException("Unable to get extensions for this stream"); + try { + boolean requestComplete = checkRequestComplete(); + if (!requestComplete) { + return false; + } + extensionsModel = extensionsModelRequest.get(DEFAULT_TIMEOUT_IN_SEC, TimeUnit.SECONDS); + if (extensionsModel == null) { + throw new ConfigurationException("Failed to load Quarkus extensions"); + } + } catch (Exception e) { + throw new ConfigurationException("Failed to load Quarkus extensions", e, "Quarkus extensions loading failure"); } return true; } + + private void showSpinner() { + spinner.setVisible(true); + spinner.resume(); + } + + private void hideSpinner() { + spinner.setVisible(false); + spinner.suspend(); + } + + private boolean checkRequestComplete() { + if (extensionsModelRequest == null) { + extensionsModelRequest = loadExtensionsModel(streamModel, indicator); + } + if (extensionsModelRequest.isDone()) { + return true; + } + ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { + // Wait for the loading to finish + try { + extensionsModelRequest.get(DEFAULT_TIMEOUT_IN_SEC, TimeUnit.SECONDS); + } catch (Exception e) { + ApplicationManager.getApplication().invokeLater(() -> { + String message = e.getMessage(); + if (e instanceof TimeoutException) { + message = "Process timeout (exceeded " + DEFAULT_TIMEOUT_IN_SEC + " seconds)"; + } + if (message == null) { + message = e.getClass().getName(); + } + Messages.showErrorDialog("Failed to load Quarkus extensions:\n" + message, "Error loading Quarkus extensions versions"); + }, ModalityState.stateForComponent(getComponent())); + } + }, "Loading Quarkus extensions...", true, context.getProject()); + return false; + } }