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 58a935e6e..cd902c15e 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusConstants.java @@ -14,6 +14,7 @@ import com.redhat.devtools.intellij.quarkus.projectWizard.QuarkusExtensionsModel; import com.redhat.devtools.intellij.quarkus.projectWizard.QuarkusModel; import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; +import org.jetbrains.jps.model.java.JdkVersionDetector; public class QuarkusConstants { public final static Key WIZARD_TOOL_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".tool"); @@ -24,8 +25,10 @@ public class QuarkusConstants { public final static Key WIZARD_CLASSNAME_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".className"); 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_JAVA_VERSION_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".javaVersion"); 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 final static Key WIZARD_JDK_INFO_KEY = Key.create(QuarkusConstants.class.getPackage().getName() + ".jdkVersionInfo"); public static final String CONFIG_ROOT_ANNOTATION = "io.quarkus.runtime.annotations.ConfigRoot"; public static final String CONFIG_GROUP_ANNOTATION = "io.quarkus.runtime.annotations.ConfigGroup"; @@ -102,6 +105,8 @@ public class QuarkusConstants { public static final String CODE_CLASSNAME_PARAMETER_NAME = "className"; + public static final String CODE_JAVA_VERSION_PARAMETER_NAME = "javaVersion"; + public static final String CODE_PATH_PARAMETER_NAME = "path"; public static final String CODE_NO_EXAMPLES_NAME = "noExamples"; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusCodeEndpointChooserStep.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusCodeEndpointChooserStep.java index 4a221cd3c..8b0c9e9a6 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusCodeEndpointChooserStep.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusCodeEndpointChooserStep.java @@ -245,6 +245,7 @@ private boolean checkRequestComplete() throws ExecutionException, InterruptedExc @Override public void updateDataModel() { 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); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistry.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistry.java index 51305818f..44652e7d1 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistry.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistry.java @@ -22,6 +22,7 @@ import com.intellij.util.Urls; import com.intellij.util.io.HttpRequests; import com.intellij.util.io.RequestBuilder; +import com.redhat.qute.utils.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zeroturnaround.zip.ZipUtil; @@ -43,6 +44,20 @@ */ public class QuarkusModelRegistry { + public static class CreateQuarkusProjectRequest { + public String endpoint; + public String tool; + public String groupId; + public String artifactId; + public String version; + public String className; + public String path; + public int javaVersion; + public QuarkusExtensionsModel model; + public File output; + public boolean codeStarts; + } + private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusModelRegistry.class); /** * Default request timeout in seconds @@ -127,7 +142,7 @@ public static QuarkusExtensionsModel loadExtensionsModel(String endPointURL, Str 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>() { + List extensions = mapper.readValue(reader, new TypeReference<>() { }); QuarkusExtensionsModel newModel = new QuarkusExtensionsModel(key, extensions); long elapsed = System.currentTimeMillis() - start; @@ -139,10 +154,9 @@ public static QuarkusExtensionsModel loadExtensionsModel(String endPointURL, Str }); } - public static void zip(String endpoint, String tool, String groupId, String artifactId, String version, - String className, String path, QuarkusExtensionsModel model, File output, boolean codeStarts) throws IOException { - Url url = Urls.newFromEncoded(normalizeURL(endpoint) + "/api/download"); - String body = buildParameters(tool, groupId, artifactId, version, className, path, model, codeStarts); + public static void zip(CreateQuarkusProjectRequest createQuarkusProjectRequest) throws IOException { + Url url = Urls.newFromEncoded(normalizeURL(createQuarkusProjectRequest.endpoint) + "/api/download"); + String body = buildParameters(createQuarkusProjectRequest); RequestBuilder builder = HttpRequests.post(url.toString(), HttpRequests.JSON_CONTENT_TYPE).userAgent(QuarkusModelRegistry.USER_AGENT).tuner(connection -> { connection.setRequestProperty(CODE_QUARKUS_IO_CLIENT_NAME_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_NAME_HEADER_VALUE); connection.setRequestProperty(CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_NAME, CODE_QUARKUS_IO_CLIENT_CONTACT_EMAIL_HEADER_VALUE); @@ -150,7 +164,7 @@ public static void zip(String endpoint, String tool, String groupId, String arti try { if (ApplicationManager.getApplication().executeOnPooledThread(() -> builder.connect(request -> { request.write(body); - ZipUtil.unpack(request.getInputStream(), output, name -> { + ZipUtil.unpack(request.getInputStream(), createQuarkusProjectRequest.output, name -> { int index = name.indexOf('/'); return name.substring(index); }); @@ -163,26 +177,31 @@ public static void zip(String endpoint, String tool, String groupId, String arti } } - private static String buildParameters(String tool, String groupId, String artifactId, String version, - String className, String path, QuarkusExtensionsModel model, - boolean codeStarts) { + private static String buildParameters(CreateQuarkusProjectRequest createQuarkusProjectRequest) { JsonObject json = new JsonObject(); - json.addProperty(CODE_TOOL_PARAMETER_NAME, tool); - json.addProperty(CODE_GROUP_ID_PARAMETER_NAME, groupId); - json.addProperty(CODE_ARTIFACT_ID_PARAMETER_NAME, artifactId); - json.addProperty(CODE_VERSION_PARAMETER_NAME, version); - json.addProperty(CODE_CLASSNAME_PARAMETER_NAME, className); - json.addProperty(CODE_PATH_PARAMETER_NAME, path); - if (!codeStarts) { + json.addProperty(CODE_TOOL_PARAMETER_NAME, createQuarkusProjectRequest.tool); + json.addProperty(CODE_GROUP_ID_PARAMETER_NAME, createQuarkusProjectRequest.groupId); + json.addProperty(CODE_ARTIFACT_ID_PARAMETER_NAME, createQuarkusProjectRequest.artifactId); + json.addProperty(CODE_VERSION_PARAMETER_NAME, createQuarkusProjectRequest.version); + if (!StringUtils.isEmpty(createQuarkusProjectRequest.className)) { + json.addProperty(CODE_CLASSNAME_PARAMETER_NAME, createQuarkusProjectRequest.className); + } + if (!StringUtils.isEmpty(createQuarkusProjectRequest.path)) { + json.addProperty(CODE_PATH_PARAMETER_NAME, createQuarkusProjectRequest.path); + } + if (createQuarkusProjectRequest.javaVersion > 0) { + json.addProperty(CODE_JAVA_VERSION_PARAMETER_NAME, createQuarkusProjectRequest.javaVersion); + } + if (!createQuarkusProjectRequest.codeStarts) { json.addProperty(CODE_NO_EXAMPLES_NAME, CODE_NO_EXAMPLES_DEFAULT); } JsonArray extensions = new JsonArray(); - model.getCategories().stream().flatMap(category -> category.getExtensions().stream()). + createQuarkusProjectRequest.model.getCategories().stream().flatMap(category -> category.getExtensions().stream()). filter(extension -> extension.isSelected() || extension.isDefaultExtension()). forEach(extension -> extensions.add(extension.getId())); json.add(CODE_EXTENSIONS_PARAMETER_NAME, extensions); - json.addProperty(CODE_STREAM_PARAMETER_NAME, model.getKey()); + json.addProperty(CODE_STREAM_PARAMETER_NAME, createQuarkusProjectRequest.model.getKey()); return json.toString(); } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleBuilder.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleBuilder.java index 5a83e78c9..ee3d955ce 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleBuilder.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleBuilder.java @@ -101,16 +101,19 @@ public Module createModule(@NotNull ModifiableModuleModel moduleModel) throws In private void processDownload() throws IOException { File moduleFile = new File(getContentEntryPath()); - QuarkusModelRegistry.zip(wizardContext.getUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_TOOL_KEY).asParameter(), - wizardContext.getUserData(QuarkusConstants.WIZARD_GROUPID_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_ARTIFACTID_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_VERSION_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_CLASSNAME_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_PATH_KEY), - wizardContext.getUserData(QuarkusConstants.WIZARD_EXTENSIONS_MODEL_KEY), - moduleFile, - wizardContext.getUserData(QuarkusConstants.WIZARD_EXAMPLE_KEY)); + var createQuarkusProjectRequest = new QuarkusModelRegistry.CreateQuarkusProjectRequest(); + createQuarkusProjectRequest.endpoint = wizardContext.getUserData(QuarkusConstants.WIZARD_ENDPOINT_URL_KEY); + createQuarkusProjectRequest.tool = wizardContext.getUserData(QuarkusConstants.WIZARD_TOOL_KEY).asParameter(); + createQuarkusProjectRequest.groupId = wizardContext.getUserData(QuarkusConstants.WIZARD_GROUPID_KEY); + createQuarkusProjectRequest.artifactId = wizardContext.getUserData(QuarkusConstants.WIZARD_ARTIFACTID_KEY); + createQuarkusProjectRequest.version = wizardContext.getUserData(QuarkusConstants.WIZARD_VERSION_KEY); + createQuarkusProjectRequest.className = wizardContext.getUserData(QuarkusConstants.WIZARD_CLASSNAME_KEY); + createQuarkusProjectRequest.path = wizardContext.getUserData(QuarkusConstants.WIZARD_PATH_KEY); + createQuarkusProjectRequest.model = wizardContext.getUserData(QuarkusConstants.WIZARD_EXTENSIONS_MODEL_KEY); + createQuarkusProjectRequest.javaVersion = wizardContext.getUserData(QuarkusConstants.WIZARD_JAVA_VERSION_KEY); + createQuarkusProjectRequest.output = moduleFile; + createQuarkusProjectRequest.codeStarts = wizardContext.getUserData(QuarkusConstants.WIZARD_EXAMPLE_KEY); + QuarkusModelRegistry.zip(createQuarkusProjectRequest); updateWrapperPermissions(moduleFile); VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(moduleFile); RefreshQueue.getInstance().refresh(true, true, (Runnable) null, new VirtualFile[]{vf}); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleInfoStep.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleInfoStep.java index 5f2cafd4c..2788f1ef7 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleInfoStep.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModuleInfoStep.java @@ -18,13 +18,13 @@ import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.projectRoots.impl.SdkVersionUtil; import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.ComponentValidator; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.ValidationInfo; import com.intellij.openapi.util.Disposer; -import com.intellij.ui.CollectionComboBoxModel; -import com.intellij.ui.ColoredListCellRenderer; -import com.intellij.ui.ScrollPaneFactory; -import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.*; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLoadingPanel; import com.intellij.ui.components.JBTextField; @@ -35,19 +35,26 @@ import com.redhat.devtools.intellij.quarkus.QuarkusConstants; import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.model.java.JdkVersionDetector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; +import javax.swing.event.DocumentEvent; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import java.awt.*; -import java.util.Arrays; +import java.util.*; +import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import static com.intellij.ide.starters.shared.ValidationFunctions.*; +import static com.intellij.ui.SimpleTextAttributes.*; import static com.redhat.devtools.intellij.quarkus.projectWizard.QuarkusModelRegistry.DEFAULT_TIMEOUT_IN_SEC; +import static com.redhat.devtools.intellij.quarkus.projectWizard.QuarkusValidationFunctions.CHECK_CLASS_NAME; import static com.redhat.devtools.intellij.quarkus.projectWizard.RequestHelper.waitFor; /** @@ -62,6 +69,8 @@ public class QuarkusModuleInfoStep extends ModuleWizardStep implements Disposabl private ComboBox toolComboBox; + private ComboBox javaVersionsComboBox; + private JBCheckBox exampleField; private JBTextField groupIdField; @@ -87,6 +96,8 @@ public class QuarkusModuleInfoStep extends ModuleWizardStep implements Disposabl private EmptyProgressIndicator indicator; private boolean isInitialized = false; + private JdkVersionDetector.JdkVersionInfo jdkVersionInfo; + private final List componentsToValidate = new ArrayList<>(); public QuarkusModuleInfoStep(WizardContext context) { Disposer.register(context.getDisposable(), this); @@ -108,6 +119,9 @@ public void updateDataModel() { context.putUserData(QuarkusConstants.WIZARD_CLASSNAME_KEY, classNameField.getText()); context.putUserData(QuarkusConstants.WIZARD_PATH_KEY, pathField.getText()); context.putUserData(QuarkusConstants.WIZARD_EXTENSIONS_MODEL_KEY, extensionsModel); + String selectedJava = (String) javaVersionsComboBox.getModel().getSelectedItem(); + Integer javaVersion = selectedJava == null? null: Integer.valueOf(selectedJava); + context.putUserData(QuarkusConstants.WIZARD_JAVA_VERSION_KEY, javaVersion); } @Override public void dispose() { @@ -123,6 +137,7 @@ public void dispose() { @Override public void _init() { + jdkVersionInfo = SdkVersionUtil.getJdkVersionInfo(context.getProjectJdk().getHomePath()); if (isInitialized) { return; } @@ -149,19 +164,17 @@ public void intervalRemoved(ListDataEvent e) { @Override public void contentsChanged(ListDataEvent e) { + updateJavaVersions(); loadExtensionsModel(streamModel, indicator); } }); streamComboBox = new ComboBox<>(streamModel); - streamComboBox.setRenderer(new ColoredListCellRenderer() { + 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); - } + SimpleTextAttributes textAttributes = getComboItemStyle(stream.isRecommended(), false); + this.append(stream.getPlatformVersion(), textAttributes, true); if (stream.getStatus() != null) { this.append(" ").append(stream.getStatus()); } @@ -177,34 +190,156 @@ protected void customizeCellRenderer(@NotNull JList lis final CollectionComboBoxModel toolModel = new CollectionComboBoxModel<>(Arrays.asList(BuildToolDelegate.getDelegates())); toolComboBox = new ComboBox<>(toolModel); - toolComboBox.setRenderer(new ColoredListCellRenderer() { + toolComboBox.setRenderer(new ColoredListCellRenderer<>() { @Override protected void customizeCellRenderer(@NotNull JList list, BuildToolDelegate 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); + + javaVersionsComboBox = new ComboBox<>(); + javaVersionsComboBox.setRenderer(new ColoredListCellRenderer<>() { + @Override + protected void customizeCellRenderer(@NotNull JList list, String version, int index, boolean selected, boolean hasFocus) { + QuarkusStream stream = getSelectedQuarkusStream(); + if (stream != null) { + String recommendedVersion = stream.getJavaCompatibility().recommended(); + SimpleTextAttributes textAttribute = getComboItemStyle(Objects.equals(version, recommendedVersion), !isValidJava(version, jdkVersionInfo)); + this.append(version, textAttribute, true); + } + } + }); + formBuilder.addLabeledComponent("Java version:", javaVersionsComboBox); + addValidator(javaVersionsComboBox, () -> { + if (!isValidJava(javaVersionsComboBox.getModel().getSelectedItem().toString(), jdkVersionInfo)) { + return new ValidationInfo(QuarkusBundle.message("quarkus.wizard.error.incompatible.jdk", jdkVersionInfo.displayVersionString()), javaVersionsComboBox); + } + return null; + }); + + exampleField = new JBCheckBox("If selected, project will contain sample code from extensions that support codestarts.", true); formBuilder.addLabeledComponent("Example code:", exampleField); + groupIdField = new JBTextField("org.acme"); formBuilder.addLabeledComponent("Group:", groupIdField); + TextFieldValidator groupIdValidator = new TextFieldValidator(groupIdField, CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_GROUP_FORMAT, CHECK_NO_RESERVED_WORDS); + addValidator(groupIdField, groupIdValidator::validate); + artifactIdField = new JBTextField("code-with-quarkus"); formBuilder.addLabeledComponent("Artifact:", artifactIdField); + TextFieldValidator artifactIdValidator = new TextFieldValidator(artifactIdField, CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_ARTIFACT_SIMPLE_FORMAT, CHECK_NO_RESERVED_WORDS); + addValidator(artifactIdField, artifactIdValidator::validate); + versionField = new JBTextField("1.0.0-SNAPSHOT"); formBuilder.addLabeledComponent("Version:", versionField); + TextFieldValidator versionValidator = new TextFieldValidator(versionField, CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES); + addValidator(versionField, versionValidator::validate); + classNameField = new JBTextField("org.acme.ExampleResource"); formBuilder.addLabeledComponent("Class name:", classNameField); + TextFieldValidator classNameValidator = new TextFieldValidator(classNameField, CHECK_NO_WHITESPACES, CHECK_CLASS_NAME); + addValidator(classNameField, classNameValidator::validate); + pathField = new JBTextField("/hello"); formBuilder.addLabeledComponent("Path:", pathField); + TextFieldValidator pathValidator = new TextFieldValidator(pathField, CHECK_NO_WHITESPACES); + addValidator(pathField, pathValidator::validate); panel.add(ScrollPaneFactory.createScrollPane(formBuilder.getPanel(), true), "North"); hideSpinner(); extensionsModelRequest = loadExtensionsModel(streamModel, indicator); + updateJavaVersions(); isInitialized = true; } + void addValidator(JComponent component, Supplier validator) { + new ComponentValidator(context.getDisposable()) + .withValidator(validator) + .installOn(component); + componentsToValidate.add(component); + if (component instanceof JBTextField textField) { + textField.getDocument().addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(@NotNull DocumentEvent e) { + validate(textField); + } + }); + } else if (component instanceof ComboBox combo) { + combo.addItemListener(e -> { + validate(combo); + }); + } + + } + + private List validateComponents(boolean requestFocus) { + List validations = new ArrayList<>(componentsToValidate.size()); + componentsToValidate.forEach(c -> validate(c).ifPresent(validations::add)); + if (requestFocus && !validations.isEmpty()) { + var firstComponent = validations.get(0).component; + if (firstComponent != null) { + firstComponent.requestFocusInWindow(); + } + } + return validations; + } + + private Optional validate(@NotNull JComponent component) { + return ComponentValidator.getInstance(component).map(v -> { + v.revalidate(); + return v.getValidationInfo(); + }); + } + + /** + * Checks is the Java version is compatible with the selected SDK version + * + * @param version the Java version + * @param jdkVersionInfo the version information of the selected JDK + * @return true if the Java version is compatible with the selected SDK version, false otherwise. + */ + private boolean isValidJava(String version, JdkVersionDetector.JdkVersionInfo jdkVersionInfo) { + return jdkVersionInfo.version.isAtLeast(Integer.valueOf(version)); + } + + private QuarkusStream getSelectedQuarkusStream() { + return (QuarkusStream)streamComboBox.getModel().getSelectedItem(); + } + + SimpleTextAttributes getComboItemStyle(boolean recommended, boolean invalid) { + var style = recommended ? REGULAR_BOLD_ATTRIBUTES : REGULAR_ATTRIBUTES; + if (invalid) { + style = SimpleTextAttributes.merge(ERROR_ATTRIBUTES, style); + } + return style; + } + + private static final List defaultJavaVersions = List.of("17", "11"); + + private void updateJavaVersions() { + QuarkusStream stream = getSelectedQuarkusStream(); + QuarkusStream.JavaCompatibility javaCompatibility = stream.getJavaCompatibility(); + List javaVersions; + String recommended; + if (javaCompatibility != null) { + javaVersions = Arrays.stream(javaCompatibility.versions()).sorted(Comparator.reverseOrder()).toList(); + recommended = javaCompatibility.recommended(); + } else { + javaVersions = defaultJavaVersions; + recommended = defaultJavaVersions.get(0); + } + ComboBoxModel javaVersionsModel = new CollectionComboBoxModel<>(javaVersions); + javaVersionsComboBox.setModel(javaVersionsModel); + javaVersionsModel.setSelectedItem(recommended); + } + @Override public JComponent getPreferredFocusedComponent() { - return toolComboBox; + var errors = validateComponents(false); + if (errors.isEmpty()) { + return toolComboBox; + } + return errors.get(0).component; } private Future loadExtensionsModel(CollectionComboBoxModel streamModel, ProgressIndicator indicator) { @@ -219,9 +354,9 @@ private Future loadExtensionsModel(CollectionComboBoxMod return model.loadExtensionsModel(key, indicator); } catch (Exception e) { if (getComponent().isShowing()) { - ApplicationManager.getApplication().invokeLater(() -> { - Messages.showErrorDialog(QuarkusBundle.message("quarkus.wizard.error.extensions.loading.message", key, e.getMessage()), QuarkusBundle.message("quarkus.wizard.error.extensions.loading")); - }, modalityState); + ApplicationManager.getApplication().invokeLater(() -> + Messages.showErrorDialog(QuarkusBundle.message("quarkus.wizard.error.extensions.loading.message", key, e.getMessage()), QuarkusBundle.message("quarkus.wizard.error.extensions.loading")) + , modalityState); } } finally { if (getComponent().isShowing()) { @@ -238,14 +373,9 @@ private Future loadExtensionsModel(CollectionComboBoxMod @Override public boolean validate() throws ConfigurationException { - if (groupIdField.getText().isEmpty()) { - throw new ConfigurationException("Group must be specified"); - } - if (artifactIdField.getText().isEmpty()) { - throw new ConfigurationException("Artifact must be specified"); - } - if (versionField.getText().isEmpty()) { - throw new ConfigurationException("Version must be specified"); + var validations = validateComponents(true); + if (!validations.isEmpty()) { + return false; } try { boolean requestComplete = checkRequestComplete(); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusStream.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusStream.java index 0cb15de8a..999d93d59 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusStream.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusStream.java @@ -15,6 +15,9 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class QuarkusStream { + public static record JavaCompatibility(String[] versions, String recommended) { + } + @JsonProperty("key") private String key; @@ -27,6 +30,8 @@ public class QuarkusStream { @JsonProperty("status") private String status; + @JsonProperty("javaCompatibility") + private JavaCompatibility javaCompatibility; public String getKey() { return key; @@ -69,4 +74,12 @@ public String getPlatformVersion() { String key = getKey(); return key.substring(key.indexOf(':') + 1); } + + public JavaCompatibility getJavaCompatibility() { + return javaCompatibility; + } + + public void setJavaCompatibility(JavaCompatibility javaCompatibility) { + this.javaCompatibility = javaCompatibility; + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusValidationFunctions.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusValidationFunctions.java new file mode 100644 index 000000000..dff1c3d39 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusValidationFunctions.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.projectWizard; + +import com.intellij.codeInsight.daemon.JavaErrorBundle; +import com.intellij.codeInsight.daemon.impl.analysis.HighlightClassUtil; +import com.intellij.ide.starters.shared.TextValidationFunction; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.impl.PsiNameHelperImpl; + +/** + * Holds {@link TextValidationFunction}s implementations used to validate elements of a Quarkus project. + * + * @see com.intellij.ide.starters.shared.ValidationFunctions + */ +public class QuarkusValidationFunctions { + + private QuarkusValidationFunctions(){} + + // Borrowed from https://github.com/JetBrains/intellij-community/blob/bfb54733f1d10f2ba9164e5a82d3fc00150bf159/java/java-impl/src/com/intellij/ide/actions/CreateClassAction.java#L63-L70 + /** + * Validates a Resource name when creating a new Quarkus project. + */ + public static final TextValidationFunction CHECK_CLASS_NAME = inputString -> { + //Technically we should target a different language level, to determine restricted names + if (!inputString.isEmpty() && !PsiNameHelperImpl.getInstance().isQualifiedName(inputString)) { + return JavaErrorBundle.message("create.class.action.this.not.valid.java.qualified.name"); + } + String shortName = StringUtil.getShortName(inputString); + if (HighlightClassUtil.isRestrictedIdentifier(shortName, LanguageLevel.HIGHEST)) { + return JavaErrorBundle.message("restricted.identifier", shortName); + } + return null; + }; +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/TextFieldValidator.java b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/TextFieldValidator.java new file mode 100644 index 000000000..b80fe6bf8 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/projectWizard/TextFieldValidator.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.projectWizard; + +import com.intellij.ide.starters.shared.TextValidationFunction; +import com.intellij.openapi.ui.ValidationInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Arrays; +import java.util.Objects; + +/** + * Validate a {@link JTextField} against several {@link TextValidationFunction}s + */ +public class TextFieldValidator { + + private final JTextField textField; + private final TextValidationFunction[] validations; + + public TextFieldValidator(@NotNull JTextField textField, @NotNull TextValidationFunction ... validations){ + this.textField = textField; + this.validations = validations; + } + + /** + * Validates the {@link JTextField} bound to this validator. + * @return a {@link ValidationInfo} instance for the first error message, if validation failed, null otherwise. + */ + public @Nullable ValidationInfo validate() { + return Arrays.stream(validations).map(v -> v.checkText(textField.getText())) + .filter(Objects::nonNull).findFirst().map(t -> new ValidationInfo(t, textField)).orElse(null); + } +} diff --git a/src/main/resources/messages/QuarkusBundle.properties b/src/main/resources/messages/QuarkusBundle.properties index e6b477648..46a638b49 100644 --- a/src/main/resources/messages/QuarkusBundle.properties +++ b/src/main/resources/messages/QuarkusBundle.properties @@ -28,3 +28,4 @@ quarkus.wizard.loading.streams=Loading Quarkus platform streams... quarkus.wizard.error.extensions.loading=Failed to load Quarkus extensions quarkus.wizard.error.extensions.loading.message=Failed to load extensions for Quarkus {0}:\n{1} quarkus.wizard.loading.extensions=Loading Quarkus extensions... +quarkus.wizard.error.incompatible.jdk=The java version is not compatible with the selected SDK ({0}) diff --git a/src/test/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistryTest.java b/src/test/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistryTest.java index b2ef2dcb7..eac0acc58 100644 --- a/src/test/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistryTest.java +++ b/src/test/java/com/redhat/devtools/intellij/quarkus/projectWizard/QuarkusModelRegistryTest.java @@ -11,6 +11,7 @@ package com.redhat.devtools.intellij.quarkus.projectWizard; import com.intellij.openapi.progress.EmptyProgressIndicator; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.testFramework.fixtures.CodeInsightTestFixture; import com.intellij.testFramework.fixtures.IdeaProjectTestFixture; import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; @@ -18,13 +19,13 @@ import com.redhat.devtools.intellij.lsp4mp4ij.classpath.ClasspathResourceChangedManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; import com.redhat.devtools.intellij.quarkus.QuarkusProjectService; +import org.jetbrains.annotations.NotNull; import org.junit.*; import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import static com.redhat.devtools.intellij.quarkus.QuarkusConstants.QUARKUS_CODE_URL; @@ -90,7 +91,7 @@ public void checkThatIOExceptionIsReturnedWithInvalidURL() throws IOException { private void enableExtension(QuarkusExtensionsModel model, String name) { Stream extStream = model.getCategories().stream().flatMap(cat -> cat.getExtensions().stream()).filter(extension -> extension.getName().equals(name)); - List extensions = extStream.collect(Collectors.toList()); + List extensions = extStream.toList(); if (extensions.isEmpty()) { fail("Could not find any "+name+" extension in "+model.getKey()); } @@ -98,18 +99,23 @@ private void enableExtension(QuarkusExtensionsModel model, String name) { } private QuarkusExtensionsModel getRecommendedModel(QuarkusModel model) throws IOException { - String key = model.getStreams().stream().filter(s -> s.isRecommended()).findFirst().orElse(model.getStreams().get(0)).getKey(); + String key = getFirstRecommendedStream(model).getKey(); return model.getExtensionsModel(key, new EmptyProgressIndicator()); } + private QuarkusStream getFirstRecommendedStream(QuarkusModel model) throws IOException { + assertFalse(model.getStreams().isEmpty()); + return model.getStreams().stream().filter(QuarkusStream::isRecommended).findFirst().orElse(model.getStreams().get(0)); + } + private File checkBaseMavenProject(boolean examples) throws IOException { File folder = temporaryFolder.newFolder(); QuarkusModel model = registry.load(QUARKUS_CODE_URL, new EmptyProgressIndicator()); QuarkusExtensionsModel extensionsModel = getRecommendedModel(model); enableExtension(extensionsModel, JAXRS_EXTENSION); - QuarkusModelRegistry.zip(QUARKUS_CODE_URL, "MAVEN", "org.acme", "code-with-quarkus", - "0.0.1-SNAPSHOT", "org.acme.ExampleResource", "/example", - extensionsModel, folder, examples); + var request = createMavenRequest(extensionsModel, folder); + request.codeStarts = examples; + QuarkusModelRegistry.zip(request); assertTrue(new File(folder, "pom.xml").exists()); return folder; } @@ -137,9 +143,8 @@ public void checkAllExtensionsMavenProject() throws IOException { QuarkusModel model = registry.load(QUARKUS_CODE_URL, new EmptyProgressIndicator()); QuarkusExtensionsModel extensionsModel = getRecommendedModel(model); enableAllExtensions(extensionsModel); - QuarkusModelRegistry.zip(QUARKUS_CODE_URL, "MAVEN", "org.acme", "code-with-quarkus", - "0.0.1-SNAPSHOT", "org.acme.ExampleResource", "/example", extensionsModel, - folder, false); + var request = createMavenRequest(extensionsModel, folder); + QuarkusModelRegistry.zip(request); assertTrue(new File(folder, "pom.xml").exists()); } @@ -148,9 +153,9 @@ private File checkBaseGradleProject(boolean examples) throws IOException { QuarkusModel model = registry.load(QUARKUS_CODE_URL, new EmptyProgressIndicator()); QuarkusExtensionsModel extensionsModel = getRecommendedModel(model); enableExtension(extensionsModel, JAXRS_EXTENSION); - QuarkusModelRegistry.zip(QUARKUS_CODE_URL, "GRADLE", "org.acme", "code-with-quarkus", - "0.0.1-SNAPSHOT", "org.acme.ExampleResource", "/example", - extensionsModel, folder, examples); + var request = createGradleRequest(extensionsModel, folder); + request.codeStarts = examples; + QuarkusModelRegistry.zip(request); assertTrue(new File(folder, "build.gradle").exists()); return folder; } @@ -173,9 +178,69 @@ public void checkAllExtensionsGradleProject() throws IOException { QuarkusModel model = registry.load(QUARKUS_CODE_URL, new EmptyProgressIndicator()); QuarkusExtensionsModel extensionsModel = getRecommendedModel(model); enableAllExtensions(extensionsModel); - QuarkusModelRegistry.zip(QUARKUS_CODE_URL, "GRADLE", "org.acme", "code-with-quarkus", - "0.0.1-SNAPSHOT", "org.acme.ExampleResource", "/example", extensionsModel, - folder, false); + var request = createGradleRequest(extensionsModel, folder); + QuarkusModelRegistry.zip(request); assertTrue(new File(folder, "build.gradle").exists()); } + + @Test + public void checkJavaCompatibility() throws IOException { + File folder = temporaryFolder.newFolder(); + QuarkusModel model = registry.load(QUARKUS_CODE_URL, new EmptyProgressIndicator()); + QuarkusExtensionsModel extensionsModel = getRecommendedModel(model); + QuarkusStream stream = getFirstRecommendedStream(model); + + var javaCompat = stream.getJavaCompatibility(); + assertNotNull("stream.javaCompatibility is null", javaCompat); + String recommendedJavaVersion = javaCompat.recommended(); + assertNotNull("Recommended Java versions is null", recommendedJavaVersion); + String[] _versions = javaCompat.versions(); + assertNotNull("Supported Java versions are null", _versions); + List versions = List.of(_versions); + assertTrue("The recommended version should be listed in all Java versions", versions.contains(recommendedJavaVersion)); + + // enableAllExtensions(extensionsModel); //fails with error 400 : + // Caused by: java.io.IOException: Server returned HTTP response code: 400 for URL: https://stage.code.quarkus.io/api/download + // but no detailed message bubbles up. + // Turns out some extensions (Camel) require at least Java 17, so java 11 is impossible to use. + // https://code.quarkus.io/d?j=11&e=org.apache.camel.quarkus%3Acamel-quarkus-core&cn=code.quarkus.io gives you + // Quarkus Command error > Some extensions are not compatible with the selected Java version (11): + // - org.apache.camel.quarkus:camel-quarkus-core (min: 17) + // Also see https://github.com/quarkusio/code.quarkus.io/issues/583#issuecomment-1895700582 + + enableExtension(extensionsModel, JAXRS_EXTENSION); + + var request = createMavenRequest(extensionsModel, folder); + String otherJava = versions.stream().filter(v -> !recommendedJavaVersion.equals(v)).findFirst().get(); + request.javaVersion = Integer.valueOf(otherJava); + QuarkusModelRegistry.zip(request); + File pomXml = new File(folder, "pom.xml"); + assertTrue(pomXml.exists()); + String pom = FileUtil.loadFile(pomXml); + String expectedReleaseVersion = ""+otherJava+""; + assertTrue(expectedReleaseVersion + " is missing from pom.xml:\n"+pom, pom.contains(expectedReleaseVersion)); + } + + + @NotNull QuarkusModelRegistry.CreateQuarkusProjectRequest createMavenRequest(QuarkusExtensionsModel extensionsModel, File outputFolder) { + return createRequest("MAVEN", extensionsModel, outputFolder); + } + + @NotNull QuarkusModelRegistry.CreateQuarkusProjectRequest createGradleRequest(QuarkusExtensionsModel extensionsModel, File outputFolder) { + return createRequest("GRADLE", extensionsModel, outputFolder); + } + + private @NotNull QuarkusModelRegistry.CreateQuarkusProjectRequest createRequest(@NotNull String tool, @NotNull QuarkusExtensionsModel extensionsModel, @NotNull File outputFolder) { + var request = new QuarkusModelRegistry.CreateQuarkusProjectRequest(); + request.endpoint = QUARKUS_CODE_URL; + request.tool = tool; + request.groupId = "org.acme"; + request.artifactId = "code-with-quarkus"; + request.version="0.0.1-SNAPSHOT"; + request.className = "org.acme.ExampleResource"; + request.path = "/example"; + request.output = outputFolder; + request.model = extensionsModel; + return request; + } }