diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java index cdccd488ed6..d6b816ca75e 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java @@ -24,6 +24,8 @@ */ package jdk.jpackage.internal; +import static jdk.jpackage.internal.cli.StandardValidator.IS_VALID_MAC_BUNDLE_IDENTIFIER; + import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -37,6 +39,7 @@ import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.AppImageSigningConfig; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLaunchers; +import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.ExternalApplication; import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Launcher; @@ -138,20 +141,6 @@ final class MacApplicationBuilder { return MacApplication.create(ApplicationBuilder.overrideAppImageLayout(app, appImageLayout), mixin); } - static boolean isValidBundleIdentifier(String id) { - for (int i = 0; i < id.length(); i++) { - char a = id.charAt(i); - // We check for ASCII codes first which we accept. If check fails, - // check if it is acceptable extended ASCII or unicode character. - if ((a >= 'A' && a <= 'Z') || (a >= 'a' && a <= 'z') - || (a >= '0' && a <= '9') || (a == '-' || a == '.')) { - continue; - } - return false; - } - return true; - } - private static void validateAppVersion(Application app) { try { CFBundleVersion.of(app.version()); @@ -246,8 +235,8 @@ final class MacApplicationBuilder { } private String validatedBundleIdentifier(Application app) { - final var value = Optional.ofNullable(bundleIdentifier).orElseGet(() -> { - return app.mainLauncher() + return Optional.ofNullable(bundleIdentifier).orElseGet(() -> { + var derivedValue = app.mainLauncher() .flatMap(Launcher::startupInfo) .map(li -> { final var packageName = li.packageName(); @@ -258,15 +247,23 @@ final class MacApplicationBuilder { } }) .orElseGet(app::name); + + if (!IS_VALID_MAC_BUNDLE_IDENTIFIER.test(derivedValue)) { + // Derived bundle identifier is invalid. Try to adjust it by dropping all invalid characters. + derivedValue = derivedValue.codePoints() + .mapToObj(Character::toString) + .filter(IS_VALID_MAC_BUNDLE_IDENTIFIER) + .collect(Collectors.joining("")); + if (!IS_VALID_MAC_BUNDLE_IDENTIFIER.test(derivedValue)) { + throw new ConfigException( + I18N.format("error.invalid-derived-bundle-identifier"), + I18N.format("error.invalid-derived-bundle-identifier.advice")); + } + } + + Log.verbose(I18N.format("message.derived-bundle-identifier", derivedValue)); + return derivedValue; }); - - if (!isValidBundleIdentifier(value)) { - throw I18N.buildConfigException("message.invalid-identifier", value) - .advice("message.invalid-identifier.advice") - .create(); - } - - return value; } private String validatedCategory() { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties index 95fff00152b..2ed1f740805 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties @@ -33,6 +33,8 @@ error.invalid-app-image-runtime-image-bin-dir=Runtime directory {0} in the prede error.invalid-runtime-image-bin-dir=Runtime image "{0}" should not contain "bin" folder error.invalid-runtime-image-bin-dir.advice=Use --strip-native-commands jlink option when generating runtime image used with {0} option error.invalid-app-image-plist-file=Invalid "{0}" file in the predefined application image +error.invalid-derived-bundle-identifier=Can't derive a valid bundle identifier from the input data +error.invalid-derived-bundle-identifier.advice=Specify bundle identifier with --mac-package-identifier option resource.app-info-plist=Application Info.plist resource.app-runtime-info-plist=Embedded Java Runtime Info.plist @@ -57,8 +59,7 @@ message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not message.version-string-too-many-components='app-version' may have between 1 and 3 numbers: 1, 1.2, 1.2.3. message.version-string-first-number-not-zero=The first number in an app-version cannot be zero or negative. message.keychain.error=Unable to get keychain list. -message.invalid-identifier=Invalid mac bundle identifier [{0}]. -message.invalid-identifier.advice=specify identifier with "--mac-package-identifier". +message.derived-bundle-identifier=Derived bundle identifier: {0} message.preparing-dmg-setup=Preparing dmg setup: {0}. message.preparing-scripts=Preparing package scripts. message.preparing-distribution-dist=Preparing distribution.dist: {0}. diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java index 438a5ac3e6f..f193cff0dc0 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java @@ -58,9 +58,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.cli.OptionValueExceptionFactory.StandardArgumentsMapper; import jdk.jpackage.internal.model.AppImageBundleType; import jdk.jpackage.internal.model.BundleType; import jdk.jpackage.internal.model.BundlingOperationDescriptor; +import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; @@ -352,6 +354,11 @@ public final class StandardOption { public static final OptionValue MAC_BUNDLE_IDENTIFIER = stringOption("mac-package-identifier") .valuePattern("package identifier") + .validator(StandardValidator.IS_VALID_MAC_BUNDLE_IDENTIFIER) + .validatorExceptionFactory(OptionValueExceptionFactory.build((message, cause) -> { + return new ConfigException(message, I18N.format("error.parameter-not-mac-bundle-identifier.advice"), cause); + }).formatArgumentsTransformer(StandardArgumentsMapper.VALUE_AND_NAME).create()) + .validatorExceptionFormatString("error.parameter-not-mac-bundle-identifier") .create(); public static final OptionValue MAC_BUNDLE_SIGNING_PREFIX = stringOption("mac-package-signing-prefix").scope(MAC_SIGNING).create(); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java index cfa97439592..ee9f39ee9cd 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java @@ -36,11 +36,12 @@ import java.nio.file.Path; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.regex.Pattern; import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException; import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.MacBundle; -final public class StandardValidator { +public final class StandardValidator { private StandardValidator() { } @@ -143,6 +144,9 @@ final public class StandardValidator { return MacBundle.fromPath(path).isPresent(); }; + // https://developer.apple.com/documentation/BundleResources/Information-Property-List/CFBundleIdentifier + public static final Predicate IS_VALID_MAC_BUNDLE_IDENTIFIER = Pattern.compile("[\\p{Alnum}-\\.]+").asMatchPredicate(); + public static final class DirectoryListingIOException extends RuntimeException { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index 02784a52acc..9dfd13d60bb 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -88,6 +88,8 @@ error.parameter-not-empty-directory=The value "{0}" provided for parameter {1} i error.parameter-not-url=The value "{0}" provided for parameter {1} is not a valid URL error.parameter-not-launcher-shortcut-dir=The value "{0}" provided for parameter {1} is not a valid shortcut startup directory error.parameter-not-mac-bundle=The value "{0}" provided for parameter {1} is not a valid macOS bundle +error.parameter-not-mac-bundle-identifier=The value "{0}" provided for parameter {1} is not a valid macOS bundle identifier. +error.parameter-not-mac-bundle-identifier.advice=Bundle identifier must be a non-empty string containing only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.) error.path-parameter-ioexception=I/O error accessing path value "{0}" of parameter {1} error.parameter-add-launcher-malformed=The value "{0}" provided for parameter {1} does not match the pattern = error.parameter-add-launcher-not-file=The value of path to a property file "{0}" provided for additional launcher "{1}" is not a valid file path diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java index d5248ee2b17..cc31c416d44 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,6 +27,7 @@ import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.UnaryOperator; +import java.util.regex.Pattern; import java.util.stream.Stream; public record CannedFormattedString(BiFunction formatter, String key, List args) implements CannedArgument { @@ -67,6 +68,23 @@ public record CannedFormattedString(BiFunction formatt } } + public interface Spec { + + String format(); + List modelArgs(); + + default CannedFormattedString asCannedFormattedString(Object ... args) { + if (args.length != modelArgs().size()) { + throw new IllegalArgumentException(); + } + return JPackageStringBundle.MAIN.cannedFormattedString(format(), args); + } + + default Pattern asPattern() { + return JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(format(), modelArgs().toArray()); + } + } + private record AddPrefixFormatter(BiFunction formatter) implements BiFunction { AddPrefixFormatter { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 4fad120d0f6..b173d345596 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1078,6 +1078,53 @@ public class JPackageCommand extends CommandArguments { return this; } + public JPackageCommand validateOutput( + Class messageGroup, + Consumer validatorMutator, + List expectedMessages) { + + Objects.requireNonNull(validatorMutator); + + if (!messageGroup.isEnum()) { + throw new IllegalArgumentException(); + } + + var messageSpecs = messageGroup.getEnumConstants(); + + var expectMessageFormats = expectedMessages.stream().map(CannedFormattedString::key).toList(); + + var groupMessageFormats = Stream.of(messageSpecs) + .map(CannedFormattedString.Spec::format) + .collect(Collectors.toMap(x -> x, x -> x)) + .keySet(); + + if (!groupMessageFormats.containsAll(expectMessageFormats)) { + // Expected format strings should be a subset of the group format strings. + throw new IllegalArgumentException(); + } + + if (!expectedMessages.isEmpty()) { + new JPackageOutputValidator().expectMatchingStrings(expectedMessages).mutate(validatorMutator).applyTo(this); + } + + Stream.of(messageSpecs).filter(spec -> { + return !expectMessageFormats.contains(spec.format()); + }).map(CannedFormattedString.Spec::asPattern).map(pattern -> { + return TKit.assertTextStream(pattern).negate(); + }).forEach(validator -> { + new JPackageOutputValidator().add(validator).stdoutAndStderr().applyTo(this); + }); + + return this; + } + + public JPackageCommand validateOutput( + Class messageGroup, + Consumer validatorMutator, + CannedFormattedString... expected) { + return validateOutput(messageGroup, validatorMutator, List.of(expected)); + } + public boolean isWithToolProvider() { return toolProviderSource.toolProvider().isPresent(); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageOutputValidator.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageOutputValidator.java index ed14f40f65c..8817ca0b730 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageOutputValidator.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageOutputValidator.java @@ -140,6 +140,11 @@ public final class JPackageOutputValidator { return this; } + public JPackageOutputValidator mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + public void applyTo(JPackageCommand cmd) { toResultConsumer(cmd).ifPresent(cmd::validateResult); } diff --git a/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacApplicationBuilderTest.java b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacApplicationBuilderTest.java new file mode 100644 index 00000000000..559e8d53827 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacApplicationBuilderTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.model.ApplicationLaunchers; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LauncherStartupInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class MacApplicationBuilderTest { + + @ParameterizedTest + @CsvSource({ + "NAME,!fo#o,foo", + "NAME,bar,bar", + "MAIN_LAUNCHER_CLASSNAME,foo.b$ar.Hello,foo.bar", + "MAIN_LAUNCHER_CLASSNAME,Hello$2,Hello2", + "NAME,!#,", + }) + void testDerivedBundleIdentifier(ApplicationBuilderProperty type, String value, String expectedBundleIdentifier) { + + var builder = buildApplication(); + var macBuilder = new MacApplicationBuilder(builder); + + type.applyTo(value, builder, macBuilder); + + if (expectedBundleIdentifier != null) { + assertEquals(expectedBundleIdentifier, macBuilder.create().bundleIdentifier()); + } else { + var ex = assertThrowsExactly(ConfigException.class, macBuilder::create); + assertEquals(I18N.format("error.invalid-derived-bundle-identifier"), ex.getMessage()); + assertEquals(I18N.format("error.invalid-derived-bundle-identifier.advice"), ex.getAdvice()); + } + } + + private static ApplicationBuilder buildApplication() { + return new ApplicationBuilder().appImageLayout(APPLICATION_LAYOUT); + } + + enum ApplicationBuilderProperty { + NAME { + void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder) { + builder.name(Objects.requireNonNull(value)); + } + }, + MAIN_LAUNCHER_CLASSNAME { + void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder) { + var startupInfo = new LauncherStartupInfo.Stub(Objects.requireNonNull(value), List.of(), List.of(), List.of()); + var launcher = new Launcher.Stub( + startupInfo.simpleClassName(), + Optional.of(startupInfo), + List.of(), + false, + "", + Optional.empty(), + null, + Map.of()); + builder.launchers(new ApplicationLaunchers(launcher)); + } + } + ; + + abstract void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder); + } +} diff --git a/test/jdk/tools/jpackage/junit/macosx/junit.java b/test/jdk/tools/jpackage/junit/macosx/junit.java index 4ab05daf1ae..55d309a5d7c 100644 --- a/test/jdk/tools/jpackage/junit/macosx/junit.java +++ b/test/jdk/tools/jpackage/junit/macosx/junit.java @@ -63,3 +63,11 @@ * jdk/jpackage/internal/ActiveKeychainListTest.java * @run junit jdk.jpackage/jdk.jpackage.internal.ActiveKeychainListTest */ + +/* @test + * @summary Test MacApplicationBuilderTest + * @requires (os.family == "mac") + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/MacApplicationBuilderTest.java + * @run junit jdk.jpackage/jdk.jpackage.internal.MacApplicationBuilderTest + */ diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes index 19478aaa4a7..a7a65402b9c 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes @@ -18,7 +18,6 @@ ErrorTest.test(IMAGE; args-add=[--module, com.foo.bar, --runtime-image, @@JAVA_H ErrorTest.test(IMAGE; args-add=[--module, java.base, --runtime-image, @@JAVA_HOME@@]; errors=[message.error-header+[ERR_NoMainClass]]) ErrorTest.test(LINUX_DEB; app-desc=Hello; args-add=[--linux-package-name, #]; errors=[message.error-header+[error.deb-invalid-value-for-package-name, #], message.advice-header+[error.deb-invalid-value-for-package-name.advice]]) ErrorTest.test(LINUX_RPM; app-desc=Hello; args-add=[--linux-package-name, #]; errors=[message.error-header+[error.rpm-invalid-value-for-package-name, #], message.advice-header+[error.rpm-invalid-value-for-package-name.advice]]) -ErrorTest.test(MAC_PKG; app-desc=Hello; args-add=[--mac-package-identifier, #1]; errors=[message.error-header+[message.invalid-identifier, #1], message.advice-header+[message.invalid-identifier.advice]]) ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--mac-app-store, --runtime-image, @@JAVA_HOME@@]; errors=[message.error-header+[error.invalid-runtime-image-bin-dir, @@JAVA_HOME@@], message.advice-header+[error.invalid-runtime-image-bin-dir.advice, --mac-app-store]]) ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--runtime-image, @@EMPTY_DIR@@]; errors=[message.error-header+[error.invalid-runtime-image-missing-file, @@EMPTY_DIR@@, lib/**/libjli.dylib]]) ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--runtime-image, @@INVALID_MAC_RUNTIME_BUNDLE@@]; errors=[message.error-header+[error.invalid-runtime-image-missing-file, @@INVALID_MAC_RUNTIME_BUNDLE@@, Contents/Home/lib/**/libjli.dylib]]) diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java index 58a78edb627..4d47bced8f1 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java @@ -58,6 +58,7 @@ import jdk.jpackage.internal.cli.StandardOption.LauncherProperty; import jdk.jpackage.internal.model.AppImageBundleType; import jdk.jpackage.internal.model.BundleType; import jdk.jpackage.internal.model.JPackageException; +import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; import jdk.jpackage.internal.util.RootedPath; @@ -71,6 +72,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; @@ -258,6 +260,40 @@ public class StandardOptionTest extends JUnitAdapter.TestSrcInitializer { }).toList(), result.errors()); } + @ParameterizedTest + @ValueSource(strings = { + ".", + "a-b.c", + "A", + "com.acme.Foo" + }) + void test_MAC_BUNDLE_IDENTIFIER_valid(String id) { + + var spec = StandardOption.MAC_BUNDLE_IDENTIFIER.getSpec(); + + var result = spec.convert(spec.name(), StringToken.of(id)).orElseThrow(); + + assertEquals(result, id); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + ",", + "Hello!" + }) + void test_MAC_BUNDLE_IDENTIFIER_invalid(String id) { + + var spec = StandardOption.MAC_BUNDLE_IDENTIFIER.getSpec(); + + var result = spec.convert(spec.name(), StringToken.of(id)); + + var ex = assertThrows(ConfigException.class, result::orElseThrow); + + assertEquals(I18N.format("error.parameter-not-mac-bundle-identifier", id, spec.name().formatForCommandLine()), ex.getMessage()); + assertEquals(I18N.format("error.parameter-not-mac-bundle-identifier.advice"), ex.getAdvice()); + } + @ParameterizedTest @EnumSource(OptionMutatorTest.TestType.class) public void test_pathOptionMutator(OptionMutatorTest.TestType type) { diff --git a/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java index 67a5cf6b609..f8638735a6a 100644 --- a/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java +++ b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,11 +21,20 @@ * questions. */ -import jdk.jpackage.test.TKit; -import jdk.jpackage.test.MacHelper; -import jdk.jpackage.test.JPackageCommand; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.CannedFormattedString; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageOutputValidator; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.TKit; /** @@ -43,30 +52,198 @@ import jdk.jpackage.test.Annotations.Parameter; * --jpt-run=MacPropertiesTest */ public class MacPropertiesTest { - @Test - @Parameter("MacPackageNameTest") - public void testPackageName(String packageName) { - testParameterInAppImage("--mac-package-name", "CFBundleName", - packageName); - } @Test - @Parameter("Foo") - public void testPackageIdetifier(String packageId) { - testParameterInAppImage("--mac-package-identifier", "CFBundleIdentifier", - packageId); + @ParameterSupplier + public void test(TestSpec spec) { + spec.run(); } - private static void testParameterInAppImage(String jpackageParameterName, - String plistKeyName, String value) { - JPackageCommand cmd = JPackageCommand.helloAppImage() - .addArguments(jpackageParameterName, value); + public static Collection test() { - cmd.executeAndAssertHelloAppImageCreated(); + var testCases = new ArrayList(); - var plist = MacHelper.readPListFromAppImage(cmd.outputBundle()); + testCases.addAll(List.of( + TestSpec.build("CFBundleName").addArgs("--mac-package-name", "MacPackageNameTest").expect("MacPackageNameTest"), + TestSpec.build("CFBundleIdentifier").addArgs("--mac-package-identifier", "Foo").expect("Foo"), + // Should derive from the input data. + TestSpec.build("CFBundleIdentifier").appDesc("com.acme.Hello").expect("com.acme").expect(BundleIdentifierMessage.VALUE.asCannedFormattedString("com.acme")) + )); - TKit.assertEquals(value, plist.queryValue(plistKeyName), String.format( - "Check value of %s plist key", plistKeyName)); + return testCases.stream().map(TestSpec.Builder::create).map(v -> { + return new Object[] {v}; + }).toList(); + } + + enum BundleIdentifierMessage implements CannedFormattedString.Spec { + VALUE("message.derived-bundle-identifier", "bundle-id"), + ; + + BundleIdentifierMessage(String key, Object ... args) { + this.key = Objects.requireNonNull(key); + this.args = List.of(args); + } + + @Override + public String format() { + return key; + } + + @Override + public List modelArgs() { + return args; + } + + private final String key; + private final List args; + } + + record TestSpec( + Optional appDesc, + List addArgs, + List delArgs, + List expectedTraceMessages, + String expectedInfoPlistKeyValue, + String infoPlistKey, + Optional> traceMessagesClass) { + + TestSpec { + Objects.requireNonNull(addArgs); + Objects.requireNonNull(delArgs); + Objects.requireNonNull(expectedTraceMessages); + Objects.requireNonNull(expectedInfoPlistKeyValue); + Objects.requireNonNull(infoPlistKey); + Objects.requireNonNull(traceMessagesClass); + } + + void run() { + var cmd = appDesc.map(JPackageCommand::helloAppImage).orElseGet(JPackageCommand::helloAppImage) + .setFakeRuntime().addArguments(addArgs); + + delArgs.forEach(cmd::removeArgumentWithValue); + + Consumer validatorMutator = validator -> { + validator.matchTimestamps().stripTimestamps(); + }; + + traceMessagesClass.ifPresentOrElse(v -> { + cmd.validateOutput(v, validatorMutator, expectedTraceMessages); + }, () -> { + new JPackageOutputValidator() + .mutate(validatorMutator) + .expectMatchingStrings(expectedTraceMessages) + .applyTo(cmd); + }); + + cmd.executeAndAssertHelloAppImageCreated(); + + var plist = MacHelper.readPListFromAppImage(cmd.outputBundle()); + + TKit.assertEquals( + expectedInfoPlistKeyValue, + plist.queryValue(infoPlistKey), + String.format("Check value of %s plist key", infoPlistKey)); + } + + @Override + public String toString() { + var tokens = new ArrayList(); + + tokens.add(String.format("%s=>%s", infoPlistKey, expectedInfoPlistKeyValue)); + + appDesc.ifPresent(v -> { + tokens.add("app-desc=" + v); + }); + + if (!addArgs.isEmpty()) { + tokens.add("args-add=" + addArgs); + } + + if (!delArgs.isEmpty()) { + tokens.add("args-del=" + delArgs); + } + + if (!expectedTraceMessages.isEmpty()) { + tokens.add("expect=" + expectedTraceMessages); + } + + return tokens.stream().collect(Collectors.joining("; ")); + } + + static Builder build() { + return new Builder(); + } + + static Builder build(String infoPlistKey) { + return build().infoPlistKey(Objects.requireNonNull(infoPlistKey)); + } + + static final class Builder { + + TestSpec create() { + + Class traceMessagesClass = switch (Objects.requireNonNull(infoPlistKey)) { + case "CFBundleIdentifier" -> BundleIdentifierMessage.class; + case "CFBundleName" -> null; + default -> { + throw new IllegalStateException(); + } + }; + + return new TestSpec( + Optional.ofNullable(appDesc), + addArgs, + delArgs, + expectedTraceMessages, + expectedInfoPlistKeyValue, + infoPlistKey, + Optional.ofNullable(traceMessagesClass)); + } + + Builder appDesc(String v) { + appDesc = v; + return this; + } + + Builder addArgs(List v) { + addArgs.addAll(v); + return this; + } + + Builder addArgs(String... args) { + return addArgs(List.of(args)); + } + + Builder delArgs(List v) { + delArgs.addAll(v); + return this; + } + + Builder delArgs(String... args) { + return delArgs(List.of(args)); + } + + Builder expect(CannedFormattedString traceMessage) { + expectedTraceMessages.add(traceMessage); + return this; + } + + Builder expect(String v) { + expectedInfoPlistKeyValue = v; + return this; + } + + Builder infoPlistKey(String v) { + infoPlistKey = v; + return this; + } + + private String appDesc; + private final List addArgs = new ArrayList<>(); + private final List delArgs = new ArrayList<>(); + private final List expectedTraceMessages = new ArrayList<>(); + private String expectedInfoPlistKeyValue; + private String infoPlistKey; + } } } diff --git a/test/jdk/tools/jpackage/share/AppVersionTest.java b/test/jdk/tools/jpackage/share/AppVersionTest.java index 7ba1870def1..598ee1551b3 100644 --- a/test/jdk/tools/jpackage/share/AppVersionTest.java +++ b/test/jdk/tools/jpackage/share/AppVersionTest.java @@ -46,7 +46,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.util.MacBundle; import jdk.jpackage.internal.util.RuntimeReleaseFile; @@ -58,8 +57,6 @@ import jdk.jpackage.test.CannedFormattedString; import jdk.jpackage.test.ConfigurationTarget; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.JPackageCommand.StandardAssert; -import jdk.jpackage.test.JPackageOutputValidator; -import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.JavaAppDesc; import jdk.jpackage.test.MacHelper; import jdk.jpackage.test.PackageTest; @@ -270,7 +267,7 @@ public final class AppVersionTest { }).toList(); } - enum Message { + enum Message implements CannedFormattedString.Spec { VERSION_FROM_MODULE("message.module-version", "version", "module"), VERSION_FROM_RELEASE_FILE("message.release-version", "version"), VERSION_NORMALIZED("message.version-normalized", "version", "version"), @@ -278,24 +275,21 @@ public final class AppVersionTest { Message(String key, Object ... args) { this.key = Objects.requireNonNull(key); - this.args = args; + this.args = List.of(args); } - CannedFormattedString cannedFormattedString(Object ... args) { - return JPackageStringBundle.MAIN.cannedFormattedString(key, args); - } - - TKit.TextStreamVerifier negateFind() { - var pattern = JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(key, args); - return TKit.assertTextStream(pattern).negate(); - } - - String key() { + @Override + public String format() { return key; } + @Override + public List modelArgs() { + return args; + } + private final String key; - private final Object[] args; + private final List args; } sealed interface VersionSource { @@ -389,16 +383,9 @@ public final class AppVersionTest { } void applyTo(JPackageCommand cmd) { - Objects.requireNonNull(cmd); - new JPackageOutputValidator().expectMatchingStrings(messages).matchTimestamps().stripTimestamps().applyTo(cmd); - cmd.version(version); - - var expectMessageKeys = messages.stream().map(CannedFormattedString::key).toList(); - Stream.of(Message.values()).filter(message -> { - return !expectMessageKeys.contains(message.key()); - }).map(Message::negateFind).forEach(validator -> { - new JPackageOutputValidator().add(validator).stdoutAndStderr().applyTo(cmd); - }); + cmd.version(version).validateOutput(Message.class, validator -> { + validator.matchTimestamps().stripTimestamps(); + }, messages); } @Override @@ -436,7 +423,7 @@ public final class AppVersionTest { } Builder message(Message message, Object ... args) { - return messages(message.cannedFormattedString(args)); + return messages(message.asCannedFormattedString(args)); } private String version; diff --git a/test/jdk/tools/jpackage/share/ErrorTest.java b/test/jdk/tools/jpackage/share/ErrorTest.java index fbaec8283e8..ed5865024fa 100644 --- a/test/jdk/tools/jpackage/share/ErrorTest.java +++ b/test/jdk/tools/jpackage/share/ErrorTest.java @@ -1015,8 +1015,8 @@ public final class ErrorTest { testSpec().noAppDesc().addArgs("--app-image", Token.APP_IMAGE.token()) .error("error.app-image.mac-sign.required"), testSpec().type(PackageType.MAC_PKG).addArgs("--mac-package-identifier", "#1") - .error("message.invalid-identifier", "#1") - .advice("message.invalid-identifier.advice"), + .error("error.parameter-not-mac-bundle-identifier", "#1", "--mac-package-identifier") + .advice("error.parameter-not-mac-bundle-identifier.advice"), // Bundle for mac app store should not have runtime commands testSpec().nativeType().addArgs("--mac-app-store", "--jlink-options", "--bind-services") .error("ERR_MissingJLinkOptMacAppStore", "--strip-native-commands"),