From 6fc8e4998019a2f3ef05ff3e73a4c855c0366d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Casta=C3=B1eda=20Lozano?= Date: Thu, 20 Nov 2025 09:13:57 +0000 Subject: [PATCH 001/616] 8372097: C2: PhasePrintLevel requires setting PrintPhaseLevel explicitly to be active Reviewed-by: mhaessig, chagedorn --- src/hotspot/share/opto/c2_globals.hpp | 2 +- src/hotspot/share/opto/compile.cpp | 2 +- .../compiler/oracle/TestPhasePrintLevel.java | 110 ++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/oracle/TestPhasePrintLevel.java diff --git a/src/hotspot/share/opto/c2_globals.hpp b/src/hotspot/share/opto/c2_globals.hpp index 0a4f231c49b..2b2b4db47b1 100644 --- a/src/hotspot/share/opto/c2_globals.hpp +++ b/src/hotspot/share/opto/c2_globals.hpp @@ -428,7 +428,7 @@ "0=print nothing except PhasePrintLevel directives, " \ "6=all details printed. " \ "Level of detail of printouts can be set on a per-method level " \ - "as well by using CompileCommand=PrintPhaseLevel.") \ + "as well by using CompileCommand=PhasePrintLevel.") \ range(-1, 6) \ \ develop(bool, PrintIdealGraph, false, \ diff --git a/src/hotspot/share/opto/compile.cpp b/src/hotspot/share/opto/compile.cpp index 6babc13e1b3..89b5e36b120 100644 --- a/src/hotspot/share/opto/compile.cpp +++ b/src/hotspot/share/opto/compile.cpp @@ -5233,7 +5233,7 @@ void Compile::end_method() { #ifndef PRODUCT bool Compile::should_print_phase(const int level) const { - return PrintPhaseLevel > 0 && directive()->PhasePrintLevelOption >= level && + return PrintPhaseLevel >= 0 && directive()->PhasePrintLevelOption >= level && _method != nullptr; // Do not print phases for stubs. } diff --git a/test/hotspot/jtreg/compiler/oracle/TestPhasePrintLevel.java b/test/hotspot/jtreg/compiler/oracle/TestPhasePrintLevel.java new file mode 100644 index 00000000000..cf60c5c9be2 --- /dev/null +++ b/test/hotspot/jtreg/compiler/oracle/TestPhasePrintLevel.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025, 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 compiler.oracle; + +import java.util.ArrayList; +import java.util.List; + +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +import compiler.lib.ir_framework.CompilePhase; + +/** + * @test + * @bug 8372097 + * @summary Checks that -XX:CompileCommand=PhasePrintLevel,... interacts with + * -XX:PrintPhaseLevel as expected. + * @library /test/lib / + * @requires vm.debug & vm.compiler2.enabled & vm.flagless + * @run driver compiler.oracle.TestPhasePrintLevel + */ + +public class TestPhasePrintLevel { + + static final String level1Phase = CompilePhase.FINAL_CODE.getName(); + static final String level2Phase = CompilePhase.GLOBAL_CODE_MOTION.getName(); + + public static void main(String[] args) throws Exception { + // Test flag level < 0: nothing should be printed regardless of the compile command level. + test(-1, -1, null, level1Phase); + test(-1, 0, null, level1Phase); + test(-1, 1, null, level1Phase); + + // Test flag level = 0: the compile command level should determine what is printed. + test(0, -1, null, level1Phase); + test(0, 0, null, level1Phase); + test(0, 1, level1Phase, null); + test(0, 2, level2Phase, null); + + // Test flag level > 0: the compile command level should take precedence. + test(1, -1, null, level1Phase); + test(1, 0, null, level1Phase); + test(1, 1, level1Phase, null); + test(2, 1, level1Phase, level2Phase); + test(1, 2, level2Phase, null); + } + + static void test(int flagLevel, int compileCommandLevel, String expectedPhase, String unexpectedPhase) throws Exception { + List options = new ArrayList(); + options.add("-Xbatch"); + options.add("-XX:CompileOnly=" + getTestName()); + options.add("-XX:PrintPhaseLevel=" + flagLevel); + options.add("-XX:CompileCommand=PhasePrintLevel," + getTestName() + "," + compileCommandLevel); + options.add(getTestClass()); + OutputAnalyzer oa = ProcessTools.executeTestJava(options); + oa.shouldHaveExitValue(0) + .shouldContain("CompileCommand: PhasePrintLevel compiler/oracle/TestPhasePrintLevel$TestMain.test intx PhasePrintLevel = " + compileCommandLevel) + .shouldNotContain("CompileCommand: An error occurred during parsing") + .shouldNotContain("# A fatal error has been detected by the Java Runtime Environment"); + if (expectedPhase != null) { + oa.shouldContain(expectedPhase); + } + if (unexpectedPhase != null) { + oa.shouldNotContain(unexpectedPhase); + } + } + + static String getTestClass() { + return TestMain.class.getName(); + } + + static String getTestName() { + return getTestClass() + "::test"; + } + + static class TestMain { + public static void main(String[] args) { + for (int i = 0; i < 10_000; i++) { + test(i); + } + } + + static void test(int i) { + if ((i % 1000) == 0) { + System.out.println("Hello World!"); + } + } + } +} From b41146cd1e5d412f69b893bfb2fd65b6206bb0d2 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 20 Nov 2025 09:32:57 +0000 Subject: [PATCH 002/616] 8367531: Template Framework: use scopes and tokens instead of misbehaving immediate-return-queries Co-authored-by: Christian Hagedorn Reviewed-by: rcastanedalo, mhaessig, chagedorn --- .../arguments/TestMethodArguments.java | 4 +- .../jtreg/compiler/igvn/ExpressionFuzzer.java | 16 +- .../lib/template_framework/AddNameToken.java | 4 + .../lib/template_framework/CodeFrame.java | 123 +- .../lib/template_framework/DataName.java | 219 +- .../compiler/lib/template_framework/Hook.java | 92 +- .../template_framework/HookAnchorToken.java | 5 +- .../template_framework/HookInsertToken.java | 6 +- .../HookIsAnchoredToken.java | 37 + .../lib/template_framework/LetToken.java | 38 + .../template_framework/NameCountToken.java | 39 + .../template_framework/NameForEachToken.java | 41 + .../template_framework/NameHasAnyToken.java | 39 + .../template_framework/NameSampleToken.java | 43 + .../lib/template_framework/NameSet.java | 1 + .../template_framework/NamesToListToken.java | 41 + .../lib/template_framework/Renderer.java | 187 +- .../{TemplateBody.java => ScopeToken.java} | 10 +- .../template_framework/ScopeTokenImpl.java | 42 + ...othingToken.java => SetFuelCostToken.java} | 5 +- .../template_framework/StructuralName.java | 161 +- .../lib/template_framework/Template.java | 510 ++-- .../lib/template_framework/TemplateFrame.java | 111 +- .../lib/template_framework/TemplateToken.java | 10 +- .../lib/template_framework/Token.java | 29 +- .../lib/template_framework/TokenParser.java | 2 +- .../library/Expression.java | 4 +- .../library/PrimitiveType.java | 4 +- .../library/TestFrameworkClass.java | 10 +- .../superword/TestAliasingFuzzer.java | 66 +- .../examples/TestAdvanced.java | 6 +- .../examples/TestExpressions.java | 4 +- .../examples/TestPrimitiveTypes.java | 31 +- .../examples/TestSimple.java | 4 +- .../examples/TestTutorial.java | 836 +++++-- .../examples/TestWithTestFrameworkClass.java | 6 +- .../tests/TestExpression.java | 8 +- .../template_framework/tests/TestFormat.java | 6 +- .../tests/TestTemplate.java | 2207 ++++++++++++++--- 39 files changed, 3988 insertions(+), 1019 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/HookIsAnchoredToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/LetToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NameCountToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NameForEachToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NameHasAnyToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NameSampleToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NamesToListToken.java rename test/hotspot/jtreg/compiler/lib/template_framework/{TemplateBody.java => ScopeToken.java} (78%) create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/ScopeTokenImpl.java rename test/hotspot/jtreg/compiler/lib/template_framework/{NothingToken.java => SetFuelCostToken.java} (89%) diff --git a/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java b/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java index 6ff830e85c6..306d0176aad 100644 --- a/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java +++ b/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java @@ -60,7 +60,7 @@ public class TestMethodArguments { : IntStream.range(0, numberOfArguments) .mapToObj(i -> "x" + i) .collect(Collectors.joining(" + ")); - return Template.make(() -> Template.body( + return Template.make(() -> Template.scope( Template.let("type", type.name()), Template.let("boxedType", type.boxedTypeName()), Template.let("arguments", arguments), @@ -115,7 +115,7 @@ public class TestMethodArguments { tests.add(generateTest(CodeGenerationDataNameType.longs(), i / 2).asToken()); tests.add(generateTest(CodeGenerationDataNameType.doubles(), i / 2).asToken()); } - return Template.make(() -> Template.body( + return Template.make(() -> Template.scope( Template.let("classpath", comp.getEscapedClassPathOfCompiledClasses()), """ import java.util.Arrays; diff --git a/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java b/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java index 60b11e8ffbc..40bfb2e4319 100644 --- a/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java +++ b/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java @@ -45,7 +45,7 @@ import java.util.stream.IntStream; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.$; import compiler.lib.template_framework.library.CodeGenerationDataNameType; @@ -99,7 +99,7 @@ public class ExpressionFuzzer { // Create the body for the test. We use it twice: compiled and reference. // Execute the expression and catch expected Exceptions. - var bodyTemplate = Template.make("expression", "arguments", "checksum", (Expression expression, List arguments, String checksum) -> body( + var bodyTemplate = Template.make("expression", "arguments", "checksum", (Expression expression, List arguments, String checksum) -> scope( """ try { """, @@ -167,14 +167,14 @@ public class ExpressionFuzzer { default -> throw new RuntimeException("not handled: " + type.name()); }; StringPair cmp = cmps.get(RANDOM.nextInt(cmps.size())); - return body( + return scope( ", ", cmp.s0(), type.con(), cmp.s1() ); }); // Checksum method: returns not just the value, but also does some range / bit checks. // This gives us enhanced verification on the range / bits of the result type. - var checksumTemplate = Template.make("expression", "checksum", (Expression expression, String checksum) -> body( + var checksumTemplate = Template.make("expression", "checksum", (Expression expression, String checksum) -> scope( let("returnType", expression.returnType), """ @ForceInline @@ -201,7 +201,7 @@ public class ExpressionFuzzer { // We need to prepare some random values to pass into the test method. We generate the values // once, and pass the same values into both the compiled and reference method. - var valueTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> body( + var valueTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope( "#type #name = ", (type instanceof PrimitiveType pt) ? pt.callLibraryRNG() : type.con(), ";\n" @@ -213,7 +213,7 @@ public class ExpressionFuzzer { // // To ensure that both the compiled and reference method use the same constraint, we put // the computation in a ForceInline method. - var constrainArgumentMethodTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> body( + var constrainArgumentMethodTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope( """ @ForceInline public static #type constrain_#name(#type v) { @@ -247,7 +247,7 @@ public class ExpressionFuzzer { """ )); - var constrainArgumentTemplate = Template.make("name", (String name) -> body( + var constrainArgumentTemplate = Template.make("name", (String name) -> scope( """ #name = constrain_#name(#name); """ @@ -279,7 +279,7 @@ public class ExpressionFuzzer { } } } - return body( + return scope( let("methodArguments", methodArguments.stream().map(ma -> ma.name).collect(Collectors.joining(", "))), let("methodArgumentsWithTypes", diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java index 4f1f7e569bf..ceb1cc263fc 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java @@ -23,4 +23,8 @@ package compiler.lib.template_framework; +/** + * Represents the addition of the specified {@link Name} to the current scope, + * or an outer scope if the inner scope is transparent to {@link Name}s. + */ record AddNameToken(Name name) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java b/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java index 5c4ff55614f..765e9bc42ba 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java @@ -29,22 +29,96 @@ import java.util.ArrayList; import java.util.List; /** - * The {@link CodeFrame} represents a frame (i.e. scope) of code, appending {@link Code} to the {@code 'codeList'} + * The {@link CodeFrame} represents a frame (i.e. scope) of generated code by appending {@link Code} to the {@link #codeList} * as {@link Token}s are rendered, and adding names to the {@link NameSet}s with {@link Template#addStructuralName}/ - * {@link Template#addDataName}. {@link Hook}s can be added to a frame, which allows code to be inserted at that - * location later. When a {@link Hook} is {@link Hook#anchor}ed, it separates the Template into an outer and inner - * {@link CodeFrame}, ensuring that names that are added inside the inner frame are only available inside that frame. + * {@link Template#addDataName}. {@link Hook}s can be added to a code frame, which allows code to be inserted at that + * location later. * *

- * On the other hand, each {@link TemplateFrame} represents the frame (or scope) of exactly one use of a - * Template. + * The {@link CodeFrame} thus implements the {@link Name} non-transparency aspect of {@link ScopeToken}. * *

- * For simple Template nesting, the {@link CodeFrame}s and {@link TemplateFrame}s overlap exactly. - * However, when using {@link Hook#insert}, we simply nest {@link TemplateFrame}s, going further "in", - * but we jump to an outer {@link CodeFrame}, ensuring that we insert {@link Code} at the outer frame, - * and operating on the names of the outer frame. Once the {@link Hook#insert}ion is complete, we jump - * back to the caller {@link TemplateFrame} and {@link CodeFrame}. + * The {@link CodeFrame}s are nested relative to the order of the final rendered code. This can + * diverge from the nesting order of the {@link Template} when using {@link Hook#insert}, where + * the execution jumps from the current (caller) {@link CodeFrame} scope to the scope of the + * {@link Hook#anchor}. This ensures that the {@link Name}s of the anchor scope are accessed, + * and not the ones from the caller scope. Once the {@link Hook#insert}ion is complete, we + * jump back to the caller {@link CodeFrame}. + * + *

+ * Note, that {@link CodeFrame}s and {@link TemplateFrame}s often go together. But they do diverge when + * we call {@link Hook#insert}. On the {@link CodeFrame} side, the inserted scope is nested in the anchoring + * scope, so that the inserted scope has access to the Names of the anchoring scope, and not the caller + * scope. But the {@link TemplateFrame} of the inserted scope is nested in the caller scope, so + * that the inserted scope has access to hashtag replacements of the caller scope, and not the + * anchoring scope. + */ + +/* + * Below, we look at an example, and show the use of CodeFrames (c) and TemplateFrames (t). + * + * Explanations: + * - Generally, every scope has a CodeFrame and a TemplateFrame. There can be multiple + * scopes inside a Template, and so there can be multiple CodeFrames and TemplateFrames. + * In the drawing below, we draw the frames vertically, and give each a unique id. + * - When we nest scopes inside scopes, we create a new CodeFrame and a new TemplateFrame, + * and so they grow the same nested structure. Example: t3 is nested inside t2 and + * c3 is nested inside c2b. + * - The exception to this: + * - At a hook.anchor, there are two CodeFrames. The first one (e.g. c2a) we call the + * hook CodeFrame, it is kept empty until we insert code to the hook. The second + * (e.g. c2b) we call the inner CodeFrame of the anchoring, into which we keep + * generating the code that is inside the scope of the hook.anchor. + * - At a hook.insert, the TemplateFrame (e.g. t4) is nested into the caller (e.g. t3), + * while the CodeFrame (e.g. c4) is nested into the anchoring CodeFrame (e.g. c2a). + * + * Template( + * t1 c1 + * t1 c1 + * t1 c1 Anchoring Scope + * t1 c1 hook.anchor(scope( + * t1 c1 t2 c2a + * t1 c1 t2 c2a <------ CodeFrame nesting--------+ + * t1 c1 t2 c2a with generated code | + * t1 c1 t2 and Names | + * t1 c1 t2 ^ | + * t1 c1 t2 +- Two CodeFramees | + * t1 c1 t2 v | + * t1 c1 t2 | + * t1 c1 t2 c2b | + * t1 c1 t2 c2b | + * t1 c1 t2 c2b Caller Scope | + * t1 c1 t2 c2b ... scope( | + * t1 c1 t2 c2b ... t3 c3 | Insertion Scope + * t1 c1 t2 c2b ... t3 c3 | hook.insert(transparentScope( + * t1 c1 t2 c2b ... t3 c3 | t4 c4 + * t1 c1 t2 c2b ... t3 c3 +---- t4 ----c4 + * t1 c1 t2 c2b ... t3 c3 t4 c4 + * t1 c1 t2 c2b ... t3 c3 <-- TemplateFrame nesting ---t4 c4 + * t1 c1 t2 c2b ... t3 c3 with hashtag t4 c4 // t: Concerns Template Frame + * t1 c1 t2 c2b ... t3 c3 and setFuelCost t4 c4 // c: Concerns Code Frame + * t1 c1 t2 c2b ... t3 c3 t4 c4 "use hashtag #x" -> t: hashtag queried in Insertion (t4) and Caller Scope (t3) + * t1 c1 t2 c2b ... t3 c3 t4 c4 c: code added to Anchoring Scope (c2a) + * t1 c1 t2 c2b ... t3 c3 t4 c4 + * t1 c1 t2 c2b ... t3 c3 t4 c4 let("x", 42) -> t: hashtag definition escapes to Caller Scope (t3) because + * t1 c1 t2 c2b ... t3 c3 t4 c4 Insertion Scope is transparent + * t1 c1 t2 c2b ... t3 c3 t4 c4 + * t1 c1 t2 c2b ... t3 c3 t4 c4 dataNames(...)...sample() -> c: sample from Insertion (c4) and Anchoring Scope (c2a) + * t1 c1 t2 c2b ... t3 c3 t4 c4 (CodeFrame nesting: c2a -> c4) + * t1 c1 t2 c2b ... t3 c3 t4 c4 addDataName(...) -> c: names escape to the Caller Scope (c3) because + * t1 c1 t2 c2b ... t3 c3 t4 c4 Insertion Scope is transparent + * t1 c1 t2 c2b ... t3 c3 t4 c4 + * t1 c1 t2 c2b ... t3 c3 )) + * t1 c1 t2 c2b ... t3 c3 + * t1 c1 t2 c2b ... t3 c3 + * t1 c1 t2 c2b ... ) + * t1 c1 t2 c2b + * t1 c1 t2 c2b + * t1 c1 )) + * t1 c1 + * t1 c1 + * ) + * */ class CodeFrame { public final CodeFrame parent; @@ -78,25 +152,16 @@ class CodeFrame { } /** - * Creates a normal frame, which has a {@link #parent} and which defines an inner - * {@link NameSet}, for the names that are generated inside this frame. Once this - * frame is exited, the name from inside this frame are not available anymore. + * Creates a normal frame, which has a {@link #parent}. It can either be + * transparent for names, meaning that names are added and accessed to and + * from an outer frame. Names that are added in a transparent frame are + * still available in the outer frames, as far out as the next non-transparent + * frame. If a frame is non-transparent, this frame defines an inner + * {@link NameSet}, for the names that are generated inside this frame. Once + * this frame is exited, the names from inside this frame are not available. */ - public static CodeFrame make(CodeFrame parent) { - return new CodeFrame(parent, false); - } - - /** - * Creates a special frame, which has a {@link #parent} but uses the {@link NameSet} - * from the parent frame, allowing {@link Template#addDataName}/ - * {@link Template#addStructuralName} to persist in the outer frame when the current frame - * is exited. This is necessary for {@link Hook#insert}, where we would possibly want to - * make field or variable definitions during the insertion that are not just local to the - * insertion but affect the {@link CodeFrame} that we {@link Hook#anchor} earlier and are - * now {@link Hook#insert}ing into. - */ - public static CodeFrame makeTransparentForNames(CodeFrame parent) { - return new CodeFrame(parent, true); + public static CodeFrame make(CodeFrame parent, boolean isTransparentForNames) { + return new CodeFrame(parent, isTransparentForNames); } void addString(String s) { diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java b/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java index f45a4db8a1e..4a82608567f 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java @@ -24,6 +24,7 @@ package compiler.lib.template_framework; import java.util.List; +import java.util.function.Function; /** * {@link DataName}s represent things like fields and local variables, and can be added to the local @@ -114,18 +115,36 @@ public record DataName(String name, DataName.Type type, boolean mutable, int wei this(mutability, null, null); } + // Wrap the FilteredSet as a Predicate. + private record DataNamePredicate(FilteredSet fs) implements NameSet.Predicate { + public boolean check(Name type) { + return fs.check(type); + } + public String toString() { + return fs.toString(); + } + } + NameSet.Predicate predicate() { if (subtype == null && supertype == null) { throw new UnsupportedOperationException("Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); } - return (Name name) -> { - if (!(name instanceof DataName dataName)) { return false; } - if (mutability == Mutability.MUTABLE && !dataName.mutable()) { return false; } - if (mutability == Mutability.IMMUTABLE && dataName.mutable()) { return false; } - if (subtype != null && !dataName.type().isSubtypeOf(subtype)) { return false; } - if (supertype != null && !supertype.isSubtypeOf(dataName.type())) { return false; } - return true; - }; + return new DataNamePredicate(this); + } + + boolean check(Name name) { + if (!(name instanceof DataName dataName)) { return false; } + if (mutability == Mutability.MUTABLE && !dataName.mutable()) { return false; } + if (mutability == Mutability.IMMUTABLE && dataName.mutable()) { return false; } + if (subtype != null && !dataName.type().isSubtypeOf(subtype)) { return false; } + if (supertype != null && !supertype.isSubtypeOf(dataName.type())) { return false; } + return true; + } + + public String toString() { + String msg1 = (subtype == null) ? "" : ", subtypeOf(" + subtype + ")"; + String msg2 = (supertype == null) ? "" : ", supertypeOf(" + supertype + ")"; + return "DataName.FilterdSet(" + mutability + msg1 + msg2 + ")"; } /** @@ -173,55 +192,179 @@ public record DataName(String name, DataName.Type type, boolean mutable, int wei /** * Samples a random {@link DataName} from the filtered set, according to the weights - * of the contained {@link DataName}s. + * of the contained {@link DataName}s, making the sampled {@link DataName} + * available to an inner scope. * - * @return The sampled {@link DataName}. - * @throws UnsupportedOperationException If the type was not constrained with either of - * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. - * @throws RendererException If the set was empty. - */ - public DataName sample() { - DataName n = (DataName)Renderer.getCurrent().sampleName(predicate()); - if (n == null) { - String msg1 = (subtype == null) ? "" : ", subtypeOf(" + subtype + ")"; - String msg2 = (supertype == null) ? "" : ", supertypeOf(" + supertype + ")"; - throw new RendererException("No variable: " + mutability + msg1 + msg2 + "."); - } - return n; - } - - /** - * Counts the number of {@link DataName}s in the filtered set. - * - * @return The number of {@link DataName}s in the filtered set. + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the sampled {@link DataName}. + * @return a token that represents the sampling and inner scope. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public int count() { - return Renderer.getCurrent().countNames(predicate()); + public Token sample(Function function) { + return new NameSampleToken<>(predicate(), null, null, function); } /** - * Checks if there are any {@link DataName}s in the filtered set. + * Samples a random {@link DataName} from the filtered set, according to the weights + * of the contained {@link DataName}s, and makes a hashtag replacement for both + * the name and type of the {@link DataName}, in the current scope. * - * @return Returns {@code true} iff there is at least one {@link DataName} in the filtered set. + *

+ * Note, that the following two do the equivalent: + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name", "type"), + * """ + * #name #type + * """ + * )); + * } + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * dataNames(MUTABLE).subtypeOf(type).sample((DataName dn) -> transparentScope( + * // The "let" hashtag definitions escape the "transparentScope". + * let("name", dn.name()), + * let("type", dn.type()) + * )), + * """ + * #name #type + * """ + * )); + * } + * + * @param name the key of the hashtag replacement for the {@link DataName} name. + * @param type the key of the hashtag replacement for the {@link DataName} type. + * @return a token that represents the sampling and hashtag replacement definition. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public boolean hasAny() { - return Renderer.getCurrent().hasAnyNames(predicate()); + public Token sampleAndLetAs(String name, String type) { + return new NameSampleToken(predicate(), name, type, n -> Template.transparentScope()); } /** - * Collects all {@link DataName}s in the filtered set. + * Samples a random {@link DataName} from the filtered set, according to the weights + * of the contained {@link DataName}s, and makes a hashtag replacement for the + * name of the {@link DataName}, in the current scope. * + *

+ * Note, that the following two do the equivalent: + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name"), + * """ + * #name + * """ + * )); + * } + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * dataNames(MUTABLE).subtypeOf(type).sample((DataName dn) -> transparentScope( + * // The "let" hashtag definition escape the "transparentScope". + * let("name", dn.name()) + * )), + * """ + * #name + * """ + * )); + * } + * + * @param name the key of the hashtag replacement for the {@link DataName} name. + * @return a token that represents the sampling and hashtag replacement definition. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token sampleAndLetAs(String name) { + return new NameSampleToken(predicate(), name, null, n -> Template.transparentScope()); + } + + /** + * Counts the number of {@link DataName}s in the filtered set, making the count + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the count. + * @return a token that represents the counting and inner scope. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token count(Function function) { + return new NameCountToken(predicate(), function); + } + + /** + * Checks if there are any {@link DataName}s in the filtered set, making the resulting boolean + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the boolean indicating iff there are any {@link DataName}s in the filtered set. + * @return a token that represents the checking and inner scope. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token hasAny(Function function) { + return new NameHasAnyToken(predicate(), function); + } + + /** + * Collects all {@link DataName}s in the filtered set, making the collected list + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the list of {@link DataName}. * @return A {@link List} of all {@link DataName}s in the filtered set. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public List toList() { - List list = Renderer.getCurrent().listNames(predicate()); - return list.stream().map(n -> (DataName)n).toList(); + public Token toList(Function, ScopeToken> function) { + return new NamesToListToken<>(predicate(), function); + } + + /** + * Calls the provided {@code function} for each {@link DataName}s in the filtered set, + * making each of these {@link DataName}s available to a separate inner scope. + * + * @param function The {@link Function} that is called to create the inner {@link ScopeToken}s + * for each of the {@link DataName}s in the filtered set. + * @return The token representing the for-each execution and the respective inner scopes. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token forEach(Function function) { + return new NameForEachToken<>(predicate(), null, null, function); + } + + /** + * Calls the provided {@code function} for each {@link DataName}s in the filtered set, + * making each of these {@link DataName}s available to a separate inner scope, and additionally + * setting hashtag replacements for the {@code name} and {@code type} of the respective + * {@link DataName}s. + * + *

+ * Note, to avoid duplication of the {@code name} and {@code type} + * hashtag replacements, the scope created by the provided {@code function} should be + * non-transparent to hashtag replacements, for example {@link Template#scope} or + * {@link Template#hashtagScope}. + * + * @param name the key of the hashtag replacement for each individual {@link DataName} name. + * @param type the key of the hashtag replacement for each individual {@link DataName} type. + * @param function The {@link Function} that is called to create the inner {@link ScopeToken}s + * for each of the {@link DataName}s in the filtereds set. + * @return The token representing the for-each execution and the respective inner scopes. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token forEach(String name, String type, Function function) { + return new NameForEachToken<>(predicate(), name, type, function); } } } diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java b/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java index 8ee2689eb2f..ef5a5df6ce0 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java @@ -23,59 +23,84 @@ package compiler.lib.template_framework; +import java.util.function.Function; + /** - * {@link Hook}s can be {@link #anchor}ed for a certain scope in a Template, and all nested - * Templates in this scope, and then from within this scope, any Template can - * {@link #insert} code to where the {@link Hook} was {@link #anchor}ed. This can be useful to reach - * "back" or to some outer scope, e.g. while generating code for a method, one can reach out - * to the class scope to insert fields. + * A {@link Hook} can be {@link #anchor}ed for a certain scope ({@link ScopeToken}), and that + * anchoring stays active for any nested scope or nested {@link Template}. With {@link #insert}, + * one can insert a template ({@link TemplateToken}) or scope ({@link ScopeToken}) to where the + * {@link Hook} was {@link #anchor}'ed. If the hook was anchored for multiple outer scopes, the + * innermost is chosen for insertion. * *

+ * This can be useful to reach "back" or to some outer scope, e.g. while generating code for a + * method, one can reach out to the class scope to insert fields. Or one may want to reach back + * to the beginning of a method to insert local variables that should be live for the whole method. + * + *

+ * The choice of {@link ScopeToken} is very important and powerful. + * For example, if you want to insert a {@link DataName} to the scope of an anchor, + * it is important that the scope of the insertion is transparent for {@link DataName}s, + * e.g. using {@link Template#transparentScope}. In most cases, we want {@link DataName}s to escape + * the inserted scope but not the anchor scope, so the anchor scope should be + * non-transparent for {@link DataName}s, e.g. using {@link Template#scope}. * Example: + * + *

* {@snippet lang=java : * var myHook = new Hook("MyHook"); * - * var template1 = Template.make("name", (String name) -> body( - * """ - * public static int #name = 42; - * """ - * )); - * - * var template2 = Template.make(() -> body( + * var template = Template.make(() -> scope( * """ * public class Test { * """, * // Anchor the hook here. - * myHook.anchor( + * myHook.anchor(scope( * """ * public static void main(String[] args) { * System.out.println("$field: " + $field) * """, - * // Reach out to where the hook was anchored, and insert the code of template1. - * myHook.insert(template1.asToken($("field"))), + * // Reach out to where the hook was anchored, and insert some code. + * myHook.insert(transparentScope( + * // The field (DataName) escapes because the inserted scope is "transparentScope" + * addDataName($("field"), Primitives.INTS, MUTABLE), + * """ + * public static int $field = 42; + * """ + * )), * """ * } * """ - * ), + * )), * """ * } * """ * )); * } * + *

+ * Note that if we use {@link #insert} with {@link Template#transparentScope}, then + * {@link DataName}s and {@link StructuralName}s escape from the inserted scope to the + * anchor scope, but hashtag replacements and {@link Template#setFuelCost} escape to + * the caller, i.e. from where we inserted the scope. This makes sense if we consider + * {@link DataName}s belonging to the structure of the generated code and the inserted + * scope belonging to the anchor scope. On the other hand, hashtag replacements and + * {@link Template#setFuelCost} rather belong to the code generation that happens + * within the context of a template. + * * @param name The name of the Hook, for debugging purposes only. */ public record Hook(String name) { /** - * Anchor this {@link Hook} for the scope of the provided {@code 'tokens'}. + * Anchor this {@link Hook} for the provided inner scope. * From anywhere inside this scope, even in nested Templates, code can be * {@link #insert}ed back to the location where this {@link Hook} was {@link #anchor}ed. * - * @param tokens A list of tokens, which have the same restrictions as {@link Template#body}. - * @return A {@link Token} that captures the anchoring of the scope and the list of validated {@link Token}s. + * @param innerScope An inner scope, for which the {@link Hook} is anchored. + * @return A {@link Token} that captures the anchoring and the inner scope. */ - public Token anchor(Object... tokens) { - return new HookAnchorToken(this, TokenParser.parse(tokens)); + public Token anchor(ScopeToken innerScope) { + return new HookAnchorToken(this, innerScope); } /** @@ -83,18 +108,31 @@ public record Hook(String name) { * This could be in the same Template, or one nested further out. * * @param templateToken The Template with applied arguments to be inserted at the {@link Hook}. - * @return The {@link Token} which when used inside a {@link Template#body} performs the code insertion into the {@link Hook}. + * @return The {@link Token} which represents the code insertion into the {@link Hook}. */ public Token insert(TemplateToken templateToken) { - return new HookInsertToken(this, templateToken); + return new HookInsertToken(this, Template.transparentScope(templateToken)); } /** - * Checks if the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope. + * Inserts a scope ({@link ScopeToken}) to the innermost location where this {@link Hook} was {@link #anchor}ed. + * This could be in the same Template, or one nested further out. * - * @return If the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope. + * @param scopeToken The scope to be inserted at the {@link Hook}. + * @return The {@link Token} which represents the code insertion into the {@link Hook}. */ - public boolean isAnchored() { - return Renderer.getCurrent().isAnchored(this); + public Token insert(ScopeToken scopeToken) { + return new HookInsertToken(this, scopeToken); + } + + /** + * Checks if the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope, + * and makes the boolean result available to an inner scope. + * + * @param function the function that generates the inner scope given the boolean result. + * @return the token that represents the check and inner scope. + */ + public Token isAnchored(Function function) { + return new HookIsAnchoredToken(this, function); } } diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java index b025c5ff041..4979365b3d0 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java @@ -25,4 +25,7 @@ package compiler.lib.template_framework; import java.util.List; -record HookAnchorToken(Hook hook, List tokens) implements Token {} +/** + * Represents the {@link Hook#anchor} with its inner scope. + */ +record HookAnchorToken(Hook hook, ScopeToken innerScope) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java index de8b60bbf24..a433d472a6e 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java @@ -23,4 +23,8 @@ package compiler.lib.template_framework; -record HookInsertToken(Hook hook, TemplateToken templateToken) implements Token {} +/** + * Represents the {@link Hook#insert} with the {@link ScopeToken} of the + * scope that is to be inserted. + */ +record HookInsertToken(Hook hook, ScopeToken scopeToken) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/HookIsAnchoredToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/HookIsAnchoredToken.java new file mode 100644 index 00000000000..5c7b92ec1fe --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/HookIsAnchoredToken.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents an {@link Hook#isAnchored} query with the function that creates an inner scope + * given the boolean answer. + */ +record HookIsAnchoredToken(Hook hook, Function function) implements Token { + + ScopeToken getScopeToken(boolean isAnchored) { + return function().apply(isAnchored); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/LetToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/LetToken.java new file mode 100644 index 00000000000..ee18dd440b7 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/LetToken.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents a let (aka hashtag) definition. The hashtag replacement is active for the + * scope ({@link ScopeToken}) that the {@code function} creates, but can escape that + * scope if it is transparent to hashtags. + */ +record LetToken(String key, T value, Function function) implements Token { + + ScopeToken getScopeToken() { + return function().apply(value); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameCountToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameCountToken.java new file mode 100644 index 00000000000..f0344efdd08 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameCountToken.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents the counting of {@link Name}s, and the function that is called + * to create an inner scope given the count. + */ +record NameCountToken( + NameSet.Predicate predicate, + Function function) implements Token { + + ScopeToken getScopeToken(int count) { + return function().apply(count); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameForEachToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameForEachToken.java new file mode 100644 index 00000000000..0e629740be1 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameForEachToken.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents the for-each execution of the provided function and (optional) hashtag replacement + * keys for name and type of each name. + */ +record NameForEachToken( + NameSet.Predicate predicate, + String name, + String type, + Function function) implements Token { + + ScopeToken getScopeToken(Name n) { + return function().apply((N)n); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameHasAnyToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameHasAnyToken.java new file mode 100644 index 00000000000..a31990af210 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameHasAnyToken.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents the check if there is any name and the function that is to + * be called given the boolean value (true iff there are any names). + */ +record NameHasAnyToken( + NameSet.Predicate predicate, + Function function) implements Token { + + ScopeToken getScopeToken(boolean hasAny) { + return function().apply(hasAny); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameSampleToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameSampleToken.java new file mode 100644 index 00000000000..0b01f00fcd9 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameSampleToken.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; + +/** + * Represents the sampling of {@link Name}s, and the function that is called given + * the sampled name, as well as the (optional) hashtag replacement keys for the + * name and type of the sampled name, which are then available in the inner scope + * created by the provided function. + */ +record NameSampleToken( + NameSet.Predicate predicate, + String name, + String type, + Function function) implements Token { + + ScopeToken getScopeToken(Name n) { + return function().apply((N)n); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java index ef79c33d48a..403dbdc694f 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java @@ -43,6 +43,7 @@ class NameSet { interface Predicate { boolean check(Name type); + String toString(); // used when sampling fails. } NameSet(NameSet parent) { diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NamesToListToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NamesToListToken.java new file mode 100644 index 00000000000..40710a01297 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NamesToListToken.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.function.Function; +import java.util.List; + +/** + * Represents the {@code toList} on a filtered name set, including the collection of the + * names and the creation of the inner scope with the function. + */ +record NamesToListToken( + NameSet.Predicate predicate, + Function, ScopeToken> function) implements Token { + + ScopeToken getScopeToken(List names) { + List castNames = names.stream().map(n -> (N)n).toList(); + return function().apply(castNames); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java index 14adfc81d3f..61ab9ab343c 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java @@ -76,7 +76,7 @@ final class Renderer { *

* When using nested templates, the user of the Template Framework may be tempted to first render * the nested template to a {@link String}, and then use this {@link String} as a token in an outer - * {@link Template#body}. This would be a bad pattern: the outer and nested {@link Template} would + * {@link Template#scope}. This would be a bad pattern: the outer and nested {@link Template} would * be rendered separately, and could not interact. For example, the nested {@link Template} would * not have access to the scopes of the outer {@link Template}. The inner {@link Template} could * not access {@link Name}s and {@link Hook}s from the outer {@link Template}. The user might assume @@ -84,8 +84,8 @@ final class Renderer { * be separated. This could lead to unexpected behavior or even bugs. * *

- * Instead, the user should create a {@link TemplateToken} from the inner {@link Template}, and - * use that {@link TemplateToken} in the {@link Template#body} of the outer {@link Template}. + * Instead, the user must create a {@link TemplateToken} from the inner {@link Template}, and + * use that {@link TemplateToken} in the {@link Template#scope} of the outer {@link Template}. * This way, the inner and outer {@link Template}s get rendered together, and the inner {@link Template} * has access to the {@link Name}s and {@link Hook}s of the outer {@link Template}. * @@ -113,7 +113,7 @@ final class Renderer { static Renderer getCurrent() { if (renderer == null) { - throw new RendererException("A Template method such as '$', 'let', 'sample', 'count' etc. was called outside a template rendering."); + throw new RendererException("A Template method such as '$', 'fuel', etc. was called outside a template rendering call."); } return renderer; } @@ -171,26 +171,6 @@ final class Renderer { return currentTemplateFrame.fuel; } - void setFuelCost(float fuelCost) { - currentTemplateFrame.setFuelCost(fuelCost); - } - - Name sampleName(NameSet.Predicate predicate) { - return currentCodeFrame.sampleName(predicate); - } - - int countNames(NameSet.Predicate predicate) { - return currentCodeFrame.countNames(predicate); - } - - boolean hasAnyNames(NameSet.Predicate predicate) { - return currentCodeFrame.hasAnyNames(predicate); - } - - List listNames(NameSet.Predicate predicate) { - return currentCodeFrame.listNames(predicate); - } - /** * Formats values to {@link String} with the goal of using them in Java code. * By default, we use the overrides of {@link Object#toString}. @@ -243,12 +223,16 @@ final class Renderer { } private void renderTemplateToken(TemplateToken templateToken) { + // We need a TemplateFrame in all cases, this ensures that the outermost scope of the template + // is not transparent for hashtags and setFuelCost, and also that the id of the template is + // unique. TemplateFrame templateFrame = TemplateFrame.make(currentTemplateFrame, nextTemplateFrameId++); currentTemplateFrame = templateFrame; templateToken.visitArguments((name, value) -> addHashtagReplacement(name, format(value))); - TemplateBody body = templateToken.instantiate(); - renderTokenList(body.tokens()); + + // If the ScopeToken is transparent to Names, then the Template is transparent to names. + renderScopeToken(templateToken.instantiate()); if (currentTemplateFrame != templateFrame) { throw new RuntimeException("Internal error: TemplateFrame mismatch!"); @@ -256,29 +240,76 @@ final class Renderer { currentTemplateFrame = currentTemplateFrame.parent; } + private void renderScopeToken(ScopeToken st) { + renderScopeToken(st, () -> {}); + } + + private void renderScopeToken(ScopeToken st, Runnable preamble) { + if (!(st instanceof ScopeTokenImpl(List tokens, + boolean isTransparentForNames, + boolean isTransparentForHashtags, + boolean isTransparentForSetFuelCost))) { + throw new RuntimeException("Internal error: could not unpack ScopeTokenImpl."); + } + + // We need the CodeFrame for local names. + CodeFrame outerCodeFrame = currentCodeFrame; + if (!isTransparentForNames) { + currentCodeFrame = CodeFrame.make(currentCodeFrame, false); + } + + // We need to be able to define local hashtag replacements, but still + // see the outer ones. We also need to have the same id for dollar + // replacement as the outer frame. And we need to be able to allow + // local setFuelCost definitions. + TemplateFrame innerTemplateFrame = null; + if (!isTransparentForHashtags || !isTransparentForSetFuelCost) { + innerTemplateFrame = TemplateFrame.makeInnerScope(currentTemplateFrame, + isTransparentForHashtags, + isTransparentForSetFuelCost); + currentTemplateFrame = innerTemplateFrame; + } + + // Allow definition of hashtags and variables to be placed in the nested frames. + preamble.run(); + + // Now render the nested code. + renderTokenList(tokens); + + if (!isTransparentForHashtags || !isTransparentForSetFuelCost) { + if (currentTemplateFrame != innerTemplateFrame) { + throw new RuntimeException("Internal error: TemplateFrame mismatch!"); + } + currentTemplateFrame = currentTemplateFrame.parent; + } + + // Tear down CodeFrame nesting. If no nesting happened, the code is already + // in the currentCodeFrame. + if (!isTransparentForNames) { + outerCodeFrame.addCode(currentCodeFrame.getCode()); + currentCodeFrame = outerCodeFrame; + } + } + private void renderToken(Token token) { switch (token) { case StringToken(String s) -> { renderStringWithDollarAndHashtagReplacements(s); } - case NothingToken() -> { - // Nothing. - } - case HookAnchorToken(Hook hook, List tokens) -> { + case HookAnchorToken(Hook hook, ScopeTokenImpl innerScope) -> { CodeFrame outerCodeFrame = currentCodeFrame; - // We need a CodeFrame to which the hook can insert code. That way, name - // definitions at the hook cannot escape the hookCodeFrame. - CodeFrame hookCodeFrame = CodeFrame.make(outerCodeFrame); + // We need a CodeFrame to which the hook can insert code. If the nested names + // are to be local, the CodeFrame must be non-transparent for names. + CodeFrame hookCodeFrame = CodeFrame.make(outerCodeFrame, innerScope.isTransparentForNames()); hookCodeFrame.addHook(hook); - // We need a CodeFrame where the tokens can be rendered. That way, name - // definitions from the tokens cannot escape the innerCodeFrame to the - // hookCodeFrame. - CodeFrame innerCodeFrame = CodeFrame.make(hookCodeFrame); + // We need a CodeFrame where the tokens can be rendered for code that is + // generated inside the anchor scope, but not inserted directly to the hook. + CodeFrame innerCodeFrame = CodeFrame.make(hookCodeFrame, innerScope.isTransparentForNames()); currentCodeFrame = innerCodeFrame; - renderTokenList(tokens); + renderScopeToken(innerScope); // Close the hookCodeFrame and innerCodeFrame. hookCodeFrame code comes before the // innerCodeFrame code from the tokens. @@ -286,20 +317,20 @@ final class Renderer { currentCodeFrame.addCode(hookCodeFrame.getCode()); currentCodeFrame.addCode(innerCodeFrame.getCode()); } - case HookInsertToken(Hook hook, TemplateToken templateToken) -> { + case HookInsertToken(Hook hook, ScopeTokenImpl scopeToken) -> { // Switch to hook CodeFrame. CodeFrame callerCodeFrame = currentCodeFrame; CodeFrame hookCodeFrame = codeFrameForHook(hook); // Use a transparent nested CodeFrame. We need a CodeFrame so that the code generated - // by the TemplateToken can be collected, and hook insertions from it can still - // be made to the hookCodeFrame before the code from the TemplateToken is added to + // by the scopeToken can be collected, and hook insertions from it can still + // be made to the hookCodeFrame before the code from the scopeToken is added to // the hookCodeFrame. // But the CodeFrame must be transparent, so that its name definitions go out to - // the hookCodeFrame, and are not limited to the CodeFrame for the TemplateToken. - currentCodeFrame = CodeFrame.makeTransparentForNames(hookCodeFrame); + // the hookCodeFrame, and are not limited to the CodeFrame for the scopeToken. + currentCodeFrame = CodeFrame.make(hookCodeFrame, true); - renderTemplateToken(templateToken); + renderScopeToken(scopeToken); hookCodeFrame.addCode(currentCodeFrame.getCode()); @@ -307,18 +338,68 @@ final class Renderer { currentCodeFrame = callerCodeFrame; } case TemplateToken templateToken -> { - // Use a nested CodeFrame. - CodeFrame callerCodeFrame = currentCodeFrame; - currentCodeFrame = CodeFrame.make(currentCodeFrame); - renderTemplateToken(templateToken); - - callerCodeFrame.addCode(currentCodeFrame.getCode()); - currentCodeFrame = callerCodeFrame; } case AddNameToken(Name name) -> { currentCodeFrame.addName(name); } + case ScopeToken scopeToken -> { + renderScopeToken(scopeToken); + } + case NameSampleToken nameScopeToken -> { + Name name = currentCodeFrame.sampleName(nameScopeToken.predicate()); + if (name == null) { + throw new RendererException("No Name found for " + nameScopeToken.predicate().toString()); + } + ScopeToken scopeToken = nameScopeToken.getScopeToken(name); + renderScopeToken(scopeToken, () -> { + if (nameScopeToken.name() != null) { + addHashtagReplacement(nameScopeToken.name(), name.name()); + } + if (nameScopeToken.type() != null) { + addHashtagReplacement(nameScopeToken.type(), name.type()); + } + }); + } + case NameForEachToken nameForEachToken -> { + List list = currentCodeFrame.listNames(nameForEachToken.predicate()); + list.stream().forEach(name -> { + ScopeToken scopeToken = nameForEachToken.getScopeToken(name); + renderScopeToken(scopeToken, () -> { + if (nameForEachToken.name() != null) { + addHashtagReplacement(nameForEachToken.name(), name.name()); + } + if (nameForEachToken.type() != null) { + addHashtagReplacement(nameForEachToken.type(), name.type()); + } + }); + }); + } + case NamesToListToken nameToListToken -> { + List list = currentCodeFrame.listNames(nameToListToken.predicate()); + renderScopeToken(nameToListToken.getScopeToken(list)); + } + case NameCountToken nameCountToken -> { + int count = currentCodeFrame.countNames(nameCountToken.predicate()); + renderScopeToken(nameCountToken.getScopeToken(count)); + } + case NameHasAnyToken nameHasAnyToken -> { + boolean hasAny = currentCodeFrame.hasAnyNames(nameHasAnyToken.predicate()); + renderScopeToken(nameHasAnyToken.getScopeToken(hasAny)); + } + case SetFuelCostToken(float fuelCost) -> { + currentTemplateFrame.setFuelCost(fuelCost); + } + case LetToken letToken -> { + ScopeToken scopeToken = letToken.getScopeToken(); + renderScopeToken(scopeToken, () -> { + addHashtagReplacement(letToken.key(), letToken.value()); + }); + } + case HookIsAnchoredToken hookIsAnchoredToken -> { + boolean isAnchored = currentCodeFrame.codeFrameForHook(hookIsAnchoredToken.hook()) != null; + renderScopeToken(hookIsAnchoredToken.getScopeToken(isAnchored)); + } } } @@ -423,10 +504,6 @@ final class Renderer { )); } - boolean isAnchored(Hook hook) { - return currentCodeFrame.codeFrameForHook(hook) != null; - } - private CodeFrame codeFrameForHook(Hook hook) { CodeFrame codeFrame = currentCodeFrame.codeFrameForHook(hook); if (codeFrame == null) { diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java b/test/hotspot/jtreg/compiler/lib/template_framework/ScopeToken.java similarity index 78% rename from test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java rename to test/hotspot/jtreg/compiler/lib/template_framework/ScopeToken.java index 440766b3f79..f81215da36b 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/ScopeToken.java @@ -23,12 +23,8 @@ package compiler.lib.template_framework; -import java.util.List; - /** - * A Template generates a {@link TemplateBody}, which is a list of {@link Token}s, - * which are then later rendered to {@link String}s. - * - * @param tokens The list of {@link Token}s that are later rendered to {@link String}s. + * A {@link ScopeToken} represents a scope in a {@link Template}, which can be + * created with {@link Template#scope}, {@link Template#transparentScope}, and other related methods. */ -public record TemplateBody(List tokens) {} +public sealed interface ScopeToken extends Token permits ScopeTokenImpl {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/ScopeTokenImpl.java b/test/hotspot/jtreg/compiler/lib/template_framework/ScopeTokenImpl.java new file mode 100644 index 00000000000..df95bd56722 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/ScopeTokenImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, 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 compiler.lib.template_framework; + +import java.util.List; + +/** + * Represents a scope with its tokens. Boolean flags indicate if names, + * hashtag replacements and {@link Template#setFuelCost} are local, or escape to + * outer scopes. + * + *

+ * Note: We want the {@link ScopeToken} to be public, but the internals of the + * record should be private. One way to solve this is with a public interface + * that exposes nothing but its name, and a private implementation via a + * record that allows easy destructuring with pattern matching. + */ +record ScopeTokenImpl(List tokens, + boolean isTransparentForNames, + boolean isTransparentForHashtags, + boolean isTransparentForSetFuelCost) implements ScopeToken, Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/SetFuelCostToken.java similarity index 89% rename from test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java rename to test/hotspot/jtreg/compiler/lib/template_framework/SetFuelCostToken.java index 540eaf1e14c..08e219b2cd9 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/SetFuelCostToken.java @@ -23,4 +23,7 @@ package compiler.lib.template_framework; -record NothingToken() implements Token {} +/** + * Represents the setting of the fuel cost in the current scope. + */ +record SetFuelCostToken(float fuelCost) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java b/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java index 866ac6dbfb8..8a1090bc5ab 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java @@ -24,6 +24,7 @@ package compiler.lib.template_framework; import java.util.List; +import java.util.function.Function; /** * {@link StructuralName}s represent things like method and class names, and can be added to the local @@ -89,16 +90,34 @@ public record StructuralName(String name, StructuralName.Type type, int weight) this(null, null); } + // Wrap the FilteredSet as a Predicate. + private record StructuralNamePredicate(FilteredSet fs) implements NameSet.Predicate { + public boolean check(Name type) { + return fs.check(type); + } + public String toString() { + return fs.toString(); + } + } + NameSet.Predicate predicate() { if (subtype == null && supertype == null) { throw new UnsupportedOperationException("Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); } - return (Name name) -> { - if (!(name instanceof StructuralName structuralName)) { return false; } - if (subtype != null && !structuralName.type().isSubtypeOf(subtype)) { return false; } - if (supertype != null && !supertype.isSubtypeOf(structuralName.type())) { return false; } - return true; - }; + return new StructuralNamePredicate(this); + } + + boolean check(Name name) { + if (!(name instanceof StructuralName structuralName)) { return false; } + if (subtype != null && !structuralName.type().isSubtypeOf(subtype)) { return false; } + if (supertype != null && !supertype.isSubtypeOf(structuralName.type())) { return false; } + return true; + } + + public String toString() { + String msg1 = (subtype == null) ? "" : " subtypeOf(" + subtype + ")"; + String msg2 = (supertype == null) ? "" : " supertypeOf(" + supertype + ")"; + return "StructuralName.FilteredSet(" + msg1 + msg2 + ")"; } /** @@ -146,55 +165,125 @@ public record StructuralName(String name, StructuralName.Type type, int weight) /** * Samples a random {@link StructuralName} from the filtered set, according to the weights - * of the contained {@link StructuralName}s. + * of the contained {@link StructuralName}s, making the sampled {@link StructuralName} + * available to an inner scope. * - * @return The sampled {@link StructuralName}. - * @throws UnsupportedOperationException If the type was not constrained with either of - * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. - * @throws RendererException If the set was empty. - */ - public StructuralName sample() { - StructuralName n = (StructuralName)Renderer.getCurrent().sampleName(predicate()); - if (n == null) { - String msg1 = (subtype == null) ? "" : " subtypeOf(" + subtype + ")"; - String msg2 = (supertype == null) ? "" : " supertypeOf(" + supertype + ")"; - throw new RendererException("No variable:" + msg1 + msg2 + "."); - } - return n; - } - - /** - * Counts the number of {@link StructuralName}s in the filtered set. - * - * @return The number of {@link StructuralName}s in the filtered set. + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the sampled {@link StructuralName}. + * @return a token that represents the sampling and inner scope. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public int count() { - return Renderer.getCurrent().countNames(predicate()); + public Token sample(Function function) { + return new NameSampleToken<>(predicate(), null, null, function); } /** - * Checks if there are any {@link StructuralName}s in the filtered set. + * Samples a random {@link StructuralName} from the filtered set, according to the weights + * of the contained {@link StructuralName}s, and makes a hashtag replacement for both + * the name and type of the {@link StructuralName}, in the current scope. * - * @return Returns {@code true} iff there is at least one {@link StructuralName} in the filtered set. + * @param name the key of the hashtag replacement for the {@link StructuralName} name. + * @param type the key of the hashtag replacement for the {@link StructuralName} type. + * @return a token that represents the sampling and hashtag replacement definition. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public boolean hasAny() { - return Renderer.getCurrent().hasAnyNames(predicate()); + public Token sampleAndLetAs(String name, String type) { + return new NameSampleToken(predicate(), name, type, n -> Template.transparentScope()); } /** - * Collects all {@link StructuralName}s in the filtered set. + * Samples a random {@link StructuralName} from the filtered set, according to the weights + * of the contained {@link StructuralName}s, and makes a hashtag replacement for the + * name of the {@link StructuralName}, in the current scope. * + * @param name the key of the hashtag replacement for the {@link StructuralName} name. + * @return a token that represents the sampling and hashtag replacement definition. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token sampleAndLetAs(String name) { + return new NameSampleToken(predicate(), name, null, n -> Template.transparentScope()); + } + + /** + * Counts the number of {@link StructuralName}s in the filtered set, making the count + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the count. + * @return a token that represents the counting and inner scope. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token count(Function function) { + return new NameCountToken(predicate(), function); + } + + /** + * Checks if there are any {@link StructuralName}s in the filtered set, making the resulting boolean + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the boolean indicating iff there are any {@link StructuralName}s in the filtered set. + * @return a token that represents the checking and inner scope. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token hasAny(Function function) { + return new NameHasAnyToken(predicate(), function); + } + /** + * Collects all {@link StructuralName}s in the filtered set, making the collected list + * available to an inner scope. + * + * @param function The {@link Function} that creates the inner {@link ScopeToken} given + * the list of {@link StructuralName}. * @return A {@link List} of all {@link StructuralName}s in the filtered set. * @throws UnsupportedOperationException If the type was not constrained with either of * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. */ - public List toList() { - List list = Renderer.getCurrent().listNames(predicate()); - return list.stream().map(n -> (StructuralName)n).toList(); + public Token toList(Function, ScopeToken> function) { + return new NamesToListToken<>(predicate(), function); + } + + /** + * Calls the provided {@code function} for each {@link StructuralName}s in the filtered set, + * making each of these {@link StructuralName}s available to a separate inner scope. + * + * @param function The {@link Function} that is called to create the inner {@link ScopeToken}s + * for each of the {@link StructuralName}s in the filtereds set. + * @return The token representing the for-each execution and the respective inner scopes. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token forEach(Function function) { + return new NameForEachToken<>(predicate(), null, null, function); + } + + /** + * Calls the provided {@code function} for each {@link StructuralName}s in the filtered set, + * making each of these {@link StructuralName}s available to a separate inner scope, and additionally + * setting hashtag replacements for the {@code name} and {@code type} of the respective + * {@link StructuralName}s. + * + *

+ * Note, to avoid duplication of the {@code name} and {@code type} + * hashtag replacements, the scope created by the provided {@code function} should be + * non-transparent to hashtag replacements, for example {@link Template#scope} or + * {@link Template#hashtagScope}. + * + * @param name the key of the hashtag replacement for each individual {@link StructuralName} name. + * @param type the key of the hashtag replacement for each individual {@link StructuralName} type. + * @param function The {@link Function} that is called to create the inner {@link ScopeToken}s + * for each of the {@link StructuralName}s in the filtereds set. + * @return The token representing the for-each execution and the respective inner scopes. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public Token forEach(String name, String type, Function function) { + return new NameForEachToken<>(predicate(), name, type, function); } } } diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Template.java b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java index 57d06e732bb..f245cda0501 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Template.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java @@ -65,7 +65,7 @@ import compiler.lib.ir_framework.TestFramework; * *

* {@snippet lang=java : - * var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> body( + * var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> scope( * let("con1", generator.next()), * let("con2", generator.next()), * """ @@ -86,13 +86,13 @@ import compiler.lib.ir_framework.TestFramework; * } * *

- * To get an executable test, we define a {@link Template} that produces a class body with a main method. The Template + * To get an executable test, we define a {@link Template} that produces a class scope with a main method. The Template * takes a list of types, and calls the {@code testTemplate} defined above for each type and operator. We use * the {@link TestFramework} to call our {@code @Test} methods. * *

* {@snippet lang=java : - * var classTemplate = Template.make("types", (List types) -> body( + * var classTemplate = Template.make("types", (List types) -> scope( * let("classpath", comp.getEscapedClassPathOfCompiledClasses()), * """ * package p.xyz; @@ -148,12 +148,12 @@ import compiler.lib.ir_framework.TestFramework; * {@link Template#make(String, Function)}. For each number of arguments there is an implementation * (e.g. {@link Template.TwoArgs} for two arguments). This allows the use of generics for the * {@link Template} argument types which enables type checking of the {@link Template} arguments. - * It is currently only allowed to use up to three arguments. + * It is currently only allowed to use up to three arguments. * *

* A {@link Template} can be rendered to a {@link String} (e.g. {@link Template.ZeroArgs#render()}). * Alternatively, we can generate a {@link Token} (more specifically, a {@link TemplateToken}) with {@code asToken()} - * (e.g. {@link Template.ZeroArgs#asToken()}), and use the {@link Token} inside another {@link Template#body}. + * (e.g. {@link Template.ZeroArgs#asToken()}), and use the {@link Token} inside another {@link Template#scope}. * *

* Ideally, we would have used string templates to inject these Template @@ -161,6 +161,11 @@ import compiler.lib.ir_framework.TestFramework; * hashtag replacements in the {@link String}s: the Template argument names are captured, and * the argument values automatically replace any {@code "#name"} in the {@link String}s. See the different overloads * of {@link #make} for examples. Additional hashtag replacements can be defined with {@link #let}. + * We have decided to keep hashtag replacements constrained to the scope of one Template. They + * do not escape to outer or inner Template uses. If one needs to pass values to inner Templates, + * this can be done with Template arguments. Keeping hashtag replacements local to Templates + * has the benefit that there is no conflict in recursive templates, where outer and inner Templates + * define the same hashtag replacement. * *

* When using nested Templates, there can be collisions with identifiers (e.g. variable names and method names). @@ -176,25 +181,6 @@ import compiler.lib.ir_framework.TestFramework; * {@code #{name}}. * *

- * A {@link TemplateToken} cannot just be used in {@link Template#body}, but it can also be - * {@link Hook#insert}ed to where a {@link Hook} was {@link Hook#anchor}ed earlier (in some outer scope of the code). - * For example, while generating code in a method, one can reach out to the scope of the class, and insert a - * new field, or define a utility method. - * - *

- * A {@link TemplateBinding} allows the recursive use of Templates. With the indirection of such a binding, - * a Template can reference itself. - * - *

- * The writer of recursive {@link Template}s must ensure that this recursion terminates. To unify the - * approach across {@link Template}s, we introduce the concept of {@link #fuel}. Templates are rendered starting - * with a limited amount of {@link #fuel} (default: 100, see {@link #DEFAULT_FUEL}), which is decreased at each - * Template nesting by a certain amount (default: 10, see {@link #DEFAULT_FUEL_COST}). The default fuel for a - * template can be changed when we {@code render()} it (e.g. {@link ZeroArgs#render(float)}) and the default - * fuel cost with {@link #setFuelCost}) when defining the {@link #body(Object...)}. Recursive templates are - * supposed to terminate once the {@link #fuel} is depleted (i.e. reaches zero). - * - *

* Code generation can involve keeping track of fields and variables, as well as the scopes in which they * are available, and if they are mutable or immutable. We model fields and variables with {@link DataName}s, * which we can add to the current scope with {@link #addDataName}. We can access the {@link DataName}s with @@ -211,61 +197,70 @@ import compiler.lib.ir_framework.TestFramework; * are not concerned about mutability. * *

- * When working with {@link DataName}s and {@link StructuralName}s, it is important to be aware of the - * relevant scopes, as well as the execution order of the {@link Template} lambdas and the evaluation - * of the {@link Template#body} tokens. When a {@link Template} is rendered, its lambda is invoked. In the - * lambda, we generate the tokens, and create the {@link Template#body}. Once the lambda returns, the - * tokens are evaluated one by one. While evaluating the tokens, the {@link Renderer} might encounter a nested - * {@link TemplateToken}, which in turn triggers the evaluation of that nested {@link Template}, i.e. - * the evaluation of its lambda and later the evaluation of its tokens. It is important to keep in mind - * that the lambda is always executed first, and the tokens are evaluated afterwards. A method like - * {@code dataNames(MUTABLE).exactOf(type).count()} is a method that is executed during the evaluation - * of the lambda. But a method like {@link #addDataName} returns a token, and does not immediately add - * the {@link DataName}. This ensures that the {@link DataName} is only inserted when the tokens are - * evaluated, so that it is inserted at the exact scope where we would expect it. + * Code generation can involve keeping track of scopes in the code (e.g. liveness and availability of + * {@link DataName}s) and of the hashtag replacements in the templates. The {@link ScopeToken} serves + * this purpose, and allows the definition of transparent scopes (e.g. {@link #transparentScope}) and + * non-transparent scopes (e.g. {@link #scope}). + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Scopes and (non-)transparency
hashtag {@link DataName} and {@link StructuralName} {@link #setFuelCost}
{@link #scope} non-transparent non-transparent non-transparent
{@link #hashtagScope} non-transparent transparent transparent
{@link #nameScope} transparent non-transparent transparent
{@link #setFuelCostScope} transparent transparent non-transparent
{@link #transparentScope} transparent transparent transparent
* *

- * Let us look at the following example to better understand the execution order. + * In some cases, we may be deeper nested in templates and scopes, and would like to reach "back" or + * to outer scopes. This is possible with {@link Hook#anchor}ing in some outer scope, and later + * {@link Hook#insert}ing from an inner scope to the scope of the anchoring. For example, while + * generating code in a method, one can reach out to the scope of the class, and insert a new field, + * or define a utility method. * *

- * {@snippet lang=java : - * var testTemplate = Template.make(() -> body( - * // The lambda has just been invoked. - * // We count the DataNames and assign the count to the hashtag replacement "c1". - * let("c1", dataNames(MUTABLE).exactOf(someType).count()), - * // We want to define a DataName "v1", and create a token for it. - * addDataName($("v1"), someType, MUTABLE), - * // We count the DataNames again, but the count does NOT change compared to "c1". - * // This is because the token for "v1" is only evaluated later. - * let("c2", dataNames(MUTABLE).exactOf(someType).count()), - * // Create a nested scope. - * METHOD_HOOK.anchor( - * // We want to define a DataName "v2", which is only valid inside this - * // nested scope. - * addDataName($("v2"), someType, MUTABLE), - * // The count is still not different to "c1". - * let("c3", dataNames(MUTABLE).exactOf(someType).count()), - * // We nest a Template. This creates a TemplateToken, which is later evaluated. - * // By the time the TemplateToken is evaluated, the tokens from above will - * // be already evaluated. Hence, "v1" and "v2" are added by then, and if the - * // "otherTemplate" were to count the DataNames, the count would be increased - * // by 2 compared to "c1". - * otherTemplate.asToken() - * ), - * // After closing the scope, "v2" is no longer available. - * // The count is still the same as "c1", since "v1" is still only a token. - * let("c4", dataNames(MUTABLE).exactOf(someType).count()), - * // We nest another Template. Again, this creates a TemplateToken, which is only - * // evaluated later. By that time, the token for "v1" is evaluated, and so the - * // nested Template would observe an increment in the count. - * anotherTemplate.asToken() - * // By this point, all methods are called, and the tokens generated. - * // The lambda returns the "body", which is all of the tokens that we just - * // generated. After returning from the lambda, the tokens will be evaluated - * // one by one. - * )); - * } - + * A {@link TemplateBinding} allows the recursive use of Templates. With the indirection of such a binding, + * a Template can reference itself. + * + *

+ * The writer of recursive {@link Template}s must ensure that this recursion terminates. To unify the + * approach across {@link Template}s, we introduce the concept of {@link #fuel}. Templates are rendered starting + * with a limited amount of {@link #fuel} (default: 100, see {@link #DEFAULT_FUEL}), which is decreased at each + * Template nesting by a certain amount (default: 10, see {@link #DEFAULT_FUEL_COST}). The default fuel for a + * template can be changed when we {@code render()} it (e.g. {@link ZeroArgs#render(float)}) and the default + * fuel cost with {@link #setFuelCost}) when defining the {@link #scope(Object...)}. Recursive templates are + * supposed to terminate once the {@link #fuel} is depleted (i.e. reaches zero). + * + *

+ * A note from the implementor to the user: We have decided to implement the Template Framework using + * a functional (lambdas) and data-oriented (tokens) model. The consequence is that there are three + * orders in template rendering: (1) the execution order in lambdas, where we usually assemble the + * tokens and pass them to some scope ({@link ScopeToken}) as arguments. (2) the token evaluation + * order, which occurs in the order of how tokens are listed in a scope. By design, the token order + * is the same order as execution in lambdas. To keep the lambda and token order in sync, most of the + * queries about the state of code generation, such as {@link DataName}s and {@link Hook}s cannot + * return the values immediately, but have to be expressed as tokens. If we had a mix of tokens and + * immediate queries, then the immediate queries would "float" by the tokens, because the immediate + * queries are executed during the lambda execution, but the tokens are only executed later. Having + * to express everything as tokens can be a little more cumbersome (e.g. sample requires a lambda + * that captures the {@link DataName}, and sample does not return the {@link DataName} directly). + * But this ensures that reasoning about execution order is relatively straight forward, namely in + * the order of the specified tokens. (3) the final code order is the same as the lambda and token + * order, except when using {@link Hook#insert}, which places the code at the innermost {@link Hook#anchor}. + * *

* More examples for these functionalities can be found in {@code TestTutorial.java}, {@code TestSimple.java}, * and {@code TestAdvanced.java}, which all produce compilable Java code. Additional examples can be found in @@ -281,10 +276,10 @@ public sealed interface Template permits Template.ZeroArgs, /** * A {@link Template} with no arguments. * - * @param function The {@link Supplier} that creates the {@link TemplateBody}. + * @param function The {@link Supplier} that creates the {@link ScopeToken}. */ - record ZeroArgs(Supplier function) implements Template { - TemplateBody instantiate() { + record ZeroArgs(Supplier function) implements Template { + ScopeToken instantiate() { return function.get(); } @@ -324,10 +319,10 @@ public sealed interface Template permits Template.ZeroArgs, * * @param arg1Name The name of the (first) argument, used for hashtag replacements in the {@link Template}. * @param The type of the (first) argument. - * @param function The {@link Function} that creates the {@link TemplateBody} given the template argument. + * @param function The {@link Function} that creates the {@link ScopeToken} given the template argument. */ - record OneArg(String arg1Name, Function function) implements Template { - TemplateBody instantiate(T1 arg1) { + record OneArg(String arg1Name, Function function) implements Template { + ScopeToken instantiate(T1 arg1) { return function.apply(arg1); } @@ -372,10 +367,10 @@ public sealed interface Template permits Template.ZeroArgs, * @param arg2Name The name of the second argument, used for hashtag replacements in the {@link Template}. * @param The type of the first argument. * @param The type of the second argument. - * @param function The {@link BiFunction} that creates the {@link TemplateBody} given the template arguments. + * @param function The {@link BiFunction} that creates the {@link ScopeToken} given the template arguments. */ - record TwoArgs(String arg1Name, String arg2Name, BiFunction function) implements Template { - TemplateBody instantiate(T1 arg1, T2 arg2) { + record TwoArgs(String arg1Name, String arg2Name, BiFunction function) implements Template { + ScopeToken instantiate(T1 arg1, T2 arg2) { return function.apply(arg1, arg2); } @@ -447,10 +442,10 @@ public sealed interface Template permits Template.ZeroArgs, * @param The type of the first argument. * @param The type of the second argument. * @param The type of the third argument. - * @param function The function with three arguments that creates the {@link TemplateBody} given the template arguments. + * @param function The function with three arguments that creates the {@link ScopeToken} given the template arguments. */ - record ThreeArgs(String arg1Name, String arg2Name, String arg3Name, TriFunction function) implements Template { - TemplateBody instantiate(T1 arg1, T2 arg2, T3 arg3) { + record ThreeArgs(String arg1Name, String arg2Name, String arg3Name, TriFunction function) implements Template { + ScopeToken instantiate(T1 arg1, T2 arg2, T3 arg3) { return function.apply(arg1, arg2, arg3); } @@ -496,28 +491,28 @@ public sealed interface Template permits Template.ZeroArgs, /** * Creates a {@link Template} with no arguments. - * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * See {@link #scope} for more details about how to construct a Template with {@link Token}s. * *

* Example: * {@snippet lang=java : - * var template = Template.make(() -> body( + * var template = Template.make(() -> scope( * """ * Multi-line string or other tokens. * """ * )); * } * - * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param scope The {@link ScopeToken} created by {@link Template#scope}. * @return A {@link Template} with zero arguments. */ - static Template.ZeroArgs make(Supplier body) { - return new Template.ZeroArgs(body); + static Template.ZeroArgs make(Supplier scope) { + return new Template.ZeroArgs(scope); } /** * Creates a {@link Template} with one argument. - * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * See {@link #scope} for more details about how to construct a Template with {@link Token}s. * Good practice but not enforced but not enforced: {@code arg1Name} should match the lambda argument name. * *

@@ -525,7 +520,7 @@ public sealed interface Template permits Template.ZeroArgs, * for use in hashtag replacements, and captured once as lambda argument with the corresponding type * of the generic argument. * {@snippet lang=java : - * var template = Template.make("a", (Integer a) -> body( + * var template = Template.make("a", (Integer a) -> scope( * """ * Multi-line string or other tokens. * We can use the hashtag replacement #a to directly insert the String value of a. @@ -534,18 +529,18 @@ public sealed interface Template permits Template.ZeroArgs, * )); * } * - * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param scope The {@link ScopeToken} created by {@link Template#scope}. * @param Type of the (first) argument. * @param arg1Name The name of the (first) argument for hashtag replacement. * @return A {@link Template} with one argument. */ - static Template.OneArg make(String arg1Name, Function body) { - return new Template.OneArg<>(arg1Name, body); + static Template.OneArg make(String arg1Name, Function scope) { + return new Template.OneArg<>(arg1Name, scope); } /** * Creates a {@link Template} with two arguments. - * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * See {@link #scope} for more details about how to construct a Template with {@link Token}s. * Good practice but not enforced: {@code arg1Name} and {@code arg2Name} should match the lambda argument names. * *

@@ -553,7 +548,7 @@ public sealed interface Template permits Template.ZeroArgs, * for use in hashtag replacements, and captured once as lambda arguments with the corresponding types * of the generic arguments. * {@snippet lang=java : - * var template = Template.make("a", "b", (Integer a, String b) -> body( + * var template = Template.make("a", "b", (Integer a, String b) -> scope( * """ * Multi-line string or other tokens. * We can use the hashtag replacement #a and #b to directly insert the String value of a and b. @@ -562,23 +557,23 @@ public sealed interface Template permits Template.ZeroArgs, * )); * } * - * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param scope The {@link ScopeToken} created by {@link Template#scope}. * @param Type of the first argument. * @param arg1Name The name of the first argument for hashtag replacement. * @param Type of the second argument. * @param arg2Name The name of the second argument for hashtag replacement. * @return A {@link Template} with two arguments. */ - static Template.TwoArgs make(String arg1Name, String arg2Name, BiFunction body) { - return new Template.TwoArgs<>(arg1Name, arg2Name, body); + static Template.TwoArgs make(String arg1Name, String arg2Name, BiFunction scope) { + return new Template.TwoArgs<>(arg1Name, arg2Name, scope); } /** * Creates a {@link Template} with three arguments. - * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * See {@link #scope} for more details about how to construct a Template with {@link Token}s. * Good practice but not enforced: {@code arg1Name}, {@code arg2Name}, and {@code arg3Name} should match the lambda argument names. * - * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param scope The {@link ScopeToken} created by {@link Template#scope}. * @param Type of the first argument. * @param arg1Name The name of the first argument for hashtag replacement. * @param Type of the second argument. @@ -587,18 +582,35 @@ public sealed interface Template permits Template.ZeroArgs, * @param arg3Name The name of the third argument for hashtag replacement. * @return A {@link Template} with three arguments. */ - static Template.ThreeArgs make(String arg1Name, String arg2Name, String arg3Name, Template.TriFunction body) { - return new Template.ThreeArgs<>(arg1Name, arg2Name, arg3Name, body); + static Template.ThreeArgs make(String arg1Name, String arg2Name, String arg3Name, Template.TriFunction scope) { + return new Template.ThreeArgs<>(arg1Name, arg2Name, arg3Name, scope); } /** - * Creates a {@link TemplateBody} from a list of tokens, which can be {@link String}s, - * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), any {@link Token}, - * or {@link List}s of any of these. + * Creates a {@link ScopeToken} that represents a scope that is completely + * non-transparent, not allowing anything to escape. This + * means that no {@link DataName}, {@link StructuralName}s, hashtag-replacement + * or {@link #setFuelCost} defined inside the scope is available outside. All + * these usages are only local to the defining scope here. + * + *

+ * The scope is formed from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), + * any {@link Token}, or {@link List}s of any of these. + * + *

+ * If you require a scope that is either fully transparent (i.e. everything escapes) + * or only restricts a specific kind to not escape, consider using one of the other + * provided scopes: {@link #transparentScope}, {@link #nameScope}, {@link #hashtagScope}, + * or {@link #setFuelCostScope}. A "scope-transparency-matrix" can also be found in + * the interface comment for {@link Template}. + * + *

+ * The most common use of {@link #scope} is in the construction of templates: * *

* {@snippet lang=java : - * var template = Template.make(() -> body( + * var template = Template.make(() -> scope( * """ * Multi-line string * """, @@ -608,14 +620,200 @@ public sealed interface Template permits Template.ZeroArgs, * )); * } * + *

+ * Note that regardless of the chosen scope for {@code Template.make}, + * hashtag-replacements and {@link #setFuelCost} are always implicitly + * non-transparent (i.e. non-escaping). For example, {@link #let} will + * not escape the template scope even when using {@link #transparentScope}. + * As a default, it is recommended to use {@link #scope} for + * {@code Template.make} since in most cases template scopes align with + * code scopes that are non-transparent for fields, variables, etc. In + * rare cases, where the scope of the template needs to be transparent + * (e.g. because we need to insert a variable or field into an outer scope), + * it is recommended to use {@link #transparentScope}. This allows to make + * {@link DataName}s and {@link StructuralName}s available outside this + * template crossing the template boundary. + * + *

+ * We can also use nested scopes inside of templates: + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * // CODE1: some code in the outer scope + * scope( + * // CODE2: some code in the inner scope. Names, hashtags and setFuelCost + * // do not escape the inner scope. + * ), + * // CODE3: more code in the outer scope, names and hashtags from CODE2 are + * // not available anymore because of the non-transparent "scope". + * transparentScope( + * // CODE4: some code in the inner "transparentScope". Names, hashtags and setFuelCost + * // escape the "transparentScope" and are still available after the "transparentScope" + * // closes. + * ) + * // CODE5: we still have access to names and hashtags from CODE4. + * )); + * } + * * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types * (for example {@link Integer}), any {@link Token}, or {@link List}s * of any of these. - * @return The {@link TemplateBody} which captures the list of validated {@link Token}s. + * @return The {@link ScopeToken} which captures the list of validated {@link Token}s. * @throws IllegalArgumentException if the list of tokens contains an unexpected object. */ - static TemplateBody body(Object... tokens) { - return new TemplateBody(TokenParser.parse(tokens)); + static ScopeToken scope(Object... tokens) { + return new ScopeTokenImpl(TokenParser.parse(tokens), false, false, false); + } + + /** + * Creates a {@link ScopeToken} that represents a completely transparent scope. + * This means that {@link DataName}s, {@link StructuralName}s, + * hashtag-replacements and {@link #setFuelCost} declared inside the scope will be available + * in the outer scope. + * The scope is formed from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), + * any {@link Token}, or {@link List}s of any of these. + * + *

+ * If you require a scope that is non-transparent (i.e. nothing escapes) or only restricts + * a specific kind to not escape, consider using one of the other provided scopes: + * {@link #scope}, {@link #nameScope}, {@link #hashtagScope}, or {@link #setFuelCostScope}. + * A "scope-transparency-matrix" can also be found in the interface comment for {@link Template}. + * + * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types + * (for example {@link Integer}), any {@link Token}, or {@link List}s + * of any of these. + * @return The {@link ScopeToken} which captures the list of validated {@link Token}s. + * @throws IllegalArgumentException if the list of tokens contains an unexpected object. + */ + static ScopeToken transparentScope(Object... tokens) { + return new ScopeTokenImpl(TokenParser.parse(tokens), true, true, true); + } + + /** + * Creates a {@link ScopeToken} that represents a scope that is non-transparent for + * {@link DataName}s and {@link StructuralName}s (i.e. cannot escape), but + * transparent for hashtag-replacements and {@link #setFuelCost} (i.e. available + * in outer scope). + * + *

+ * The scope is formed from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), + * any {@link Token}, or {@link List}s of any of these. + * + *

+ * If you require a scope that is transparent or uses a different restriction, consider + * using one of the other provided scopes: {@link #scope}, {@link #transparentScope}, + * {@link #hashtagScope}, or {@link #setFuelCostScope}. A "scope-transparency-matrix" can + * also be found in the interface comment for {@link Template}. + * + * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types + * (for example {@link Integer}), any {@link Token}, or {@link List}s + * of any of these. + * @return The {@link ScopeToken} which captures the list of validated {@link Token}s. + * @throws IllegalArgumentException if the list of tokens contains an unexpected object. + */ + static ScopeToken nameScope(Object... tokens) { + return new ScopeTokenImpl(TokenParser.parse(tokens), false, true, true); + } + + /** + * Creates a {@link ScopeToken} that represents a scope that is non-transparent for + * hashtag-replacements (i.e. cannot escape), but transparent for {@link DataName}s + * and {@link StructuralName}s and {@link #setFuelCost} (i.e. available in outer scope). + * + *

+ * The scope is formed from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), + * any {@link Token}, or {@link List}s of any of these. + * + *

+ * If you require a scope that is transparent or uses a different restriction, consider + * using one of the other provided scopes: {@link #scope}, {@link #transparentScope}, + * {@link #nameScope}, or {@link #setFuelCostScope}. A "scope-transparency-matrix" can + * also be found in the interface comment for {@link Template}. + * + *

+ * Keeping hashtag-replacements local but letting {@link DataName}s escape can be + * useful in cases like the following, where we may want to reuse the hashtag + * multiple times: + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * List.of("a", "b", "c").stream().map(name -> hashtagScope( + * let("name", name), // assumes values: a, b, c + * addDataName(name, PrimitiveType.INTS, MUTABLE), // escapes + * """ + * int #name = 42; + * """ + * )) + * // We still have access to the three DataNames. + * )); + * } + * + * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types + * (for example {@link Integer}), any {@link Token}, or {@link List}s + * of any of these. + * @return The {@link ScopeToken} which captures the list of validated {@link Token}s. + * @throws IllegalArgumentException if the list of tokens contains an unexpected object. + */ + static ScopeToken hashtagScope(Object... tokens) { + return new ScopeTokenImpl(TokenParser.parse(tokens), true, false, true); + } + + /** + * Creates a {@link ScopeToken} that represents a scope that is non-transparent for + * {@link #setFuelCost} (i.e. cannot escape), but transparent for hashtag-replacements, + * {@link DataName}s and {@link StructuralName}s (i.e. available in outer scope). + * The scope is formed from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), + * any {@link Token}, or {@link List}s of any of these. + * + *

+ * If you require a scope that is transparent or uses a different restriction, consider + * using one of the other provided scopes: {@link #scope}, {@link #transparentScope}, + * {@link #hashtagScope}, or {@link #nameScope}. A "scope-transparency-matrix" can + * also be found in the interface comment for {@link Template}. + * + *

+ * In some cases, it can be helpful to have different {@link #setFuelCost} within + * a single template, depending on the code nesting depth. Example: + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> scope( + * setFuelCost(1), + * // CODE1: some shallow code, allowing recursive template uses here + * // to use more fuel. + * """ + * for (int i = 0; i < 1000; i++) { + * """, + * setFuelCostScope( + * setFuelCost(100) + * // CODE2: with the for-loop, we already have a deeper nesting + * // depth, and recursive template uses should not get + * // as much fuel as in CODE1. + * ), + * """ + * } + * """ + * // CODE3: we are back in the outer scope of CODE1, and can use + * // more fuel again in nested template uses. setFuelCost + * // is automatically restored to what was set before the + * // inner scope. + * )); + * } + * + * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types + * (for example {@link Integer}), any {@link Token}, or {@link List}s + * of any of these. + * @return The {@link ScopeToken} which captures the list of validated {@link Token}s. + * @throws IllegalArgumentException if the list of tokens contains an unexpected object. + */ + static ScopeToken setFuelCostScope(Object... tokens) { + return new ScopeTokenImpl(TokenParser.parse(tokens), true, true, false); } /** @@ -628,7 +826,7 @@ public sealed interface Template permits Template.ZeroArgs, * with an implicit dollar replacement, and then captures that dollar replacement * using {@link #$} for the use inside a nested template. * {@snippet lang=java : - * var template = Template.make(() -> body( + * var template = Template.make(() -> scope( * """ * int $var = 42; * """, @@ -640,6 +838,9 @@ public sealed interface Template permits Template.ZeroArgs, * @return The dollar replacement for the {@code 'name'}. */ static String $(String name) { + // Note, since the dollar replacements do not change within a template + // and the retrieval has no side effects, we can return the value immediately, + // and do not need a token. return Renderer.getCurrent().$(name); } @@ -648,7 +849,7 @@ public sealed interface Template permits Template.ZeroArgs, * *

* {@snippet lang=java : - * var template = Template.make("a", (Integer a) -> body( + * var template = Template.make("a", (Integer a) -> scope( * let("b", a * 5), * """ * System.out.println("Use a and b with hashtag replacement: #a and #b"); @@ -656,41 +857,50 @@ public sealed interface Template permits Template.ZeroArgs, * )); * } * + *

+ * Note that a {@code let} definition makes the hashtag replacement available + * for anything that follows it, until the the end of the next outer scope + * that is non-transparent for hashtag replacements. Additionally, hashtag + * replacements are limited to the template they were defined in. + * If you want to pass values from an outer to an inner template, this cannot + * be done with hashtags directly. Instead, one has to pass the values via + * template arguments. + * * @param key Name for the hashtag replacement. * @param value The value that the hashtag is replaced with. - * @return A token that does nothing, so that the {@link #let} can easily be put in a list of tokens - * inside a {@link Template#body}. - * @throws RendererException if there is a duplicate hashtag {@code key}. + * @return A token that represents the hashtag replacement definition. */ static Token let(String key, Object value) { - Renderer.getCurrent().addHashtagReplacement(key, value); - return new NothingToken(); + return new LetToken(key, value, v -> transparentScope()); } /** * Define a hashtag replacement for {@code "#key"}, with a specific value, which is also captured - * by the provided {@code function} with type {@code }. + * by the provided {@code function} with type {@code }. While the argument of the lambda that + * captures the value is naturally bounded to the scope of the lambda, the hashtag replacement + * may be bound to the scope or escape it, depending on the choice of scope, see {@link #scope} + * and {@link #transparentScope}. * *

* {@snippet lang=java : - * var template = Template.make("a", (Integer a) -> let("b", a * 2, (Integer b) -> body( - * """ - * System.out.println("Use a and b with hashtag replacement: #a and #b"); - * """, - * "System.out.println(\"Use a and b as capture variables:\"" + a + " and " + b + ");\n" - * ))); + * var template = Template.make("a", (Integer a) -> scope( + * let("b", a * 2, (Integer b) -> scope( + * """ + * System.out.println("Use a and b with hashtag replacement: #a and #b"); + * """, + * "System.out.println(\"Use a and b as capture variables:\"" + a + " and " + b + ");\n" + * )) + * )); * } * * @param key Name for the hashtag replacement. * @param value The value that the hashtag is replaced with. * @param The type of the value. * @param function The function that is applied with the provided {@code value}. - * @return A {@link TemplateBody}. - * @throws RendererException if there is a duplicate hashtag {@code key}. + * @return A {@link Token} representing the hashtag replacement definition and inner scope. */ - static TemplateBody let(String key, T value, Function function) { - Renderer.getCurrent().addHashtagReplacement(key, value); - return function.apply(value); + static Token let(String key, T value, Function function) { + return new LetToken(key, value, function); } /** @@ -702,7 +912,7 @@ public sealed interface Template permits Template.ZeroArgs, /** * The default amount of fuel spent per Template. It is subtracted from the current {@link #fuel} at every * nesting level, and once the {@link #fuel} reaches zero, the nesting is supposed to terminate. Can be changed - * with {@link #setFuelCost(float)} inside {@link #body(Object...)}. + * with {@link #setFuelCost(float)} inside {@link #scope(Object...)}. */ float DEFAULT_FUEL_COST = 10.0f; @@ -721,7 +931,7 @@ public sealed interface Template permits Template.ZeroArgs, *

* {@snippet lang=java : * var binding = new TemplateBinding>(); - * var template = Template.make("depth", (Integer depth) -> body( + * var template = Template.make("depth", (Integer depth) -> scope( * setFuelCost(5.0f), * let("fuel", fuel()), * """ @@ -737,6 +947,9 @@ public sealed interface Template permits Template.ZeroArgs, * @return The amount of fuel left for nested Template use. */ static float fuel() { + // Note, since the fuel amount does not change within a template + // and the retrieval has no side effects, we can return the value immediately, + // and do not need a token. return Renderer.getCurrent().fuel(); } @@ -745,16 +958,17 @@ public sealed interface Template permits Template.ZeroArgs, * {@link Template#DEFAULT_FUEL_COST}. * * @param fuelCost The amount of fuel used for the current Template. - * @return A token for convenient use in {@link Template#body}. + * @return A token for convenient use in {@link Template#scope}. */ static Token setFuelCost(float fuelCost) { - Renderer.getCurrent().setFuelCost(fuelCost); - return new NothingToken(); + return new SetFuelCostToken(fuelCost); } /** - * Add a {@link DataName} in the current scope, that is the innermost of either - * {@link Template#body} or {@link Hook#anchor}. + * Add a {@link DataName} in the current {@link #scope}. + * If the current scope is transparent to {@link DataName}s, it escapes to the next + * outer scope that is non-transparent, and is available for everything that follows + * the {@code addDataName} until the end of that non-transparent scope. * * @param name The name of the {@link DataName}, i.e. the {@link String} used in code. * @param type The type of the {@link DataName}. @@ -779,8 +993,10 @@ public sealed interface Template permits Template.ZeroArgs, } /** - * Add a {@link DataName} in the current scope, that is the innermost of either - * {@link Template#body} or {@link Hook#anchor}, with a {@code weight} of 1. + * Add a {@link DataName} in the current {@link #scope}, with a {@code weight} of 1. + * If the current scope is transparent to {@link DataName}s, it escapes to the next + * outer scope that is non-transparent, and is available for everything that follows + * the {@code addDataName} until the end of that non-transparent scope. * * @param name The name of the {@link DataName}, i.e. the {@link String} used in code. * @param type The type of the {@link DataName}. @@ -804,8 +1020,10 @@ public sealed interface Template permits Template.ZeroArgs, } /** - * Add a {@link StructuralName} in the current scope, that is the innermost of either - * {@link Template#body} or {@link Hook#anchor}. + * Add a {@link StructuralName} in the current {@link #scope}. + * If the current scope is transparent to {@link StructuralName}s, it escapes to the next + * outer scope that is non-transparent, and is available for everything that follows + * the {@code addStructuralName} until the end of that non-transparent scope. * * @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code. * @param type The type of the {@link StructuralName}. @@ -822,8 +1040,10 @@ public sealed interface Template permits Template.ZeroArgs, } /** - * Add a {@link StructuralName} in the current scope, that is the innermost of either - * {@link Template#body} or {@link Hook#anchor}, with a {@code weight} of 1. + * Add a {@link StructuralName} in the current {@link #scope}, with a {@code weight} of 1. + * If the current scope is transparent to {@link StructuralName}s, it escapes to the next + * outer scope that is non-transparent, and is available for everything that follows + * the {@code addStructuralName} until the end of that non-transparent scope. * * @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code. * @param type The type of the {@link StructuralName}. diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java index cf8c4afb321..04305dff02f 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java @@ -27,38 +27,96 @@ import java.util.HashMap; import java.util.Map; /** - * The {@link TemplateFrame} is the frame for a {@link Template}, i.e. the corresponding - * {@link TemplateToken}. It ensures that each template use has its own unique {@link #id} - * used to deconflict names using {@link Template#$}. It also has a set of hashtag - * replacements, which combine the key-value pairs from the template argument and the - * {@link Template#let} definitions. The {@link #parent} relationship provides a trace - * for the use chain of templates. The {@link #fuel} is reduced over this chain, to give - * a heuristic on how much time is spent on the code from the template corresponding to - * the frame, and to give a termination criterion to avoid nesting templates too deeply. + * The {@link TemplateFrame} keeps track of the nested hashtag replacements available + * inside the {@link Template}, as well as the unique id of the {@link Template} use, + * and how much fuel is available for recursive {@link Template} calls. The name of + * the {@link TemplateFrame} indicates that it corresponds to the structure of the + * {@link Template}, whereas the {@link CodeFrame} corresponds to the structure of + * the generated code. * *

- * See also {@link CodeFrame} for more explanations about the frames. + * The unique id is used to deconflict names using {@link Template#$}. + * + *

+ * A {@link Template} can have multiple {@link TemplateFrame}s, if there are nested + * scopes. The outermost {@link TemplateFrame} determines the id of the {@link Template} + * use and performs the subtraction of fuel from the outer {@link Template}. Inner + * {@link TemplateFrame}s ensure the correct availability of hashtag replacement and + * {@link Template#setFuelCost} definitions, so that they are local to their scope and + * nested scopes, and only escape if the scope is transparent. + * + *

+ * The hashtag replacements are a set of key-value pairs from the template arguments + * and queries such as {@link Template#let} definitions. Each {@link TemplateFrame} + * has such a set of hashtag replacements, and implicitly provides access to the + * hashtag replacements of the outer {@link TemplateFrame}s, up to the outermost + * of the current {@link Template}. If a hashtag replacement is added in a scope, + * we have to traverse to outer scopes until we find one that is not transparent + * for hashtags (at most it is the frame of the Template), and insert it there. + * The hashtag replacent is local to that frame, and accessible for any frames nested + * inside it, but not inside other Templates. The hashtag replacement disappears once + * the corresponding scope is exited, i.e. the frame removed. + * + *

+ * The {@link #parent} relationship provides a trace for the use chain of templates and + * their inner scopes. The {@link #fuel} is reduced over this chain to give a heuristic + * on how deeply nested the code is at a given point, correlating to the runtime that + * would be spent if the code was executed. The idea is that once the fuel is depleated, + * we do not want to nest more deeply, so that there is a reasonable chance that the + * execution of the generated code can terminate. + * + *

+ * The {@link TemplateFrame} thus implements the hashtag and {@link Template#setFuelCost} + * non-transparency aspect of {@link ScopeToken}. + * + *

+ * See also {@link CodeFrame} for more explanations about the frames. Note, that while + * {@link TemplateFrame} always nests inward, even with {@link Hook#insert}, the + * {@link CodeFrame} can also jump to the {@link Hook#anchor} {@link CodeFrame} when + * using {@link Hook#insert}. */ class TemplateFrame { final TemplateFrame parent; + private final boolean isInnerScope; private final int id; private final Map hashtagReplacements = new HashMap<>(); final float fuel; private float fuelCost; + private final boolean isTransparentForHashtag; + private final boolean isTransparentForFuel; public static TemplateFrame makeBase(int id, float fuel) { - return new TemplateFrame(null, id, fuel, 0.0f); + return new TemplateFrame(null, false, id, fuel, 0.0f, false, false); } public static TemplateFrame make(TemplateFrame parent, int id) { - return new TemplateFrame(parent, id, parent.fuel - parent.fuelCost, Template.DEFAULT_FUEL_COST); + float fuel = parent.fuel - parent.fuelCost; + return new TemplateFrame(parent, false, id, fuel, Template.DEFAULT_FUEL_COST, false, false); } - private TemplateFrame(TemplateFrame parent, int id, float fuel, float fuelCost) { + public static TemplateFrame makeInnerScope(TemplateFrame parent, + boolean isTransparentForHashtag, + boolean isTransparentForFuel) { + // We keep the id of the parent, so that we have the same dollar replacements. + // And we subtract no fuel, but forward the cost. + return new TemplateFrame(parent, true, parent.id, parent.fuel, parent.fuelCost, + isTransparentForHashtag, isTransparentForFuel); + } + + private TemplateFrame(TemplateFrame parent, + boolean isInnerScope, + int id, + float fuel, + float fuelCost, + boolean isTransparentForHashtag, + boolean isTransparentForFuel) { this.parent = parent; + this.isInnerScope = isInnerScope; this.id = id; this.fuel = fuel; this.fuelCost = fuelCost; + this.isTransparentForHashtag = isTransparentForHashtag; + this.isTransparentForFuel = isTransparentForFuel; } public String $(String name) { @@ -78,8 +136,15 @@ class TemplateFrame { if (!Renderer.isValidHashtagOrDollarName(key)) { throw new RendererException("Is not a valid hashtag replacement name: '" + key + "'."); } - if (hashtagReplacements.putIfAbsent(key, value) != null) { - throw new RendererException("Duplicate hashtag replacement for #" + key); + String previous = findHashtagReplacementInScopes(key); + if (previous != null) { + throw new RendererException("Duplicate hashtag replacement for #" + key + ". " + + "previous: " + previous + ", new: " + value); + } + if (isTransparentForHashtag) { + parent.addHashtagReplacement(key, value); + } else { + hashtagReplacements.put(key, value); } } @@ -87,13 +152,27 @@ class TemplateFrame { if (!Renderer.isValidHashtagOrDollarName(key)) { throw new RendererException("Is not a valid hashtag replacement name: '" + key + "'."); } - if (hashtagReplacements.containsKey(key)) { - return hashtagReplacements.get(key); + String value = findHashtagReplacementInScopes(key); + if (value != null) { + return value; } throw new RendererException("Missing hashtag replacement for #" + key); } + private String findHashtagReplacementInScopes(String key) { + if (hashtagReplacements.containsKey(key)) { + return hashtagReplacements.get(key); + } + if (!isInnerScope) { + return null; + } + return parent.findHashtagReplacementInScopes(key); + } + void setFuelCost(float fuelCost) { this.fuelCost = fuelCost; + if (isTransparentForFuel) { + parent.setFuelCost(fuelCost); + } } } diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java index 47262f152d4..ffbfcfdf2d0 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java @@ -49,7 +49,7 @@ public sealed abstract class TemplateToken implements Token } @Override - public TemplateBody instantiate() { + public ScopeToken instantiate() { return zeroArgs.instantiate(); } @@ -74,7 +74,7 @@ public sealed abstract class TemplateToken implements Token } @Override - public TemplateBody instantiate() { + public ScopeToken instantiate() { return oneArgs.instantiate(arg1); } @@ -104,7 +104,7 @@ public sealed abstract class TemplateToken implements Token } @Override - public TemplateBody instantiate() { + public ScopeToken instantiate() { return twoArgs.instantiate(arg1, arg2); } @@ -138,7 +138,7 @@ public sealed abstract class TemplateToken implements Token } @Override - public TemplateBody instantiate() { + public ScopeToken instantiate() { return threeArgs.instantiate(arg1, arg2, arg3); } @@ -150,7 +150,7 @@ public sealed abstract class TemplateToken implements Token } } - abstract TemplateBody instantiate(); + abstract ScopeToken instantiate(); @FunctionalInterface interface ArgumentVisitor { diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Token.java b/test/hotspot/jtreg/compiler/lib/template_framework/Token.java index 0e9f9b272c5..6e9d5f7650a 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Token.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Token.java @@ -24,16 +24,25 @@ package compiler.lib.template_framework; /** - * The {@link Template#body} and {@link Hook#anchor} are given a list of tokens, which are either + * The {@link Template#scope} and {@link Hook#anchor} are given a list of tokens, which are either * {@link Token}s or {@link String}s or some permitted boxed primitives. */ public sealed interface Token permits StringToken, - TemplateToken, - TemplateToken.ZeroArgs, - TemplateToken.OneArg, - TemplateToken.TwoArgs, - TemplateToken.ThreeArgs, - HookAnchorToken, - HookInsertToken, - AddNameToken, - NothingToken {} + TemplateToken, + TemplateToken.ZeroArgs, + TemplateToken.OneArg, + TemplateToken.TwoArgs, + TemplateToken.ThreeArgs, + HookAnchorToken, + HookInsertToken, + HookIsAnchoredToken, + AddNameToken, + NameSampleToken, + NameForEachToken, + NamesToListToken, + NameCountToken, + NameHasAnyToken, + LetToken, + ScopeToken, + ScopeTokenImpl, + SetFuelCostToken {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TokenParser.java b/test/hotspot/jtreg/compiler/lib/template_framework/TokenParser.java index 0c335bd4fb8..bee6246bdc5 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/TokenParser.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TokenParser.java @@ -31,7 +31,7 @@ import java.util.List; * Helper class for {@link Token}, to keep the parsing methods package private. * *

- * The {@link Template#body} and {@link Hook#anchor} are given a list of tokens, which are either + * The {@link Template#scope} and {@link Hook#anchor} are given a list of tokens, which are either * {@link Token}s or {@link String}s or some permitted boxed primitives. These are then parsed * and all non-{@link Token}s are converted to {@link StringToken}s. The parsing also flattens * {@link List}s. diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java index 360937c8f7f..43ab16af415 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java @@ -33,7 +33,7 @@ import java.util.stream.Collectors; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; /** * {@link Expression}s model Java expressions, that have a list of arguments with specified @@ -357,7 +357,7 @@ public class Expression { } tokens.add(strings.getLast()); - var template = Template.make(() -> body( + var template = Template.make(() -> scope( tokens )); return template.asToken(); diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java index 46a9d5bbabe..c0db3d51545 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java @@ -33,7 +33,7 @@ import compiler.lib.generators.RestrictableGenerator; import compiler.lib.template_framework.DataName; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; /** * The {@link PrimitiveType} models Java's primitive types, and provides a set @@ -190,7 +190,7 @@ public final class PrimitiveType implements CodeGenerationDataNameType { * @return a TemplateToken that holds all the {@code LibraryRNG} class. */ public static TemplateToken generateLibraryRNG() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ public static class LibraryRNG { private static final Random RANDOM = Utils.getRandomInstance(); diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/TestFrameworkClass.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/TestFrameworkClass.java index 5194b75af43..a9db9285b78 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/library/TestFrameworkClass.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/TestFrameworkClass.java @@ -30,7 +30,7 @@ import compiler.lib.ir_framework.TestFramework; import compiler.lib.compile_framework.CompileFramework; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; /** @@ -51,7 +51,7 @@ public final class TestFrameworkClass { private TestFrameworkClass() {} /** - * This method renders a list of {@code testTemplateTokens} into the body of a class + * This method renders a list of {@code testTemplateTokens} into the scope of a class * and generates a {@code main} method which launches the {@link TestFramework} * to run the generated tests. * @@ -81,7 +81,7 @@ public final class TestFrameworkClass { final Set imports, final String classpath, final List testTemplateTokens) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("packageName", packageName), let("className", className), let("classpath", classpath), @@ -96,7 +96,7 @@ public final class TestFrameworkClass { public class #className { // --- CLASS_HOOK insertions start --- """, - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( """ // --- CLASS_HOOK insertions end --- public static void main(String[] vmFlags) { @@ -108,7 +108,7 @@ public final class TestFrameworkClass { // --- LIST OF TESTS start --- """, testTemplateTokens - ), + )), """ // --- LIST OF TESTS end --- } diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java index 62e474ecb2c..5d20ce659b9 100644 --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java @@ -61,7 +61,7 @@ import compiler.lib.compile_framework.*; import compiler.lib.generators.Generators; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.$; @@ -333,7 +333,7 @@ public class TestAliasingFuzzer { } public TemplateToken index(String invar0, String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("con", con), let("ivScale", ivScale), let("invar0Scale", invar0Scale), @@ -349,7 +349,7 @@ public class TestAliasingFuzzer { // MemorySegment need to be long-addressed, otherwise there can be int-overflow // in the index, and that prevents RangeCheck Elimination and Vectorization. public TemplateToken indexLong(String invar0, String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("con", con), let("ivScale", ivScale), let("invar0Scale", invar0Scale), @@ -365,7 +365,7 @@ public class TestAliasingFuzzer { // Mirror the IndexForm from the generator to the test. public static TemplateToken generateIndexForm() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ private static final Random RANDOM = Utils.getRandomInstance(); @@ -610,7 +610,7 @@ public class TestAliasingFuzzer { for (int i = 0; i < indexFormNames.length; i++) { indexFormNames[i] = $("index" + i); } - return body( + return scope( """ // --- $test start --- """, @@ -662,7 +662,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateArrayField(String name, MyType type) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("size", containerByteSize / type.byteSize()), let("name", name), let("type", type), @@ -676,7 +676,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateMemorySegmentField(String name, MyType type) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("size", containerByteSize / type.byteSize()), let("byteSize", containerByteSize), let("name", name), @@ -698,7 +698,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateIndexField(String name, IndexForm form) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("name", name), let("form", form.generate()), """ @@ -709,7 +709,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateTestFields(String[] invarRest, String[] containerNames, String[] indexFormNames) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("ivType", isLongIvType ? "long" : "int"), """ // invarRest fields: @@ -741,7 +741,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateContainerInitArray(String name) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("size", containerByteSize / containerElementType.byteSize()), let("name", name), """ @@ -753,7 +753,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateContainerInitMemorySegment(String name) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("size", containerByteSize / containerElementType.byteSize()), let("name", name), """ @@ -765,7 +765,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateContainerInit(String[] containerNames) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Init containers from original data: """, @@ -784,7 +784,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateContainerAliasingAssignment(int i, String name1, String name2, String iterations) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("i", i), let("name1", name1), let("name2", name2), @@ -798,7 +798,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateContainerAliasing(String[] containerNames, String iterations) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Container aliasing: """, @@ -832,7 +832,7 @@ public class TestAliasingFuzzer { if (accessIndexForm.length != 2) { throw new RuntimeException("not yet implemented"); } - var templateSplitRanges = Template.make(() -> body( + var templateSplitRanges = Template.make(() -> scope( let("size", size), """ int middle = RANDOM.nextInt(#size / 3, #size * 2 / 3); @@ -865,7 +865,7 @@ public class TestAliasingFuzzer { """ )); - var templateWholeRanges = Template.make(() -> body( + var templateWholeRanges = Template.make(() -> scope( let("size", size), """ var r0 = new IndexForm.Range(0, #size); @@ -873,7 +873,7 @@ public class TestAliasingFuzzer { """ )); - var templateRandomRanges = Template.make(() -> body( + var templateRandomRanges = Template.make(() -> scope( let("size", size), """ int lo0 = RANDOM.nextInt(0, #size * 3 / 4); @@ -883,7 +883,7 @@ public class TestAliasingFuzzer { """ )); - var templateSmallOverlapRanges = Template.make(() -> body( + var templateSmallOverlapRanges = Template.make(() -> scope( // Idea: same size ranges, with size "range". A small overlap, // so that bad runtime checks would create wrong results. let("size", size), @@ -907,7 +907,7 @@ public class TestAliasingFuzzer { // -> safe with rnd = size/10 )); - var templateAnyRanges = Template.make(() -> body( + var templateAnyRanges = Template.make(() -> scope( switch(RANDOM.nextInt(4)) { case 0 -> templateSplitRanges.asToken(); case 1 -> templateWholeRanges.asToken(); @@ -917,7 +917,7 @@ public class TestAliasingFuzzer { } )); - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Generate ranges: """, @@ -941,7 +941,7 @@ public class TestAliasingFuzzer { // We want there to be at least 1000 iterations. final int minIvRange = ivStrideAbs * 1000; - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("containerByteSize", containerByteSize), """ // Compute loop bounds and loop invariants. @@ -949,7 +949,7 @@ public class TestAliasingFuzzer { int ivHi = ivLo + #containerByteSize; """, IntStream.range(0, indexFormNames.length).mapToObj(i -> - Template.make(() -> body( + Template.make(() -> scope( let("i", i), let("form", indexFormNames[i]), """ @@ -990,7 +990,7 @@ public class TestAliasingFuzzer { """, IntStream.range(0, indexFormNames.length).mapToObj(i1 -> IntStream.range(0, i1).mapToObj(i2 -> - Template.make(() -> body( + Template.make(() -> scope( let("i1", i1), let("i2", i2), // i1 < i2 or i1 > i2 @@ -1021,7 +1021,7 @@ public class TestAliasingFuzzer { private TemplateToken generateCallMethod(String output, String methodName, String containerPrefix) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("output", output), let("methodName", methodName), "var #output = #methodName(", @@ -1034,7 +1034,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateIRRules() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( switch (containerKind) { case ContainerKind.ARRAY -> generateIRRulesArray(); @@ -1094,7 +1094,7 @@ public class TestAliasingFuzzer { // Regular array-accesses are vectorized quite predictably, and we can create nice // IR rules - even for cases where we do not expect vectorization. private TemplateToken generateIRRulesArray() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("T", containerElementType.letter()), switch (accessScenario) { case COPY_LOAD_STORE -> @@ -1145,7 +1145,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateIRRulesMemorySegmentAtIndex() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Unfortunately, there are some issues that prevent RangeCheck elimination. // The cases are currently quite unpredictable, so we cannot create any IR @@ -1158,7 +1158,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateIRRulesMemorySegmentLongAdrStride() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Unfortunately, there are some issues that prevent RangeCheck elimination. // The cases are currently quite unpredictable, so we cannot create any IR @@ -1169,7 +1169,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateIRRulesMemorySegmentLongAdrScale() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ // Unfortunately, there are some issues that prevent RangeCheck elimination. // The cases are currently quite unpredictable, so we cannot create any IR @@ -1180,7 +1180,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateTestMethod(String methodName, String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("methodName", methodName), let("containerElementType", containerElementType), let("ivStrideAbs", ivStrideAbs), @@ -1230,7 +1230,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateTestLoopIterationArray(String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("type", containerElementType), switch (accessScenario) { case COPY_LOAD_STORE -> @@ -1245,7 +1245,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateTestLoopIterationMemorySegmentAtIndex(String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("type0", accessType[0]), let("type1", accessType[1]), let("type0Layout", accessType[0].layout()), @@ -1265,7 +1265,7 @@ public class TestAliasingFuzzer { } private TemplateToken generateTestLoopIterationMemorySegmentLongAdr(String[] invarRest) { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( let("type0", accessType[0]), let("type1", accessType[1]), let("type0Layout", accessType[0].layout()), diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java index c5a4528f63d..784f1ded065 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java @@ -43,7 +43,7 @@ import compiler.lib.generators.RestrictableGenerator; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; /** @@ -96,7 +96,7 @@ public class TestAdvanced { // - The GOLD value is computed at the beginning, hopefully by the interpreter. // - The test method is eventually compiled, and the values are verified by the // check method. - var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> body( + var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> scope( let("con1", generator.next()), let("con2", generator.next()), """ @@ -116,7 +116,7 @@ public class TestAdvanced { )); // Template for the Class. - var classTemplate = Template.make("types", (List types) -> body( + var classTemplate = Template.make("types", (List types) -> scope( let("classpath", comp.getEscapedClassPathOfCompiledClasses()), """ package p.xyz; diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java index c21d2492fc7..6a0a2d3786a 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java @@ -40,7 +40,7 @@ import java.util.Set; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; import compiler.lib.template_framework.library.Expression; import compiler.lib.template_framework.library.Operations; @@ -78,7 +78,7 @@ public class TestExpressions { // precision results from some operators. We only compare the results if we know that the // result is deterministically the same. TemplateToken expressionToken = expression.asToken(expression.argumentTypes.stream().map(t -> t.con()).toList()); - return body( + return scope( let("returnType", expression.returnType), """ @Test diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java index a04a5771cb4..b1f5f74e682 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java @@ -41,7 +41,8 @@ import java.util.HashMap; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; +import static compiler.lib.template_framework.Template.transparentScope; import static compiler.lib.template_framework.Template.dataNames; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.$; @@ -77,7 +78,7 @@ public class TestPrimitiveTypes { Map tests = new HashMap<>(); // The boxing tests check if we can autobox with "boxedTypeName". - var boxingTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> body( + var boxingTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> scope( let("CON1", type.con()), let("CON2", type.con()), let("Boxed", type.boxedTypeName()), @@ -99,7 +100,7 @@ public class TestPrimitiveTypes { } // Integral and Float types have a size. Also test if "isFloating" is correct. - var integralFloatTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> body( + var integralFloatTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> scope( let("size", type.byteSize()), let("isFloating", type.isFloating()), """ @@ -129,27 +130,31 @@ public class TestPrimitiveTypes { // Finally, test the type by creating some DataNames (variables), and sampling // from them. There should be no cross-over between the types. - var variableTemplate = Template.make("type", (PrimitiveType type) -> body( + // IMPORTANT: since we are adding the DataName via an inserted Template, we + // must chose a "transparentScope", so that the DataName escapes. If we + // instead chose "scope", the test would fail, because it later + // finds no DataNames when we sample. + var variableTemplate = Template.make("type", (PrimitiveType type) -> transparentScope( let("CON", type.con()), - addDataName($("var"), type, MUTABLE), + addDataName($("var"), type, MUTABLE), // escapes the Template """ #type $var = #CON; """ )); - var sampleTemplate = Template.make("type", (PrimitiveType type) -> body( - let("var", dataNames(MUTABLE).exactOf(type).sample().name()), + var sampleTemplate = Template.make("type", (PrimitiveType type) -> scope( let("CON", type.con()), + dataNames(MUTABLE).exactOf(type).sampleAndLetAs("var"), """ #var = #CON; """ )); - var namesTemplate = Template.make(() -> body( + var namesTemplate = Template.make(() -> scope( """ public static void test_names() { """, - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( Collections.nCopies(10, CodeGenerationDataNameType.PRIMITIVE_TYPES.stream().map(type -> Hooks.METHOD_HOOK.insert(variableTemplate.asToken(type)) @@ -161,7 +166,7 @@ public class TestPrimitiveTypes { Collections.nCopies(10, CodeGenerationDataNameType.PRIMITIVE_TYPES.stream().map(sampleTemplate::asToken).toList() ) - ), + )), """ } """ @@ -172,7 +177,7 @@ public class TestPrimitiveTypes { // Test runtime random value generation with LibraryRNG // Runtime random number generation of a given primitive type can be very helpful // when writing tests that require random inputs. - var libraryRNGWithTypeTemplate = Template.make("type", (PrimitiveType type) -> body( + var libraryRNGWithTypeTemplate = Template.make("type", (PrimitiveType type) -> scope( """ { // Fill an array with 1_000 random values. Every type has at least 2 values, @@ -196,7 +201,7 @@ public class TestPrimitiveTypes { """ )); - var libraryRNGTemplate = Template.make(() -> body( + var libraryRNGTemplate = Template.make(() -> scope( // Make sure we instantiate the LibraryRNG class. PrimitiveType.generateLibraryRNG(), // Now we can use it inside the test. @@ -213,7 +218,7 @@ public class TestPrimitiveTypes { // Finally, put all the tests together in a class, and invoke all // tests from the main method. - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ package p.xyz; diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java index e06671ca951..c8afb34e423 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java @@ -34,7 +34,7 @@ package template_framework.examples; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; public class TestSimple { @@ -61,7 +61,7 @@ public class TestSimple { // Generate a source Java file as String public static String generate() { // Create a Template with two arguments. - var template = Template.make("arg1", "arg2", (Integer arg1, String arg2) -> body( + var template = Template.make("arg1", "arg2", (Integer arg1, String arg2) -> scope( """ package p.xyz; public class InnerTest { diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java index faa05b29d82..ed542180bad 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java @@ -43,7 +43,9 @@ import compiler.lib.template_framework.Hook; import compiler.lib.template_framework.TemplateBinding; import compiler.lib.template_framework.DataName; import compiler.lib.template_framework.StructuralName; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; +import static compiler.lib.template_framework.Template.transparentScope; +import static compiler.lib.template_framework.Template.hashtagScope; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.$; import static compiler.lib.template_framework.Template.fuel; @@ -68,13 +70,14 @@ public class TestTutorial { comp.addJavaSourceCode("p.xyz.InnerTest2", generateWithTemplateArguments()); comp.addJavaSourceCode("p.xyz.InnerTest3", generateWithHashtagAndDollarReplacements()); comp.addJavaSourceCode("p.xyz.InnerTest3b", generateWithHashtagAndDollarReplacements2()); + comp.addJavaSourceCode("p.xyz.InnerTest3c", generateWithHashtagAndDollarReplacements3()); comp.addJavaSourceCode("p.xyz.InnerTest4", generateWithCustomHooks()); comp.addJavaSourceCode("p.xyz.InnerTest5", generateWithLibraryHooks()); comp.addJavaSourceCode("p.xyz.InnerTest6", generateWithRecursionAndBindingsAndFuel()); comp.addJavaSourceCode("p.xyz.InnerTest7", generateWithDataNamesSimple()); comp.addJavaSourceCode("p.xyz.InnerTest8", generateWithDataNamesForFieldsAndVariables()); - comp.addJavaSourceCode("p.xyz.InnerTest9a", generateWithDataNamesAndScopes1()); - comp.addJavaSourceCode("p.xyz.InnerTest9b", generateWithDataNamesAndScopes2()); + comp.addJavaSourceCode("p.xyz.InnerTest9a", generateWithScopes1()); + comp.addJavaSourceCode("p.xyz.InnerTest9b", generateWithScopes2()); comp.addJavaSourceCode("p.xyz.InnerTest10", generateWithDataNamesForFuzzing()); comp.addJavaSourceCode("p.xyz.InnerTest11", generateWithStructuralNamesForMethods()); @@ -91,6 +94,7 @@ public class TestTutorial { comp.invoke("p.xyz.InnerTest2", "main", new Object[] {}); comp.invoke("p.xyz.InnerTest3", "main", new Object[] {}); comp.invoke("p.xyz.InnerTest3b", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest3c", "main", new Object[] {}); comp.invoke("p.xyz.InnerTest4", "main", new Object[] {}); comp.invoke("p.xyz.InnerTest5", "main", new Object[] {}); comp.invoke("p.xyz.InnerTest6", "main", new Object[] {}); @@ -105,9 +109,9 @@ public class TestTutorial { // This example shows the use of various Tokens. public static String generateWithListOfTokens() { // A Template is essentially a function / lambda that produces a - // token body, which is a list of Tokens that are concatenated. - var templateClass = Template.make(() -> body( - // The "body" method is filled by a sequence of "Tokens". + // scope, which contains a list of Tokens that are concatenated. + var templateClass = Template.make(() -> scope( + // The "scope" arguments are a sequence of "Tokens". // These can be Strings and multi-line Strings, but also // boxed primitives. """ @@ -141,14 +145,14 @@ public class TestTutorial { // This example shows the use of Templates, with and without arguments. public static String generateWithTemplateArguments() { // A Template with no arguments. - var templateHello = Template.make(() -> body( + var templateHello = Template.make(() -> scope( """ System.out.println("Hello"); """ )); // A Template with a single Integer argument. - var templateCompare = Template.make("arg", (Integer arg) -> body( + var templateCompare = Template.make("arg", (Integer arg) -> scope( "System.out.println(", arg, ");\n", // capture arg via lambda argument "System.out.println(#arg);\n", // capture arg via hashtag replacement "System.out.println(#{arg});\n", // capture arg via hashtag replacement with brackets @@ -156,7 +160,7 @@ public class TestTutorial { // argument values into Strings. However, since these are not (yet) // available, the Template Framework provides two alternative ways of // formatting Strings: - // 1) By appending to the comma-separated list of Tokens passed to body(). + // 1) By appending to the comma-separated list of Tokens passed to scope(). // Appending as a Token works whenever one has a reference to the Object // in Java code. But often, this is rather cumbersome and looks awkward, // given all the additional quotes and commands required. Hence, it @@ -180,7 +184,7 @@ public class TestTutorial { // A Template that creates the body of the Class and main method, and then // uses the two Templates above inside it. - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; @@ -204,8 +208,16 @@ public class TestTutorial { // Note: hashtag replacements are a workaround for the missing string templates. // If we had string templates, we could just capture the typed lambda // arguments, and use them directly in the String via string templating. + // + // Important: hashtag replacements are always constrained to a single template + // and are not available in any nested templates. Hashtag replacements + // are only there to facilitate string templating within the limited + // scope of a template. You may consider it like a "local variable" + // for code generation purposes only. + // If you need to pass some value to a nested Template, consider using + // a Template argument, and capturing that Template argument. public static String generateWithHashtagAndDollarReplacements() { - var template1 = Template.make("x", (Integer x) -> body( + var template1 = Template.make("x", (Integer x) -> scope( // We have the "#x" hashtag replacement from the argument capture above. // Additionally, we can define "#con" as a hashtag replacement from let: let("con", 3 * x), @@ -219,29 +231,27 @@ public class TestTutorial { """ )); - var template2 = Template.make("x", (Integer x) -> + var template2 = Template.make("x", (Integer x) -> scope( // Sometimes it can be helpful to not just create a hashtag replacement // with let, but also to capture the variable to use it as lambda parameter. - let("y", 11 * x, y -> - body( - """ - System.out.println("T2: #x, #y"); - """, - template1.asToken(y) - ) - ) - ); + let("y", 11 * x, y -> scope( + """ + System.out.println("T2: #x, #y"); + """, + template1.asToken(y) + )) + )); // This template generates an int variable and assigns it a value. // Together with template4, we see that each template has a unique renaming // for a $-name replacement. - var template3 = Template.make("name", "value", (String name, Integer value) -> body( + var template3 = Template.make("name", "value", (String name, Integer value) -> scope( """ int #name = #value; // Note: $var is not #name """ )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( """ // We will define the variable $var: """, @@ -252,7 +262,7 @@ public class TestTutorial { """ )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( // The Template Framework API only guarantees that every Template use // has a unique ID. When using the Templates, all we need is that // variables from different Template uses do not conflict. But it can @@ -300,7 +310,7 @@ public class TestTutorial { // "INT_CON" and "LONG_CON". public static String generateWithHashtagAndDollarReplacements2() { // Let us define some final static variables of a specific type. - var template1 = Template.make("type", (String type) -> body( + var template1 = Template.make("type", (String type) -> scope( // The type (e.g. "int") is lower case, let us create the upper case "INT_CON" from it. let("TYPE", type.toUpperCase()), """ @@ -309,7 +319,7 @@ public class TestTutorial { )); // Let's write a simple class to demonstrate that this works, i.e. produces compilable code. - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; @@ -331,50 +341,221 @@ public class TestTutorial { return templateClass.render(); } + // We already have used "scope" multiple times, but not explained it yet. + // So far, we have seen "scope" mostly in the context of Template scopes, but they + // can be used in many contexts as we will see below. They can also be used on + // their own and in the use of "let", as we will show right now. + // + // Scopes are even more relevant for DataNames and Structural names. + // See: generateWithDataNamesForFieldsAndVariables + // See: generateWithScopes1 + // See: generateWithScopes2 + public static String generateWithHashtagAndDollarReplacements3() { + + var template1 = Template.make(() -> scope( + // We can use scopes to limit the liveness of hashtag replacements. + scope( + let("x", 3), // does not escape + """ + static int v1_3 = #x; + """ + ), + scope( + let("x", 5), // does not escape + """ + static int v1_5 = #x; + """ + ), + // Using "scope" does not just limit the liveness / availability + // of hashtag replacements, but also of DataNames, StructuralNames, + // and setFuelCost. We can use "hashtagScope" to only limit hashtag + // replacements. + hashtagScope( + let("x", 7), // does not escape + """ + static int v1_7 = #x; + """ + ), + // Using "transparentScope" means the scope is transparent, and the hashtag + // replacements escape the scope. + transparentScope( + let("x", 11), // escapes the "transparentScope". + """ + static int v1_11a = #x; + """ + ), + // The hashtag replacement from the "transparentScope" escaped, and is + // still available. + """ + static int v1_11b = #x; + """ + )); + + var template2 = Template.make("x", (Integer x) -> scope( + // We can map a list of values to a list of scopes. Using a scope that is + // non-transparent for hashtag replacements means that we can reuse the same + // hashtag key when looping / streaming over multiple values. + List.of(3, 5, 7).stream().map(y -> scope( + let("y", y), // does not escape -> allows reuse of hashtag key "y". + """ + static int v2_#{x}_#{y} = #x * #y; + """ + )).toList() + )); + + var template3 = Template.make("x", (Integer x) -> scope( + // When using a "let" that captures the value in a lambda argument, we have + // to choose what kind of scope we generate. In most cases "scope" or + // "hashtagScope" are the best, because they limit the hashtag replacement + // of "y" to the same scope as the lambda argument. + let("y", x * 11, y -> scope( + """ + static int v3a_#{x} = #y; + """ + )), + // But in rare cases, we may want "y" and some nested "z" to escape. + let("y", x * 11, y -> transparentScope( + let("z", y * 2), + """ + static int v3b_#{x} = #y - #z; + """ + )), + // Because of the "transparentScope", "y" and "z" have escaped. + """ + static int v3c_#{x} = #y - #z; + """, + // Side note: We can simulate a "let" without lambda with a "let" that has a lambda. + // That is not very useful, but a similar trick can be used for other queries, that + // only provide a lambda version, and where we only want to use the hashtag replacement. + // + // Below we see the standard use of "let", where we add a hashtag replacement for "a" + // for the rest of the enclosing scope. We then also use a lambda version of "let" + // with a transparent scope, which means that "b" escapes that scope and is also + // available in the enclosing scope. In the implementation of the framework, we + // actually use a "transparentScope", so the standard "let" is really just syntactic + // sugar for the lambda "let" with "transparentScope". + let("a", -x), + let("b", -x, b -> transparentScope()), + """ + static int v3d_#{x} = #a + #b; + """ + )); + + // Let's write a simple class to demonstrate that this works, i.e. produces compilable code. + var templateClass = Template.make(() -> scope( + """ + package p.xyz; + + public class InnerTest3c { + """, + template1.asToken(), + template2.asToken(1), + template2.asToken(2), + template3.asToken(2), + """ + public static void main() { + if (v1_3 != 3 || + v1_5 != 5 || + v1_7 != 7 || + v1_11a != 11 || + v1_11b != 11 || + v2_1_3 != 3 || + v2_1_5 != 5 || + v2_1_7 != 7 || + v2_2_3 != 6 || + v2_2_5 != 10 || + v2_2_7 != 14 || + v3a_2 != 22 || + v3b_2 != -22 || + v3c_2 != -22 || + v3d_2 != -4) { + throw new RuntimeException("Wrong result!"); + } + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + // In this example, we look at the use of Hooks. They allow us to reach back, to outer // scopes. For example, we can reach out from inside a method body to a hook anchored at // the top of the class, and insert a field. + // + // When we insert to a hook, we have 3 relevant scopes: + // - Anchor scope: the scope defined at "hook.anchor(scope(...))" + // - Insertion scope: the scope that is inserted, see "hook.insert(scope(...))" + // - Caller scope: the scope we insert from. + // + // The choice of transparency of an insertion scope (the scope that is inserted) is quite + // important. A common use case is to insert a DataName. + // See: generateWithDataNamesForFieldsAndVariables + // See: generateWithScopes1 + // See: generateWithScopes2 public static String generateWithCustomHooks() { // We can define a custom hook. // Note: generally we prefer using the pre-defined CLASS_HOOK and METHOD_HOOK from the library, // whenever possible. See also the example after this one. var myHook = new Hook("MyHook"); - var template1 = Template.make("name", "value", (String name, Integer value) -> body( + var template1 = Template.make("name", "value", (String name, Integer value) -> scope( """ public static int #name = #value; """ )); - var template2 = Template.make("x", (Integer x) -> body( + var template2 = Template.make("x", (Integer x) -> scope( """ - // Let us go back to where we anchored the hook with anchor() and define a field named $field there. - // Note that in the Java code we have not defined anchor() on the hook, yet. But since it's a lambda - // expression, it is not evaluated, yet! Eventually, anchor() will be evaluated before insert() in - // this example. + // Let us go back to where we anchored the hook with anchor() (see 'templateClass' below) and define a field + // named $field1 there. """, - myHook.insert(template1.asToken($("field"), x)), + myHook.insert(scope( // <- insertion scope + """ + public static int $field1 = #x; + """ + // Note that we were able to use the dollar replacement "$field1" and the hashtag + // replacement "#x" inside the scope that is inserted to myHook. + )), """ - System.out.println("$field: " + $field); - if ($field != #x) { throw new RuntimeException("Wrong value!"); } + // We can do that by inserting a scope like above, or by inserting a template, like below. + // + // Which method is used is up to the user. General guidance is if the same code may also + // be inserted elsewhere, one should lean towards inserting templates. But in many cases + // it is nice to see the inserted code directly, and to be able to use hashtag replacements + // from the outer scope directly, without having to route them via template arguments, + // as we have to do below. + """, + // <- caller scope + myHook.insert(template1.asToken($("field2"), x)), + """ + System.out.println("$field1: " + $field1); + System.out.println("$field2: " + $field2); + if ($field1 != #x) { throw new RuntimeException("Wrong value 1!"); } + if ($field2 != #x) { throw new RuntimeException("Wrong value 2!"); } """ )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; public class InnerTest4 { """, // We anchor a Hook outside the main method, but inside the Class. - // Anchoring a Hook creates a scope, spanning the braces of the - // "anchor" call. Any Hook.insert that happens inside this scope - // goes to the top of that scope. - myHook.anchor( + // Anchoring a Hook requires the definition of an inner scope, + // aka the "anchor scope", spanning the braces of the "anchor" call. + // Any Hook.insert that happens inside this scope goes to the top of + // that scope. + myHook.anchor(scope( // <- anchor scope // Any Hook.insert goes here. // - // <-------- field_X = 5 ------------------+ - // <-------- field_Y = 7 -------------+ | + // <-------- field1_X = 5 -----------------+ + // field2_X = 5 | + // | + // <-------- field1_Y = 7 ------------+ | + // field2_Y = 7 | | // | | """ public static void main() { @@ -384,7 +565,7 @@ public class TestTutorial { """ } """ - ), // The Hook scope ends here. + )), // The Hook scope ends here. """ } """ @@ -408,46 +589,54 @@ public class TestTutorial { // there is a class scope inside another class scope. Similarly, we can nest lambda bodies // inside method bodies, so also METHOD_HOOK can be used in such a "re-entrant" way. public static String generateWithLibraryHooks() { - var templateStaticField = Template.make("name", "value", (String name, Integer value) -> body( - """ - static { System.out.println("Defining static field #name"); } - public static int #name = #value; - """ - )); - var templateLocalVariable = Template.make("name", "value", (String name, Integer value) -> body( - """ - System.out.println("Defining local variable #name"); - int #name = #value; - """ - )); - - var templateMethodBody = Template.make(() -> body( + var templateMethodBody = Template.make(() -> scope( """ // Let's define a local variable $var and a static field $field. - """, - Hooks.CLASS_HOOK.insert(templateStaticField.asToken($("field"), 5)), - Hooks.METHOD_HOOK.insert(templateLocalVariable.asToken($("var"), 11)), - """ + // Since we are inserting them at the anchor before the code below, + // they will already be available: System.out.println("$field: " + $field); System.out.println("$var: " + $var); + """, + Hooks.CLASS_HOOK.insert(scope( + """ + static { System.out.println("Defining static field $field"); } + public static int $field = 5; + """ + )), + Hooks.METHOD_HOOK.insert(scope( + """ + System.out.println("Defining local variable $var"); + int $var = 11; + """ + )), + """ if ($field * $var != 55) { throw new RuntimeException("Wrong value!"); } """ + // Note: we have used "scope" for the "insert" scope. This is fine here as + // we are only working with code and hashtags, but not with DataNames. If + // we were to also "addDataName" inside the insert scope, we would have to + // make sure that the scope is transparent for DataNames, so that they can + // escape to the anchor scope, and can be available to the caller of the + // insertion. One might want to use "transparentScope" for the insertion scope. + // See: generateWithDataNamesForFieldsAndVariables. + // See: generateWithScopes1 + // See: generateWithScopes2 )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; public class InnerTest5 { """, // Class Hook for fields. - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( """ public static void main() { """, // Method Hook for local variables, and earlier computations. - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( """ // This is the beginning of the "main" method body. System.out.println("Welcome to main!"); @@ -457,7 +646,7 @@ public class TestTutorial { System.out.println("Going to call other..."); other(); """ - ), + )), """ } @@ -465,7 +654,7 @@ public class TestTutorial { """, // Have a separate method hook for other, so that it can insert // its own local variables. - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( """ System.out.println("Welcome to other!"); """, @@ -473,11 +662,11 @@ public class TestTutorial { """ System.out.println("Done with other."); """ - ), + )), """ } """ - ), + )), """ } """ @@ -493,7 +682,7 @@ public class TestTutorial { public static String generateWithRecursionAndBindingsAndFuel() { // Binding allows the use of template1 inside of template1, via the binding indirection. var binding1 = new TemplateBinding>(); - var template1 = Template.make("depth", (Integer depth) -> body( + var template1 = Template.make("depth", (Integer depth) -> scope( let("fuel", fuel()), """ System.out.println("At depth #depth with fuel #fuel."); @@ -514,7 +703,7 @@ public class TestTutorial { )); binding1.bind(template1); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; @@ -561,6 +750,12 @@ public class TestTutorial { // // To get started, we show an example where all DataNames have the same type, and where // all Names are mutable. For simplicity, our type represents the primitive int type. + // + // Note: the template library contains a lot of types that model the Java types, + // such as primitive types ({@code PrimitiveType}). The following examples + // give insight into how those types work. If you are just interested in + // how to use the predefined types, then you can find other examples in + // {@code examples/TestPrimitiveTypes.java}. private record MySimpleInt() implements DataName.Type { // The type is only subtype of itself. This is relevant when sampling or weighing // DataNames, because we do not just sample from the given type, but also its subtypes. @@ -577,31 +772,25 @@ public class TestTutorial { private static final MySimpleInt mySimpleInt = new MySimpleInt(); // In this example, we generate 3 fields, and add their names to the - // current scope. In a nested Template, we can then sample one of these - // DataNames, which gives us one of the fields. We increment that randomly - // chosen field. At the end, we print all three fields. + // current scope. We can then sample some of these DataNames, which + // gives us one of those fields each time. We increment those randomly + // chosen fields. At the end, we print all three fields. public static String generateWithDataNamesSimple() { - var templateMain = Template.make(() -> body( - // Sample a random DataName, i.e. field, and assign its name to - // the hashtag replacement "#f". - // We are picking a mutable DataName, because we are not just - // reading but also writing to the field. - let("f", dataNames(MUTABLE).exactOf(mySimpleInt).sample().name()), - """ - // Let us now sample a random field #f, and increment it. - #f += 42; - """ - )); - - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( // Let us define the names for the three fields. - // We can then sample from these names in a nested Template. // We make all DataNames mutable, and with the same weight of 1, // so that they have equal probability of being sampled. // Note: the default weight is 1, so we can also omit the weight. + // + // Also note that DataNames are only available once they are defined: + // + // Nothing defined, yet: dataNames() = {} addDataName($("f1"), mySimpleInt, MUTABLE, 1), + // Only now dataNames() contains f1: dataNames() = {f1} addDataName($("f2"), mySimpleInt, MUTABLE, 1), + // dataNames() = {f1, f2} addDataName($("f3"), mySimpleInt, MUTABLE), // omit weight, default is 1. + // dataNames() = {f1, f2, f3} """ package p.xyz; @@ -612,18 +801,35 @@ public class TestTutorial { public static int $f3 = 0; public static void main() { - // Let us now call the nested template that samples - // a random field and increments it. + // Let us now sample a random field and assign its name to + // the hashtag replacement "a". """, - templateMain.asToken(), + dataNames(MUTABLE).exactOf(mySimpleInt).sampleAndLetAs("a"), + """ + // We can now access the field, and increment it. + #a += 42; + // If we are also interested in the type of the field, we can do: + """, + dataNames(MUTABLE).exactOf(mySimpleInt).sampleAndLetAs("b", "bType"), + """ + #b += 7; + // In some cases, we may want to capture the DataName directly, which + // requires capturing the value in a lambda that creates an inner scope: + """, + dataNames(MUTABLE).exactOf(mySimpleInt).sample((DataName dn) -> scope( + let("c", dn.name()), + """ + #c += 12; + """ + )), """ // Now, we can print all three fields, and see which - // one was incremented. + // ones were incremented. System.out.println("f1: " + $f1); System.out.println("f2: " + $f2); System.out.println("f3: " + $f3); - // We have two zeros, and one 42. - if ($f1 + $f2 + $f3 != 42) { throw new RuntimeException("wrong result!"); } + // Make sure they add up to the correct sum. + if ($f1 + $f2 + $f3 != 42 + 7 + 12) { throw new RuntimeException("wrong result!"); } } } """ @@ -662,8 +868,15 @@ public class TestTutorial { public static String generateWithDataNamesForFieldsAndVariables() { // Define a static field. - var templateStaticField = Template.make("type", (DataName.Type type) -> body( - addDataName($("field"), type, MUTABLE), + // Note: it is very important that we use a "transparentScope" for the template here, + // so that the DataName can escape to outer scopes, so that it is available to + // everything that follows the DataName definition in the outer scope. + // (We could also use "hashtagScope", since those are also transparent for + // names. But it is not great style, because template boundaries are + // non-transparent for hashtags and setFuelCost anyway. So we might as + // well just use "transparentScope".) + var templateStaticField = Template.make("type", (DataName.Type type) -> transparentScope( + addDataName($("field"), type, MUTABLE), // escapes template because of "transparentScope" // Note: since we have overridden MyPrimitive::toString, we can use // the type directly as "#type" in the template, which then // gets hashtag replaced with "int" or "long". @@ -673,8 +886,10 @@ public class TestTutorial { )); // Define a local variable. - var templateLocalVariable = Template.make("type", (DataName.Type type) -> body( - addDataName($("var"), type, MUTABLE), + // Note: it is very important that we use a "transparentScope" for the template here, + // so that the DataName can escape to outer scopes. + var templateLocalVariable = Template.make("type", (DataName.Type type) -> transparentScope( + addDataName($("var"), type, MUTABLE), // escapes template because of "transparentScope" """ #type $var = 0; """ @@ -682,8 +897,8 @@ public class TestTutorial { // Sample a random field or variable, from those that are available at // the current scope. - var templateSample = Template.make("type", (DataName.Type type) -> body( - let("name", dataNames(MUTABLE).exactOf(type).sample().name()), + var templateSample = Template.make("type", (DataName.Type type) -> scope( + dataNames(MUTABLE).exactOf(type).sampleAndLetAs("name"), // Note: we could also sample from MUTABLE_OR_IMMUTABLE, we will // cover the concept of mutability in an example further down. """ @@ -692,18 +907,36 @@ public class TestTutorial { )); // Check how many fields and variables are available at the current scope. - var templateStatus = Template.make(() -> body( - let("ints", dataNames(MUTABLE).exactOf(myInt).count()), - let("longs", dataNames(MUTABLE).exactOf(myLong).count()), - // Note: we could also count the MUTABLE_OR_IMMUTABLE, we will - // cover the concept of mutability in an example further down. + var templateStatus = Template.make(() -> scope( + dataNames(MUTABLE).exactOf(myInt).count(ints -> scope( + dataNames(MUTABLE).exactOf(myLong).count(longs -> scope( + // We have now captured the values as Java variables, and can + // use them inside the scope in some "let" definitions. + let("ints", ints), + let("longs", longs), + // Note: we could also count the MUTABLE_OR_IMMUTABLE, we will + // cover the concept of mutability in an example further down. + """ + System.out.println("Status: #ints ints, #longs longs."); + """ + )) + )), + // In a real code generation case, we would most likely want to + // have the count as a Java variable so that one can take conditional + // action based on the value. For that we have to capture the count + // with a lambda and inner scope as above. If we only need to have + // the count as a hashtag replacement, we can also use the following + // trick: + dataNames(MUTABLE).exactOf(myInt).count(c -> transparentScope(let("ints", c))), + dataNames(MUTABLE).exactOf(myLong).count(c -> transparentScope(let("longs", c))), + // Because of the "transparentScope", the hashtag replacements escape. """ System.out.println("Status: #ints ints, #longs longs."); """ )); // Definition of the main method body. - var templateMain = Template.make(() -> body( + var templateMain = Template.make(() -> scope( """ System.out.println("Starting inside main..."); """, @@ -736,7 +969,7 @@ public class TestTutorial { // Definition of another method's body. It is in the same class // as the main method, so it has access to the same static fields. - var templateOther = Template.make(() -> body( + var templateOther = Template.make(() -> scope( """ System.out.println("Starting inside other..."); """, @@ -755,19 +988,19 @@ public class TestTutorial { )); // Finally, we put it all together in a class. - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; public class InnerTest8 { """, // Class Hook for fields. - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( """ public static void main() { """, // Method Hook for local variables. - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( """ // This is the beginning of the "main" method body. System.out.println("Welcome to main!"); @@ -777,7 +1010,7 @@ public class TestTutorial { System.out.println("Going to call other..."); other(); """ - ), + )), """ } @@ -785,7 +1018,7 @@ public class TestTutorial { """, // Have a separate method hook for other, where it could insert // its own local variables (but happens not to). - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( """ System.out.println("Welcome to other!"); """, @@ -793,11 +1026,11 @@ public class TestTutorial { """ System.out.println("Done with other."); """ - ), + )), """ } """ - ), + )), """ } """ @@ -807,83 +1040,119 @@ public class TestTutorial { return templateClass.render(); } - // Let us have a closer look at how DataNames interact with scopes created by - // Templates and Hooks. Additionally, we see how the execution order of the - // lambdas and token evaluation affects the availability of DataNames. - // - // We inject the results directly into verification inside the code, so it - // is relatively simple to see what the expected results are. - // - // For simplicity, we define a simple "list" function. It collects all - // field and variable names, and immediately returns the comma separated - // list of the names. We can use that to visualize the available names - // at any point. - public static String listNames() { - return "{" + String.join(", ", dataNames(MUTABLE).exactOf(myInt).toList() - .stream().map(DataName::name).toList()) + "}"; - } + public static String generateWithScopes1() { - // Even simpler: count the available variables and return the count immediately. - public static int countNames() { - return dataNames(MUTABLE).exactOf(myInt).count(); - } - - // Having defined these helper methods, let us start with the first example. - // You should start reading this example bottom-up, starting at - // templateClass, then going to templateMain and last to templateInner. - public static String generateWithDataNamesAndScopes1() { - - var templateInner = Template.make(() -> body( - // We just got called from the templateMain. All tokens from there - // are already evaluated, so "v1" is now available: - let("l1", listNames()), + // For the examples below, we need a convenient way of asserting the state + // of the available DataNames. + var templateVerify = Template.make("count", "hasAny", "toList", (Integer count, Boolean hasAny, String toList) -> scope( + dataNames(MUTABLE).exactOf(myInt).count(c -> transparentScope(let("count2", c))), + dataNames(MUTABLE).exactOf(myInt).hasAny(h -> transparentScope(let("hasAny2", h))), + dataNames(MUTABLE).exactOf(myInt).toList(list -> transparentScope( + let("toList2", String.join(", ", list.stream().map(DataName::name).toList())) + )), """ - if (!"{v1}".equals("#l1")) { throw new RuntimeException("l1 should have been '{v1}' but was '#l1'"); } + if (#count != #count2 || + #hasAny != #hasAny2 || + !"#toList".equals("#toList2")) { + throw new RuntimeException("verify failed"); + } """ )); - var templateMain = Template.make(() -> body( - // So far, no names were defined. We expect "c1" to be zero. - let("c1", countNames()), - """ - if (#c1 != 0) { throw new RuntimeException("c1 was not zero but #c1"); } - """, - // We now add a local variable "v1" to the scope of this templateMain. - // This only generates a token, and does not immediately add the name. - // The name is only added once we evaluate the tokens, and arrive at - // this particular token. + var templateMain = Template.make(() -> scope( + "// Start with nothing:\n", + templateVerify.asToken(0, false, ""), + "// Add v1:\n", addDataName("v1", myInt, MUTABLE), - // We count again with "c2". The variable "v1" is at this point still - // in token form, hence it is not yet made available while executing - // the template lambda of templateMain. - let("c2", countNames()), + "int v1 = 1;\n", + "// Check that it is visible:\n", + templateVerify.asToken(1, true, "v1"), + "// Add v2:\n", + addDataName("v2", myInt, MUTABLE), + "int v2 = 2;\n", + "// Check that both are visible:\n", + templateVerify.asToken(2, true, "v1, v2"), + + "// Create a local scope:\n", + "{\n", scope( // for consistency, we model the code and template scope together. + "// Add v3:\n", + addDataName("v3", myInt, MUTABLE), + "int v3 = 3;\n", + "// Check that all are visible:\n", + templateVerify.asToken(3, true, "v1, v2, v3") + ), "}\n", + "// But after the scope, v3 is no longer available:\n", + templateVerify.asToken(2, true, "v1, v2"), + + "// Now let's create a list of variables.\n", + List.of(4, 5, 6).stream().map(i -> hashtagScope( + // The hashtagScope allows hashtag replacements to be local, + // and DataNames to escape, so we can use them afterwards. + let("i", i), + addDataName("v" + i, myInt, MUTABLE), + "int v#i = #i;\n" + )).toList(), + templateVerify.asToken(5, true, "v1, v2, v4, v5, v6"), + + "// Let's multiply all variables by a factor of 2, using forEach:\n", + dataNames(MUTABLE).exactOf(myInt).forEach(dn -> scope( + let("v", dn.name()), + "#v *= 2;\n" + )), + "// We can also capture the name (v) and type of the DataName:\n", + dataNames(MUTABLE).exactOf(myInt).forEach("v", "type", dn -> scope( + "#v *= 2;\n" + )), + "// Yet another option is using toList, but here that is more cumbersome:\n", + dataNames(MUTABLE).exactOf(myInt).toList(list -> scope( + list.stream().map(dn -> scope( + let("v", dn.name()), + "#v *= 2;\n" + )).toList() + )), + """ - if (#c2 != 0) { throw new RuntimeException("c2 was not zero but #c2"); } + // We verify the result again. """, - // But now we call an inner Template. This is added as a TemplateToken. - // This means it is not evaluated immediately, but only once we evaluate - // the tokens. By that time, all tokens from above are already evaluated - // and we see that "v1" is available. - templateInner.asToken() + templateVerify.asToken(5, true, "v1, v2, v4, v5, v6"), + """ + if (v1 != 1 * 8 || + v2 != 2 * 8 || + v4 != 4 * 8 || + v5 != 5 * 8 || + v6 != 6 * 8) { + throw new RuntimeException("wrong value!"); + } + """, + + "// Let us copy each variable:\n", + dataNames(MUTABLE).exactOf(myInt).forEach("v", "type", dn -> hashtagScope( + // Note that we need a hashtagScope here, so that we can reuse "v" and + // "type" as hashtag replacements in each iteration, but still let the + // copied DataNames escape. + addDataName(dn.name() + "_copy", myInt, MUTABLE), + "#type #{v}_copy = #v;\n" + )), + templateVerify.asToken(10, true, "v1, v2, v4, v5, v6, v1_copy, v2_copy, v4_copy, v5_copy, v6_copy") )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; public class InnerTest9a { """, - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( """ public static void main() { """, - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( templateMain.asToken() - ), + )), """ } """ - ), + )), """ } """ @@ -893,111 +1162,129 @@ public class TestTutorial { return templateClass.render(); } - // Now that we understand this simple example, we go to a more complicated one - // where we use Hook.insert. Just as above, you should read this example - // bottom-up, starting at templateClass. - public static String generateWithDataNamesAndScopes2() { + public static String generateWithScopes2() { - var templateFields = Template.make(() -> body( - // We were just called from templateMain. But the code is not - // generated into the main scope, rather into the class scope - // out in templateClass. - // Let us now add a field "f1". - addDataName("f1", myInt, MUTABLE), - // And let's also generate the code for it. + // In this section, we will look at some subtle facts about the behavior of + // transparent scopes around hook insertion. This is intended for expert users + // so feel free to skip it until you extensively use hook insertion. + // More info can also be found in the Javadocs of the Hook class. + + // Helper method to check that the expected DataNames are available. + var templateVerify = Template.make("toList", (String toList) -> scope( + dataNames(MUTABLE).exactOf(myInt).toList(list -> transparentScope( + let("toList2", String.join(", ", list.stream().map(DataName::name).toList())) + )), """ - public static int f1 = 42; - """, - // But why is this DataName now available inside the scope of - // templateInner? Does that not mean that "f1" escapes this - // templateFields here? Yes it does! - // For normal template nesting, the names do not escape the - // scope of the nested template. But this here is no normal - // template nesting, rather it is an insertion into a Hook, - // and we treat those differently. We make the scope of the - // inserted templateFields transparent, so that any added - // DataNames are added to the scope of the Hook we just - // inserted into, i.e. the CLASS_HOOK. This is very important, - // if we did not make that scope transparent, we could not - // add any DataNames to the class scope anymore, and we could - // not add any fields that would be available in the class - // scope. - Hooks.METHOD_HOOK.anchor( - // We now create a separate scope. This one is not the - // template scope from above, and it is not transparent. - // Hence, "f2" will not be available outside of this + if (!"#toList".equals("#toList2")) { + throw new RuntimeException("verify failed: '#toList' vs '#toList2'."); + } + """ + )); + + var myHook = new Hook("MyHook"); + + var templateMain = Template.make(() -> scope( + // Start with nothing: + templateVerify.asToken(""), + addDataName("v1", myInt, MUTABLE), + templateVerify.asToken("v1"), + // Non-transparent hook anchor: + myHook.anchor(scope( + templateVerify.asToken("v1"), + addDataName("v2", myInt, MUTABLE), + templateVerify.asToken("v1, v2"), + // Insert a non-transparent scope: nothing escapes. + myHook.insert(scope( + // Note that at the anchor insertion point, v2 is not yet + // available, because it is added after the anchoring. + templateVerify.asToken("v1"), + let("x3", 42), + addDataName("v3", myInt, MUTABLE), + templateVerify.asToken("v1, v3") + )), + // Note: x3 and v3 do not escape. + let("x3", 7), // we can define it again. + templateVerify.asToken("v1, v2"), + // While not letting hashtags escape may be helpful, it is probably + // not very helpful if the DataNames don't escape. For example, if + // we are inserting some variable at an outer scope, we would like + // it to be available for the rest of the scope. + // That's where a transparent scope can be helpful. + myHook.insert(transparentScope( + // At the anchoring, still only v1 is available. + templateVerify.asToken("v1"), + let("x4", 42), // escapes to caller scope + addDataName("v4", myInt, MUTABLE), // escapes to anchor scope + templateVerify.asToken("v1, v4") + )), + // x4 escapes to the caller out here, and not to the anchor scope. + "// x4: #x4\n", + // And v4 escapes to the anchor scope, which is available from here too. + // Interesting detail: the ordering in the list indicates that v1 + // is from the outermost scope of the template, v4 is located at the + // anchor scope, and v2 is located inside the anchor scope, and + // thus comes last. + templateVerify.asToken("v1, v4, v2"), + // In most practical cases we probably don't want to let the hashtag + // escape, because they just represent something local. So we can + // use a hashtagScope, so that DataNames escape, but not hashtags. + myHook.insert(hashtagScope( + // Note: both v1 and v4 are now available at the anchoring, since + // v1 was inserted outside the anchoring scope, and v4 was just + // inserted to the anchoring scope. + templateVerify.asToken("v1, v4"), + let("x5", 42), // local, does not escape. + addDataName("v5", myInt, MUTABLE), // escapes to anchor scope + templateVerify.asToken("v1, v4, v5") + )), + let("x5", 7), // we can define it again. + templateVerify.asToken("v1, v4, v5, v2") + )), + // We left the non-transparent anchoring scope which does not let anything escape + templateVerify.asToken("v1"), + + // Let us now do something that probably should never be done. But still + // we want to demonstrate it for educational purposes: transparent anchoring + // scopes. + myHook.anchor(transparentScope( + templateVerify.asToken("v1"), + // For one, this means that DataName escape the scope directly. + addDataName("v6", myInt, MUTABLE), + templateVerify.asToken("v1, v6"), + // But also if we insert to the anchoring scope, DataNames don't just + // escape from the anchoring scope, but further out to the enclosing // scope. - addDataName("f2", myInt, MUTABLE), - // And let's also generate the code for it. - """ - public static int f2 = 666; - """ - // Similarly, if we called any nested Template here, - // and added DataNames inside, this would happen inside - // nested scopes that are not transparent. If one wanted - // to add names to the CLASS_HOOK from there, one would - // have to do another Hook.insert, and make sure that - // the names are added from the outermost scope of that - // inserted Template, because only that outermost scope - // is transparent to the CLASS_HOOK. - ) + myHook.insert(transparentScope( + templateVerify.asToken("v1, v6"), + addDataName("v7", myInt, MUTABLE), + templateVerify.asToken("v1, v6, v7") + )), + templateVerify.asToken("v1, v6, v7"), + let("x6", 42) // escapes the anchor scope + )), + // We left the transparent anchoring scope which lets the DataNames and + // hashtags escape. + "// x6: #x6\n", + templateVerify.asToken("v1, v6, v7") )); - var templateInner = Template.make(() -> body( - // We just got called from the templateMain. All tokens from there - // are already evaluated, so there should be some fields available. - // We can see field "f1". - let("l1", listNames()), - """ - if (!"{f1}".equals("#l1")) { throw new RuntimeException("l1 should have been '{f1}' but was '#l1'"); } - """ - // Now go and have a look at templateFields, to understand how that - // field was added, and why not any others. - )); - - var templateMain = Template.make(() -> body( - // So far, no names were defined. We expect "c1" to be zero. - let("c1", countNames()), - """ - if (#c1 != 0) { throw new RuntimeException("c1 was not zero but #c1"); } - """, - // We would now like to add some fields to the class scope, out in the - // templateClass. This creates a token, which is only evaluated after - // the completion of the templateMain lambda. Before you go and look - // at templateFields, just assume that it does add some fields, and - // continue reading in templateMain. - Hooks.CLASS_HOOK.insert(templateFields.asToken()), - // We count again with "c2". The fields we wanted to add above are not - // yet available, because the token is not yet evaluated. Hence, we - // still only count zero names. - let("c2", countNames()), - """ - if (#c2 != 0) { throw new RuntimeException("c2 was not zero but #c2"); } - """, - // Now we call an inner Template. This also creates a token, and so it - // is not evaluated immediately. And by the time this token is evaluated - // the tokens from above are already evaluated, and so the fields should - // be available. Go have a look at templateInner now. - templateInner.asToken() - )); - - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; public class InnerTest9b { """, - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( """ public static void main() { """, - Hooks.METHOD_HOOK.anchor( + Hooks.METHOD_HOOK.anchor(scope( templateMain.asToken() - ), + )), """ } """ - ), + )), """ } """ @@ -1006,8 +1293,6 @@ public class TestTutorial { // Render templateClass to String. return templateClass.render(); } - - // There are two more concepts to understand more deeply with DataNames. // // One is the use of mutable and immutable DataNames. @@ -1045,38 +1330,40 @@ public class TestTutorial { private static final List myClassList = List.of(myClassA, myClassA1, myClassA2, myClassA11, myClassB); public static String generateWithDataNamesForFuzzing() { - var templateStaticField = Template.make("type", "mutable", (DataName.Type type, Boolean mutable) -> body( - addDataName($("field"), type, mutable ? MUTABLE : IMMUTABLE), + // This template is used to insert a DataName (field) into an outer scope, hence we must use + // "transparentScope" instead of "scope". + var templateStaticField = Template.make("type", "mutable", (DataName.Type type, Boolean mutable) -> transparentScope( + addDataName($("field"), type, mutable ? MUTABLE : IMMUTABLE), // Escapes the template. let("isFinal", mutable ? "" : "final"), """ public static #isFinal #type $field = new #type(); """ )); - var templateLoad = Template.make("type", (DataName.Type type) -> body( + var templateLoad = Template.make("type", (DataName.Type type) -> scope( // We only load from the field, so we do not need a mutable one, // we can load from final and non-final fields. // We want to find any field from which we can read the value and store // it in our variable v of our given type. Hence, we can take a field // of the given type or any subtype thereof. - let("field", dataNames(MUTABLE_OR_IMMUTABLE).subtypeOf(type).sample().name()), + dataNames(MUTABLE_OR_IMMUTABLE).subtypeOf(type).sampleAndLetAs("field"), """ #type $v = #field; System.out.println("#field: " + $v); """ )); - var templateStore = Template.make("type", (DataName.Type type) -> body( + var templateStore = Template.make("type", (DataName.Type type) -> scope( // We are storing to a field, so it better be non-final, i.e. mutable. // We want to store a new instance of our given type to a field. This // field must be of the given type or any supertype. - let("field", dataNames(MUTABLE).supertypeOf(type).sample().name()), + dataNames(MUTABLE).supertypeOf(type).sampleAndLetAs("field"), """ #field = new #type(); """ )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; @@ -1094,7 +1381,7 @@ public class TestTutorial { // addDataName is restricted to the scope of the templateStaticField. But // with the insertion to CLASS_HOOK, the addDataName goes through the scope // of the templateStaticField out to the scope of the CLASS_HOOK. - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( myClassList.stream().map(c -> (Object)Hooks.CLASS_HOOK.insert(templateStaticField.asToken(c, true)) ).toList(), @@ -1118,7 +1405,7 @@ public class TestTutorial { """ } """ - ), + )), """ } """ @@ -1126,7 +1413,6 @@ public class TestTutorial { // Render templateClass to String. return templateClass.render(); - } // "DataNames" are useful for modeling fields and variables. They hold data, @@ -1165,9 +1451,9 @@ public class TestTutorial { public static String generateWithStructuralNamesForMethods() { // Define a method, which takes two ints, returns the result of op. - var templateMethod = Template.make("op", (String op) -> body( + var templateMethod = Template.make("op", (String op) -> transparentScope( // Register the method name, so we can later sample. - addStructuralName($("methodName"), myMethodType), + addStructuralName($("methodName"), myMethodType), // escapes the template because of "transparentScope" """ public static int $methodName(int a, int b) { return a #op b; @@ -1175,16 +1461,16 @@ public class TestTutorial { """ )); - var templateSample = Template.make(() -> body( + var templateSample = Template.make(() -> scope( // Sample a random method, and retrieve its name. - let("methodName", structuralNames().exactOf(myMethodType).sample().name()), + structuralNames().exactOf(myMethodType).sampleAndLetAs("methodName"), """ System.out.println("Calling #methodName with inputs 7 and 11"); System.out.println(" result: " + #methodName(7, 11)); """ )); - var templateClass = Template.make(() -> body( + var templateClass = Template.make(() -> scope( """ package p.xyz; @@ -1192,7 +1478,7 @@ public class TestTutorial { // Let us define some methods that we can sample from later. """, // We must anchor a CLASS_HOOK here, and insert the method definitions to that hook. - Hooks.CLASS_HOOK.anchor( + Hooks.CLASS_HOOK.anchor(scope( // If we directly nest the templateMethod, then the addStructuralName goes to the nested // scope, and is not available at the class scope, i.e. it is not visible // for sampleStructuralName outside of the templateMethod. @@ -1218,7 +1504,7 @@ public class TestTutorial { } } """ - ) + )) )); // Render templateClass to String. diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithTestFrameworkClass.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithTestFrameworkClass.java index 813f2976ef2..01b49db2c01 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithTestFrameworkClass.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithTestFrameworkClass.java @@ -43,7 +43,7 @@ import compiler.lib.generators.Generators; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; import compiler.lib.template_framework.library.Hooks; @@ -82,7 +82,7 @@ public class TestWithTestFrameworkClass { // Generate a source Java file as String public static String generate(CompileFramework comp) { // A simple template that adds a comment. - var commentTemplate = Template.make(() -> body( + var commentTemplate = Template.make(() -> scope( """ // Comment inserted from test method to class hook. """ @@ -103,7 +103,7 @@ public class TestWithTestFrameworkClass { // - The test method makes use of hashtag replacements (#con2 and #op). // - The Check method verifies the results of the test method with the // GOLD value. - var testTemplate = Template.make("op", (String op) -> body( + var testTemplate = Template.make("op", (String op) -> scope( let("size", Generators.G.safeRestrict(Generators.G.ints(), 10_000, 20_000).next()), let("con1", Generators.G.ints().next()), let("con2", Generators.G.safeRestrict(Generators.G.ints(), 1, Integer.MAX_VALUE).next()), diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java index 2dac740dd93..b34538c39c1 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java @@ -38,7 +38,7 @@ import java.util.Set; import compiler.lib.template_framework.DataName; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import compiler.lib.template_framework.library.CodeGenerationDataNameType; import compiler.lib.template_framework.library.Expression; @@ -93,7 +93,7 @@ public class TestExpression { Expression e3 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, "]"); Expression e4 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, ",", myTypeA, "]"); - var template = Template.make(() -> body( + var template = Template.make(() -> scope( "xx", e1.toString(), "yy\n", "xx", e2.toString(), "yy\n", "xx", e3.toString(), "yy\n", @@ -141,7 +141,7 @@ public class TestExpression { Expression e3e1 = e3.nest(0, e1); Expression e4e5 = e4.nest(1, e5); - var template = Template.make(() -> body( + var template = Template.make(() -> scope( "xx", e1e1.toString(), "yy\n", "xx", e2e1.toString(), "yy\n", "xx", e3e1.toString(), "yy\n", @@ -184,7 +184,7 @@ public class TestExpression { // Alternating pattern Expression deep2 = Expression.nestRandomly(myTypeA, List.of(e5, e3), 5); - var template = Template.make(() -> body( + var template = Template.make(() -> scope( "xx", e1e2.toString(), "yy\n", "xx", e1ex.toString(), "yy\n", "xx", e1e4.toString(), "yy\n", diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java index fe267a3ff63..577542e085b 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java @@ -39,7 +39,7 @@ import compiler.lib.compile_framework.*; import compiler.lib.generators.*; import compiler.lib.verify.*; import compiler.lib.template_framework.Template; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; public class TestFormat { @@ -84,7 +84,7 @@ public class TestFormat { private static String generate(List list) { // Generate 2 "get" methods, one that formats via "let" (hashtag), the other via direct token. - var template1 = Template.make("info", (FormatInfo info) -> body( + var template1 = Template.make("info", (FormatInfo info) -> scope( let("id", info.id()), let("type", info.type()), let("value", info.value()), @@ -95,7 +95,7 @@ public class TestFormat { )); // For each FormatInfo in list, generate the "get" methods inside InnerTest class. - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( """ package p.xyz; public class InnerTest { diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java index 35d020b6080..9be74d232a7 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java @@ -44,7 +44,11 @@ import compiler.lib.template_framework.StructuralName; import compiler.lib.template_framework.Hook; import compiler.lib.template_framework.TemplateBinding; import compiler.lib.template_framework.RendererException; -import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.scope; +import static compiler.lib.template_framework.Template.transparentScope; +import static compiler.lib.template_framework.Template.nameScope; +import static compiler.lib.template_framework.Template.hashtagScope; +import static compiler.lib.template_framework.Template.setFuelCostScope; import static compiler.lib.template_framework.Template.$; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.fuel; @@ -121,41 +125,56 @@ public class TestTemplate { // The following tests all pass, i.e. have no errors during rendering. testSingleLine(); testMultiLine(); - testBodyTokens(); + testBasicTokens(); testWithOneArgument(); testWithTwoArguments(); testWithThreeArguments(); - testNested(); - testHookSimple(); + testNestedTemplates(); + testHookSimple1(); + testHookSimple2(); + testHookSimple3(); testHookIsAnchored(); testHookNested(); testHookWithNestedTemplates(); testHookRecursion(); testDollar(); - testLet(); + testLet1(); + testLet2(); testDollarAndHashtagBrackets(); testSelector(); testRecursion(); testFuel(); testFuelCustom(); + testFuelAndScopes(); + testDataNames0a(); + testDataNames0b(); + testDataNames0c(); + testDataNames0d(); testDataNames1(); testDataNames2(); testDataNames3(); testDataNames4(); testDataNames5(); + testDataNames6(); + testStructuralNames0(); testStructuralNames1(); testStructuralNames2(); + testStructuralNames3(); + testStructuralNames4(); + testStructuralNames5(); + testStructuralNames6(); testListArgument(); + testNestedScopes1(); + testNestedScopes2(); + testTemplateScopes(); + testHookAndScopes1(); + testHookAndScopes2(); + testHookAndScopes3(); // The following tests should all fail, with an expected exception and message. expectRendererException(() -> testFailingNestedRendering(), "Nested render not allowed."); expectRendererException(() -> $("name"), "A Template method such as"); - expectRendererException(() -> let("x","y"), "A Template method such as"); expectRendererException(() -> fuel(), "A Template method such as"); - expectRendererException(() -> setFuelCost(1.0f), "A Template method such as"); - expectRendererException(() -> dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).count(), "A Template method such as"); - expectRendererException(() -> dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).sample(), "A Template method such as"); - expectRendererException(() -> (new Hook("abc")).isAnchored(), "A Template method such as"); expectRendererException(() -> testFailingDollarName1(), "Is not a valid '$' name: ''."); expectRendererException(() -> testFailingDollarName2(), "Is not a valid '$' name: '#abc'."); expectRendererException(() -> testFailingDollarName3(), "Is not a valid '$' name: 'abc#'."); @@ -178,20 +197,31 @@ public class TestTemplate { expectRendererException(() -> testFailingDollarHashtagName3(), "Is not a valid '#' replacement pattern: '#' in '#$name'."); expectRendererException(() -> testFailingDollarHashtagName4(), "Is not a valid '$' replacement pattern: '$' in '$#name'."); expectRendererException(() -> testFailingHook(), "Hook 'Hook1' was referenced but not found!"); - expectRendererException(() -> testFailingSample1(), "No variable: MUTABLE, subtypeOf(int), supertypeOf(int)."); + expectRendererException(() -> testFailingSample1a(), "No Name found for DataName.FilterdSet(MUTABLE, subtypeOf(int), supertypeOf(int))"); + expectRendererException(() -> testFailingSample1b(), "No Name found for StructuralName.FilteredSet( subtypeOf(StructuralA) supertypeOf(StructuralA))"); expectRendererException(() -> testFailingHashtag1(), "Duplicate hashtag replacement for #a"); expectRendererException(() -> testFailingHashtag2(), "Duplicate hashtag replacement for #a"); expectRendererException(() -> testFailingHashtag3(), "Duplicate hashtag replacement for #a"); expectRendererException(() -> testFailingHashtag4(), "Missing hashtag replacement for #a"); + expectRendererException(() -> testFailingHashtag5(), "Missing hashtag replacement for #a"); expectRendererException(() -> testFailingBinding1(), "Duplicate 'bind' not allowed."); expectRendererException(() -> testFailingBinding2(), "Cannot 'get' before 'bind'."); - expectIllegalArgumentException(() -> body(null), "Unexpected tokens: null"); - expectIllegalArgumentException(() -> body("x", null), "Unexpected token: null"); - expectIllegalArgumentException(() -> body(new Hook("Hook1")), "Unexpected token:"); + expectIllegalArgumentException(() -> scope(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> scope("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> scope(new Hook("Hook1")), "Unexpected token:"); + expectIllegalArgumentException(() -> transparentScope(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> transparentScope("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> transparentScope(new Hook("Hook1")), "Unexpected token:"); + expectIllegalArgumentException(() -> nameScope(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> nameScope("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> nameScope(new Hook("Hook1")), "Unexpected token:"); + expectIllegalArgumentException(() -> hashtagScope(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> hashtagScope("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> hashtagScope(new Hook("Hook1")), "Unexpected token:"); + expectIllegalArgumentException(() -> setFuelCostScope(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> setFuelCostScope("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> setFuelCostScope(new Hook("Hook1")), "Unexpected token:"); Hook hook1 = new Hook("Hook1"); - expectIllegalArgumentException(() -> hook1.anchor(null), "Unexpected tokens: null"); - expectIllegalArgumentException(() -> hook1.anchor("x", null), "Unexpected token: null"); - expectIllegalArgumentException(() -> hook1.anchor(hook1), "Unexpected token:"); expectIllegalArgumentException(() -> testFailingAddDataName1(), "Unexpected mutability: MUTABLE_OR_IMMUTABLE"); expectIllegalArgumentException(() -> testFailingAddDataName2(), "Unexpected weight: "); expectIllegalArgumentException(() -> testFailingAddDataName3(), "Unexpected weight: "); @@ -199,7 +229,8 @@ public class TestTemplate { expectIllegalArgumentException(() -> testFailingAddStructuralName1(), "Unexpected weight: "); expectIllegalArgumentException(() -> testFailingAddStructuralName2(), "Unexpected weight: "); expectIllegalArgumentException(() -> testFailingAddStructuralName3(), "Unexpected weight: "); - expectUnsupportedOperationException(() -> testFailingSample2(), "Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); + expectUnsupportedOperationException(() -> testFailingSample2a(), "Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); + expectUnsupportedOperationException(() -> testFailingSample2b(), "Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); expectRendererException(() -> testFailingAddNameDuplication1(), "Duplicate name:"); expectRendererException(() -> testFailingAddNameDuplication2(), "Duplicate name:"); expectRendererException(() -> testFailingAddNameDuplication3(), "Duplicate name:"); @@ -208,16 +239,23 @@ public class TestTemplate { expectRendererException(() -> testFailingAddNameDuplication6(), "Duplicate name:"); expectRendererException(() -> testFailingAddNameDuplication7(), "Duplicate name:"); expectRendererException(() -> testFailingAddNameDuplication8(), "Duplicate name:"); + expectRendererException(() -> testFailingScope1(), "Duplicate hashtag replacement for #x. previous: x1, new: x2"); + expectRendererException(() -> testFailingScope2(), "Duplicate hashtag replacement for #x. previous: x1, new: x2"); + expectRendererException(() -> testFailingScope3(), "Duplicate hashtag replacement for #x. previous: a, new: b"); + expectRendererException(() -> testFailingScope4(), "Duplicate hashtag replacement for #x. previous: a, new: b"); + expectRendererException(() -> testFailingScope5(), "Duplicate name:"); + expectRendererException(() -> testFailingScope6(), "Duplicate name:"); + expectRendererException(() -> testFailingScope7(), "Duplicate name:"); } public static void testSingleLine() { - var template = Template.make(() -> body("Hello World!")); + var template = Template.make(() -> scope("Hello World!")); String code = template.render(); checkEQ(code, "Hello World!"); } public static void testMultiLine() { - var template = Template.make(() -> body( + var template = Template.make(() -> scope( """ Code on more than a single line @@ -232,10 +270,10 @@ public class TestTemplate { checkEQ(code, expected); } - public static void testBodyTokens() { - // We can fill the body with Objects of different types, and they get concatenated. - // Lists get flattened into the body. - var template = Template.make(() -> body( + public static void testBasicTokens() { + // We can fill the scope with Objects of different types, and they get concatenated. + // Lists get flattened into the scope. + var template = Template.make(() -> scope( "start ", Integer.valueOf(1), 1, Long.valueOf(2), 2L, @@ -250,31 +288,31 @@ public class TestTemplate { public static void testWithOneArgument() { // Capture String argument via String name. - var template1 = Template.make("a", (String a) -> body("start #a end")); + var template1 = Template.make("a", (String a) -> scope("start #a end")); checkEQ(template1.render("x"), "start x end"); checkEQ(template1.render("a"), "start a end"); checkEQ(template1.render("" ), "start end"); // Capture String argument via typed lambda argument. - var template2 = Template.make("a", (String a) -> body("start ", a, " end")); + var template2 = Template.make("a", (String a) -> scope("start ", a, " end")); checkEQ(template2.render("x"), "start x end"); checkEQ(template2.render("a"), "start a end"); checkEQ(template2.render("" ), "start end"); // Capture Integer argument via String name. - var template3 = Template.make("a", (Integer a) -> body("start #a end")); + var template3 = Template.make("a", (Integer a) -> scope("start #a end")); checkEQ(template3.render(0 ), "start 0 end"); checkEQ(template3.render(22 ), "start 22 end"); checkEQ(template3.render(444), "start 444 end"); // Capture Integer argument via templated lambda argument. - var template4 = Template.make("a", (Integer a) -> body("start ", a, " end")); + var template4 = Template.make("a", (Integer a) -> scope("start ", a, " end")); checkEQ(template4.render(0 ), "start 0 end"); checkEQ(template4.render(22 ), "start 22 end"); checkEQ(template4.render(444), "start 444 end"); // Test Strings with backslashes: - var template5 = Template.make("a", (String a) -> body("start #a " + a + " end")); + var template5 = Template.make("a", (String a) -> scope("start #a " + a + " end")); checkEQ(template5.render("/"), "start / / end"); checkEQ(template5.render("\\"), "start \\ \\ end"); checkEQ(template5.render("\\\\"), "start \\\\ \\\\ end"); @@ -282,25 +320,25 @@ public class TestTemplate { public static void testWithTwoArguments() { // Capture 2 String arguments via String names. - var template1 = Template.make("a1", "a2", (String a1, String a2) -> body("start #a1 #a2 end")); + var template1 = Template.make("a1", "a2", (String a1, String a2) -> scope("start #a1 #a2 end")); checkEQ(template1.render("x", "y"), "start x y end"); checkEQ(template1.render("a", "b"), "start a b end"); checkEQ(template1.render("", "" ), "start end"); // Capture 2 String arguments via typed lambda arguments. - var template2 = Template.make("a1", "a2", (String a1, String a2) -> body("start ", a1, " ", a2, " end")); + var template2 = Template.make("a1", "a2", (String a1, String a2) -> scope("start ", a1, " ", a2, " end")); checkEQ(template2.render("x", "y"), "start x y end"); checkEQ(template2.render("a", "b"), "start a b end"); checkEQ(template2.render("", "" ), "start end"); // Capture 2 Integer arguments via String names. - var template3 = Template.make("a1", "a2", (Integer a1, Integer a2) -> body("start #a1 #a2 end")); + var template3 = Template.make("a1", "a2", (Integer a1, Integer a2) -> scope("start #a1 #a2 end")); checkEQ(template3.render(0, 1 ), "start 0 1 end"); checkEQ(template3.render(22, 33 ), "start 22 33 end"); checkEQ(template3.render(444, 555), "start 444 555 end"); // Capture 2 Integer arguments via templated lambda arguments. - var template4 = Template.make("a1", "a2", (Integer a1, Integer a2) -> body("start ", a1, " ", a2, " end")); + var template4 = Template.make("a1", "a2", (Integer a1, Integer a2) -> scope("start ", a1, " ", a2, " end")); checkEQ(template4.render(0, 1 ), "start 0 1 end"); checkEQ(template4.render(22, 33 ), "start 22 33 end"); checkEQ(template4.render(444, 555), "start 444 555 end"); @@ -308,46 +346,46 @@ public class TestTemplate { public static void testWithThreeArguments() { // Capture 3 String arguments via String names. - var template1 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> body("start #a1 #a2 #a3 end")); + var template1 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> scope("start #a1 #a2 #a3 end")); checkEQ(template1.render("x", "y", "z"), "start x y z end"); checkEQ(template1.render("a", "b", "c"), "start a b c end"); checkEQ(template1.render("", "", "" ), "start end"); // Capture 3 String arguments via typed lambda arguments. - var template2 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> body("start ", a1, " ", a2, " ", a3, " end")); + var template2 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> scope("start ", a1, " ", a2, " ", a3, " end")); checkEQ(template1.render("x", "y", "z"), "start x y z end"); checkEQ(template1.render("a", "b", "c"), "start a b c end"); checkEQ(template1.render("", "", "" ), "start end"); // Capture 3 Integer arguments via String names. - var template3 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> body("start #a1 #a2 #a3 end")); + var template3 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> scope("start #a1 #a2 #a3 end")); checkEQ(template3.render(0, 1 , 2 ), "start 0 1 2 end"); checkEQ(template3.render(22, 33 , 44 ), "start 22 33 44 end"); checkEQ(template3.render(444, 555, 666), "start 444 555 666 end"); // Capture 2 Integer arguments via templated lambda arguments. - var template4 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> body("start ", a1, " ", a2, " ", a3, " end")); + var template4 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> scope("start ", a1, " ", a2, " ", a3, " end")); checkEQ(template3.render(0, 1 , 2 ), "start 0 1 2 end"); checkEQ(template3.render(22, 33 , 44 ), "start 22 33 44 end"); checkEQ(template3.render(444, 555, 666), "start 444 555 666 end"); } - public static void testNested() { - var template1 = Template.make(() -> body("proton")); + public static void testNestedTemplates() { + var template1 = Template.make(() -> scope("proton")); - var template2 = Template.make("a1", "a2", (String a1, String a2) -> body( + var template2 = Template.make("a1", "a2", (String a1, String a2) -> scope( "electron #a1\n", "neutron #a2\n" )); - var template3 = Template.make("a1", "a2", (String a1, String a2) -> body( + var template3 = Template.make("a1", "a2", (String a1, String a2) -> scope( "Universe ", template1.asToken(), " {\n", template2.asToken("up", "down"), template2.asToken(a1, a2), "}\n" )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( template3.asToken("low", "high"), "{\n", template3.asToken("42", "24"), @@ -374,19 +412,19 @@ public class TestTemplate { checkEQ(code, expected); } - public static void testHookSimple() { + public static void testHookSimple1() { var hook1 = new Hook("Hook1"); - var template1 = Template.make(() -> body("Hello\n")); + var template1 = Template.make(() -> scope("Hello\n")); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "{\n", - hook1.anchor( + hook1.anchor(scope( "World\n", // Note: "Hello" from the template below will be inserted // above "World" above. hook1.insert(template1.asToken()) - ), + )), "}" )); @@ -400,21 +438,85 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testHookSimple2() { + var hook1 = new Hook("Hook1"); + + var template2 = Template.make(() -> scope( + "{\n", + hook1.anchor(scope( + "World\n", + // Note: "Hello" from the scope below will be inserted + // above "World" above. + hook1.insert(scope( + "Hello\n" + )) + )), + "}" + )); + + String code = template2.render(); + String expected = + """ + { + Hello + World + }"""; + checkEQ(code, expected); + } + + public static void testHookSimple3() { + var hook1 = new Hook("Hook1"); + + // Ensure that insert inside insert really goes first. + var template = Template.make(() -> scope( + "{\n", + hook1.anchor(scope( + "Outer Insert\n" + )), + ">Anchor\n" + )), + "}" + )); + + String code = template.render(); + String expected = + """ + { + Inner Insert + Outer Insert + Anchor + }"""; + checkEQ(code, expected); + } + public static void testHookIsAnchored() { var hook1 = new Hook("Hook1"); - var template0 = Template.make(() -> body("isAnchored: ", hook1.isAnchored(), "\n")); + var template0 = Template.make(() -> scope("t0 isAnchored: ", hook1.isAnchored(a -> scope(a)), "\n")); - var template1 = Template.make(() -> body("Hello\n", template0.asToken())); + var template1 = Template.make(() -> scope("Hello\n", template0.asToken())); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "{\n", + "t2 isAnchored: ", hook1.isAnchored(a -> scope(a)), "\n", template0.asToken(), - hook1.anchor( + hook1.anchor(scope( "World\n", + "t2 isAnchored: ", hook1.isAnchored(a -> scope(a)), "\n", template0.asToken(), - hook1.insert(template1.asToken()) - ), + hook1.insert(template1.asToken()), + hook1.insert(scope("Beautiful\n", template0.asToken())), + "t2 isAnchored: ", hook1.isAnchored(a -> scope(a)), "\n" + )), + "t2 isAnchored: ", hook1.isAnchored(a -> scope(a)), "\n", template0.asToken(), "}" )); @@ -423,12 +525,18 @@ public class TestTemplate { String expected = """ { - isAnchored: false + t2 isAnchored: false + t0 isAnchored: false Hello - isAnchored: true + t0 isAnchored: true + Beautiful + t0 isAnchored: true World - isAnchored: true - isAnchored: false + t2 isAnchored: true + t0 isAnchored: true + t2 isAnchored: true + t2 isAnchored: false + t0 isAnchored: false }"""; checkEQ(code, expected); } @@ -436,36 +544,41 @@ public class TestTemplate { public static void testHookNested() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("a", (String a) -> body("x #a x\n")); + var template1 = Template.make("a", (String a) -> scope("x #a x\n")); // Test nested use of hooks in the same template. - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "{\n", - hook1.anchor(), // empty + hook1.anchor(scope()), // empty "zero\n", - hook1.anchor( + hook1.anchor(scope( template1.asToken("one"), template1.asToken("two"), hook1.insert(template1.asToken("intoHook1a")), hook1.insert(template1.asToken("intoHook1b")), + hook1.insert(scope("y 1 y\n")), + hook1.insert(scope("y 2 y\n")), template1.asToken("three"), - hook1.anchor( + hook1.anchor(scope( template1.asToken("four"), hook1.insert(template1.asToken("intoHook1c")), + hook1.insert(scope("y 3 y\n")), template1.asToken("five") - ), + )), template1.asToken("six"), - hook1.anchor(), // empty + hook1.anchor(scope()), // empty template1.asToken("seven"), hook1.insert(template1.asToken("intoHook1d")), + hook1.insert(scope("y 4 y\n")), template1.asToken("eight"), - hook1.anchor( + hook1.anchor(scope( template1.asToken("nine"), hook1.insert(template1.asToken("intoHook1e")), + hook1.insert(scope("y 5 y\n")), template1.asToken("ten") - ), + )), template1.asToken("eleven") - ), + )), "}" )); @@ -476,17 +589,22 @@ public class TestTemplate { zero x intoHook1a x x intoHook1b x + y 1 y + y 2 y x intoHook1d x + y 4 y x one x x two x x three x x intoHook1c x + y 3 y x four x x five x x six x x seven x x eight x x intoHook1e x + y 5 y x nine x x ten x x eleven x @@ -498,30 +616,30 @@ public class TestTemplate { var hook1 = new Hook("Hook1"); var hook2 = new Hook("Hook2"); - var template1 = Template.make("a", (String a) -> body("x #a x\n")); + var template1 = Template.make("a", (String a) -> scope("x #a x\n")); - var template2 = Template.make("b", (String b) -> body( + var template2 = Template.make("b", (String b) -> scope( "{\n", template1.asToken(b + "A"), hook1.insert(template1.asToken(b + "B")), hook2.insert(template1.asToken(b + "C")), template1.asToken(b + "D"), - hook1.anchor( + hook1.anchor(scope( template1.asToken(b + "E"), hook1.insert(template1.asToken(b + "F")), hook2.insert(template1.asToken(b + "G")), template1.asToken(b + "H"), - hook2.anchor( + hook2.anchor(scope( template1.asToken(b + "I"), hook1.insert(template1.asToken(b + "J")), hook2.insert(template1.asToken(b + "K")), template1.asToken(b + "L") - ), + )), template1.asToken(b + "M"), hook1.insert(template1.asToken(b + "N")), hook2.insert(template1.asToken(b + "O")), template1.asToken(b + "O") - ), + )), template1.asToken(b + "P"), hook1.insert(template1.asToken(b + "Q")), hook2.insert(template1.asToken(b + "R")), @@ -530,18 +648,18 @@ public class TestTemplate { )); // Test use of hooks across templates. - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "{\n", "base-A\n", - hook1.anchor( + hook1.anchor(scope( "base-B\n", - hook2.anchor( + hook2.anchor(scope( "base-C\n", template2.asToken("sub-"), "base-D\n" - ), + )), "base-E\n" - ), + )), "base-F\n", "}\n" )); @@ -586,32 +704,32 @@ public class TestTemplate { public static void testHookRecursion() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("a", (String a) -> body("x #a x\n")); + var template1 = Template.make("a", (String a) -> scope("x #a x\n")); - var template2 = Template.make("b", (String b) -> body( + var template2 = Template.make("b", (String b) -> scope( "<\n", template1.asToken(b + "A"), hook1.insert(template1.asToken(b + "B")), // sub-B is rendered before template2. template1.asToken(b + "C"), "inner-hook-start\n", - hook1.anchor( + hook1.anchor(scope( "inner-hook-end\n", template1.asToken(b + "E"), hook1.insert(template1.asToken(b + "E")), template1.asToken(b + "F") - ), + )), ">\n" )); // Test use of hooks across templates. - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "{\n", "hook-start\n", - hook1.anchor( + hook1.anchor(scope( "hook-end\n", hook1.insert(template2.asToken("sub-")), "base-C\n" - ), + )), "base-D\n", "}\n" )); @@ -642,16 +760,16 @@ public class TestTemplate { public static void testDollar() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("a", (String a) -> body("x $name #a x\n")); + var template1 = Template.make("a", (String a) -> scope("x $name #a x\n")); - var template2 = Template.make("a", (String a) -> body( + var template2 = Template.make("a", (String a) -> scope( "{\n", "y $name #a y\n", template1.asToken($("name")), "}\n" )); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "{\n", "$name\n", "$name", "\n", @@ -660,11 +778,11 @@ public class TestTemplate { template1.asToken("name"), // does not capture -> literal "$name" template1.asToken("$name"), // does not capture -> literal "$name" template1.asToken($("name")), // capture replacement name "name_1" - hook1.anchor( + hook1.anchor(scope( "$name\n" - ), + )), "break\n", - hook1.anchor( + hook1.anchor(scope( "one\n", hook1.insert(template1.asToken($("name"))), "two\n", @@ -672,7 +790,7 @@ public class TestTemplate { "three\n", hook1.insert(template2.asToken($("name"))), "four\n" - ), + )), "}\n" )); @@ -704,10 +822,10 @@ public class TestTemplate { checkEQ(code, expected); } - public static void testLet() { + public static void testLet1() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("a", (String a) -> body( + var template1 = Template.make("a", (String a) -> scope( "{\n", "y #a y\n", let("b", "<" + a + ">"), @@ -715,25 +833,25 @@ public class TestTemplate { "}\n" )); - var template2 = Template.make("a", (Integer a) -> let("b", a * 10, b -> - body( + var template2 = Template.make("a", (Integer a) -> scope( + let("b", a * 10, b -> scope( let("c", b * 3), "abc = #a #b #c\n" - ) + )) )); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "{\n", let("x", "abc"), template1.asToken("alpha"), "break\n", "x1 = #x\n", - hook1.anchor( + hook1.anchor(transparentScope( // transparentScope allows hashtags to escape "x2 = #x\n", // leaks inside template1.asToken("beta"), let("y", "one"), "y1 = #y\n" - ), + )), "break\n", "y2 = #y\n", // leaks outside "break\n", @@ -766,8 +884,30 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testLet2() { + var template = Template.make(() -> scope( + "outer {\n", + let("x", "x1", x -> scope( + "x: #x ", x, ".\n" + )), + let("x", "x2"), // definition above is limited to its scope + "x: #x\n", + "} outer\n" + )); + + String code = template.render(); + String expected = + """ + outer { + x: x1 x1. + x: x2 + } outer + """; + checkEQ(code, expected); + } + public static void testDollarAndHashtagBrackets() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("xyz", "abc"), let("xyz_", "def"), let("xyz_klm", "ghi"), @@ -792,19 +932,19 @@ public class TestTemplate { } public static void testSelector() { - var template1 = Template.make("a", (String a) -> body( + var template1 = Template.make("a", (String a) -> scope( "<\n", "x #a x\n", ">\n" )); - var template2 = Template.make("a", (String a) -> body( + var template2 = Template.make("a", (String a) -> scope( "<\n", "y #a y\n", ">\n" )); - var template3 = Template.make("a", (Integer a) -> body( + var template3 = Template.make("a", (Integer a) -> scope( "[\n", "z #a z\n", // Select which template should be used: @@ -813,7 +953,7 @@ public class TestTemplate { "]\n" )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( "{\n", template3.asToken(-1), "break\n", @@ -865,7 +1005,7 @@ public class TestTemplate { // Binding allows use of template1 inside template1, via the Binding indirection. var binding1 = new TemplateBinding>(); - var template1 = Template.make("i", (Integer i) -> body( + var template1 = Template.make("i", (Integer i) -> scope( "[ #i\n", // We cannot yet use the template1 directly, as it is being defined. // So we use binding1 instead. @@ -874,7 +1014,7 @@ public class TestTemplate { )); binding1.bind(template1); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "{\n", // Now, we can use template1 normally, as it is already defined. template1.asToken(3), @@ -902,7 +1042,7 @@ public class TestTemplate { } public static void testFuel() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("f", fuel()), "<#f>\n" @@ -910,7 +1050,7 @@ public class TestTemplate { // Binding allows use of template2 inside template2, via the Binding indirection. var binding2 = new TemplateBinding>(); - var template2 = Template.make("i", (Integer i) -> body( + var template2 = Template.make("i", (Integer i) -> scope( let("f", fuel()), "[ #i #f\n", @@ -920,7 +1060,7 @@ public class TestTemplate { )); binding2.bind(template2); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "{\n", template2.asToken(3), "}\n" @@ -948,7 +1088,7 @@ public class TestTemplate { } public static void testFuelCustom() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( setFuelCost(2.0f), let("f", fuel()), @@ -957,7 +1097,7 @@ public class TestTemplate { // Binding allows use of template2 inside template2, via the Binding indirection. var binding2 = new TemplateBinding>(); - var template2 = Template.make("i", (Integer i) -> body( + var template2 = Template.make("i", (Integer i) -> scope( setFuelCost(3.0f), let("f", fuel()), @@ -968,7 +1108,7 @@ public class TestTemplate { )); binding2.bind(template2); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( setFuelCost(5.0f), let("f", fuel()), @@ -1002,46 +1142,277 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testFuelAndScopes() { + var readFuelTemplate = Template.make(() -> scope( + let("f", fuel()), + "<#f>\n" + )); + + var template = Template.make(() -> scope( + let("f", fuel()), + "{#f}\n", + readFuelTemplate.asToken(), + + "scope:\n", + setFuelCost(1.0f), + scope( + readFuelTemplate.asToken(), + setFuelCost(2.0f), + readFuelTemplate.asToken() + ), + readFuelTemplate.asToken(), + + "transparentScope:\n", + setFuelCost(4.0f), + transparentScope( + readFuelTemplate.asToken(), + setFuelCost(8.0f), + readFuelTemplate.asToken() + ), + readFuelTemplate.asToken(), + + "nameScope:\n", + setFuelCost(16.0f), + nameScope( + readFuelTemplate.asToken(), + setFuelCost(32.0f), + readFuelTemplate.asToken() + ), + readFuelTemplate.asToken(), + + "hashtagScope:\n", + setFuelCost(64.0f), + hashtagScope( + readFuelTemplate.asToken(), + setFuelCost(128.0f), + readFuelTemplate.asToken() + ), + readFuelTemplate.asToken(), + + "setFuelCostScope:\n", + setFuelCost(256.0f), + setFuelCostScope( + readFuelTemplate.asToken(), + setFuelCost(512.0f), + readFuelTemplate.asToken() + ), + readFuelTemplate.asToken() + )); + + String code = template.render(1000.0f); + String expected = + """ + {1000.0f} + <990.0f> + scope: + <999.0f> + <998.0f> + <999.0f> + transparentScope: + <996.0f> + <992.0f> + <992.0f> + nameScope: + <984.0f> + <968.0f> + <968.0f> + hashtagScope: + <936.0f> + <872.0f> + <872.0f> + setFuelCostScope: + <744.0f> + <488.0f> + <744.0f> + """; + checkEQ(code, expected); + } + + public static void testDataNames0a() { + var template = Template.make(() -> scope( + // When a DataName is added, it is immediately available afterwards. + // This may seem trivial, but it requires that either both "add" and + // "sample" happen in lambda execution, or in token evaluation. + // Otherwise, one can float above the other, and lead to unintuitive + // behavior. + addDataName("x", myInt, MUTABLE), + dataNames(MUTABLE).exactOf(myInt).sampleAndLetAs("v"), + "sample: #v." + )); + + String code = template.render(); + checkEQ(code, "sample: x."); + } + + public static void testDataNames0b() { + // Test that the scope keeps local DataNames only for the scope, but that + // we can see DataNames of outer scopes. + var template = Template.make(() -> scope( + // Outer scope DataName: + addDataName("outerInt", myInt, MUTABLE), + dataNames(MUTABLE).exactOf(myInt).sample((DataName dn) -> scope( + let("name1", dn.name()), + "sample: #name1.\n", + // We can also see the outer DataName: + dataNames(MUTABLE).exactOf(myInt).sampleAndLetAs("name2"), + "sample: #name2.\n", + // Local DataName: + addDataName("innerLong", myLong, MUTABLE), + dataNames(MUTABLE).exactOf(myLong).sampleAndLetAs("name3"), + "sample: #name3.\n" + )), + // We can still see the outer scope DataName: + dataNames(MUTABLE).exactOf(myInt).sampleAndLetAs("name4"), + "sample: #name4.\n", + // But we cannot see the DataNames that are local to the inner scope. + // So here, we will always see "outerLong", and never "innerLong". + addDataName("outerLong", myLong, MUTABLE), + dataNames(MUTABLE).exactOf(myLong).sampleAndLetAs("name5"), + "sample: #name5.\n" + )); + + String code = template.render(); + String expected = + """ + sample: outerInt. + sample: outerInt. + sample: innerLong. + sample: outerInt. + sample: outerLong. + """; + checkEQ(code, expected); + } + + public static void testDataNames0c() { + // Test that hashtag replacements that are local to inner scopes are + // only visible to inner scopes, but dollar replacements are the same + // for the whole Template. + var template = Template.make(() -> scope( + let("global", "GLOBAL"), + "g: #global. $a\n", + // Create a dummy DataName so we don't get an exception from sample. + addDataName("x", myInt, MUTABLE), + dataNames(MUTABLE).exactOf(myInt).sample((DataName dn) -> scope( + "g: #global. $b\n", + let("local", "LOCAL1"), + "l: #local. $c\n" + )), + "g: #global. $d\n", + // Open the scope again just to see if we can create the local again there. + dataNames(MUTABLE).exactOf(myInt).sample((DataName dn) -> scope( + "g: #global. $e\n", + let("local", "LOCAL2"), + "l: #local. $f\n" + )), + // We can now use the "local" hashtag replacement again, since it + // was previously only defined in an inner scope. + let("local", "LOCAL3"), + "g: #global. $g\n", + "l: #local. $h\n" + )); + + String code = template.render(); + String expected = + """ + g: GLOBAL. a_1 + g: GLOBAL. b_1 + l: LOCAL1. c_1 + g: GLOBAL. d_1 + g: GLOBAL. e_1 + l: LOCAL2. f_1 + g: GLOBAL. g_1 + l: LOCAL3. h_1 + """; + checkEQ(code, expected); + } + + public static void testDataNames0d() { + var template = Template.make(() -> scope( + addDataName("x", myInt, MUTABLE), + addDataName("y", myInt, MUTABLE), + addDataName("z", myInt, MUTABLE), + addDataName("a", myLong, MUTABLE), + addDataName("b", myLong, MUTABLE), + addDataName("c", myLong, MUTABLE), + dataNames(MUTABLE).exactOf(myInt).forEach((DataName dn) -> scope( + let("name", dn.name()), + let("type", dn.type()), + "listI: #name #type.\n" + )), + dataNames(MUTABLE).exactOf(myLong).forEach((DataName dn) -> scope( + let("name", dn.name()), + let("type", dn.type()), + "listL: #name #type.\n" + )) + )); + + String code = template.render(); + String expected = + """ + listI: x int. + listI: y int. + listI: z int. + listL: a long. + listL: b long. + listL: c long. + """; + checkEQ(code, expected); + } public static void testDataNames1() { var hook1 = new Hook("Hook1"); - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "[", - dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).hasAny(), + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).hasAny(h -> scope(h)), ", ", - dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).count(), + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).count(c -> scope(c)), ", names: {", - String.join(", ", dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).toList().stream().map(DataName::name).toList()), + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), "}]\n" )); - var template2 = Template.make("name", "type", (String name, DataName.Type type) -> body( - addDataName(name, type, MUTABLE), + // Note: the scope of the template must be transparentScope, so that the addDataName can escape. + var template2 = Template.make("name", "type", (String name, DataName.Type type) -> transparentScope( + addDataName(name, type, MUTABLE), // escapes "define #type #name\n", template1.asToken() )); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( "<\n", hook1.insert(template2.asToken($("name"), myInt)), "$name = 5\n", ">\n" )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( "{\n", template1.asToken(), - hook1.anchor( + hook1.anchor(scope( template1.asToken(), "something\n", - template3.asToken(), + template3.asToken(), // name_4 is inserted to hook1 "more\n", template1.asToken(), "more\n", - template2.asToken($("name"), myInt), + template2.asToken($("name"), myInt), // name_1 escapes "more\n", + template1.asToken(), + "extra\n", + hook1.insert(scope( + addDataName($("extra1"), myInt, MUTABLE), // does not escape + "$extra1 = 666\n" + )), + hook1.insert(transparentScope( + addDataName($("extra2"), myInt, MUTABLE), // escapes + "$extra2 = 42\n" + )), template1.asToken() - ), + )), + // But no names escape to down here, because the anchor scope is "scope". + "final:\n", template1.asToken(), "}\n" )); @@ -1053,6 +1424,8 @@ public class TestTemplate { [false, 0, names: {}] define int name_4 [true, 1, names: {name_4}] + extra1_1 = 666 + extra2_1 = 42 [false, 0, names: {}] something < @@ -1064,7 +1437,10 @@ public class TestTemplate { define int name_1 [true, 2, names: {name_4, name_1}] more - [true, 1, names: {name_4}] + [true, 2, names: {name_4, name_1}] + extra + [true, 3, names: {name_4, extra2_1, name_1}] + final: [false, 0, names: {}] } """; @@ -1074,17 +1450,19 @@ public class TestTemplate { public static void testDataNames2() { var hook1 = new Hook("Hook1"); - var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> body( + var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> scope( " #mutability: [", - dataNames(mutability).exactOf(myInt).hasAny(), + dataNames(mutability).exactOf(myInt).hasAny(h -> scope(h)), ", ", - dataNames(mutability).exactOf(myInt).count(), + dataNames(mutability).exactOf(myInt).count(c -> scope(c)), ", names: {", - String.join(", ", dataNames(mutability).exactOf(myInt).toList().stream().map(DataName::name).toList()), + dataNames(mutability).exactOf(myInt).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), "}]\n" )); - var template1 = Template.make("type", (DataName.Type type) -> body( + var template1 = Template.make("type", (DataName.Type type) -> scope( "[#type:\n", template0.asToken(type, MUTABLE), template0.asToken(type, IMMUTABLE), @@ -1092,51 +1470,51 @@ public class TestTemplate { "]\n" )); - var template2 = Template.make("name", "type", (String name, DataName.Type type) -> body( - addDataName(name, type, MUTABLE), + var template2 = Template.make("name", "type", (String name, DataName.Type type) -> transparentScope( + addDataName(name, type, MUTABLE), // escapes "define mutable #type #name\n", template1.asToken(type) )); - var template3 = Template.make("name", "type", (String name, DataName.Type type) -> body( - addDataName(name, type, IMMUTABLE), + var template3 = Template.make("name", "type", (String name, DataName.Type type) -> transparentScope( + addDataName(name, type, IMMUTABLE), // escapes "define immutable #type #name\n", template1.asToken(type) )); - var template4 = Template.make("type", (DataName.Type type) -> body( + var template4 = Template.make("type", (DataName.Type type) -> scope( "{ $store\n", hook1.insert(template2.asToken($("name"), type)), "$name = 5\n", "} $store\n" )); - var template5 = Template.make("type", (DataName.Type type) -> body( + var template5 = Template.make("type", (DataName.Type type) -> scope( "{ $load\n", hook1.insert(template3.asToken($("name"), type)), "blackhole($name)\n", "} $load\n" )); - var template6 = Template.make("type", (DataName.Type type) -> body( - let("v", dataNames(MUTABLE).exactOf(type).sample().name()), + var template6 = Template.make("type", (DataName.Type type) -> scope( + dataNames(MUTABLE).exactOf(type).sampleAndLetAs("v"), "{ $sample\n", "#v = 7\n", "} $sample\n" )); - var template7 = Template.make("type", (DataName.Type type) -> body( - let("v", dataNames(MUTABLE_OR_IMMUTABLE).exactOf(type).sample().name()), + var template7 = Template.make("type", (DataName.Type type) -> scope( + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(type).sampleAndLetAs("v"), "{ $sample\n", "blackhole(#v)\n", "} $sample\n" )); - var template8 = Template.make(() -> body( + var template8 = Template.make(() -> scope( "class $X {\n", template1.asToken(myInt), - hook1.anchor( - "begin $body\n", + hook1.anchor(scope( + "begin $scope\n", template1.asToken(myInt), "start with immutable\n", template5.asToken(myInt), @@ -1148,7 +1526,7 @@ public class TestTemplate { "then store to it\n", template6.asToken(myInt), template1.asToken(myInt) - ), + )), template1.asToken(myInt), "}\n" )); @@ -1174,7 +1552,7 @@ public class TestTemplate { IMMUTABLE: [true, 1, names: {name_10}] MUTABLE_OR_IMMUTABLE: [true, 2, names: {name_10, name_21}] ] - begin body_1 + begin scope_1 [int: MUTABLE: [false, 0, names: {}] IMMUTABLE: [false, 0, names: {}] @@ -1219,17 +1597,19 @@ public class TestTemplate { public static void testDataNames3() { var hook1 = new Hook("Hook1"); - var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> body( + var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> scope( " #mutability: [", - dataNames(mutability).exactOf(myInt).hasAny(), + dataNames(mutability).exactOf(myInt).hasAny(h -> scope(h)), ", ", - dataNames(mutability).exactOf(myInt).count(), + dataNames(mutability).exactOf(myInt).count(c -> scope(c)), ", names: {", - String.join(", ", dataNames(mutability).exactOf(myInt).toList().stream().map(DataName::name).toList()), + dataNames(mutability).exactOf(myInt).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), "}]\n" )); - var template1 = Template.make("type", (DataName.Type type) -> body( + var template1 = Template.make("type", (DataName.Type type) -> scope( "[#type:\n", template0.asToken(type, MUTABLE), template0.asToken(type, IMMUTABLE), @@ -1237,11 +1617,11 @@ public class TestTemplate { "]\n" )); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "class $Y {\n", template1.asToken(myInt), - hook1.anchor( - "begin $body\n", + hook1.anchor(scope( + "begin $scope\n", template1.asToken(myInt), "define mutable $v1\n", addDataName($("v1"), myInt, MUTABLE), @@ -1249,7 +1629,7 @@ public class TestTemplate { "define immutable $v2\n", addDataName($("v2"), myInt, IMMUTABLE), template1.asToken(myInt) - ), + )), template1.asToken(myInt), "}\n" )); @@ -1263,7 +1643,7 @@ public class TestTemplate { IMMUTABLE: [false, 0, names: {}] MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] ] - begin body_1 + begin scope_1 [int: MUTABLE: [false, 0, names: {}] IMMUTABLE: [false, 0, names: {}] @@ -1294,47 +1674,62 @@ public class TestTemplate { public static void testDataNames4() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("type", (DataName.Type type) -> body( + var template1 = Template.make("type", (DataName.Type type) -> scope( "[#type:\n", " exact: ", - dataNames(MUTABLE).exactOf(type).hasAny(), + dataNames(MUTABLE).exactOf(type).hasAny(h -> scope(h)), ", ", - dataNames(MUTABLE).exactOf(type).count(), + dataNames(MUTABLE).exactOf(type).count(c -> scope(c)), ", {", - String.join(", ", dataNames(MUTABLE).exactOf(type).toList().stream().map(DataName::name).toList()), + dataNames(MUTABLE).exactOf(type).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), "}\n", " subtype: ", - dataNames(MUTABLE).subtypeOf(type).hasAny(), + dataNames(MUTABLE).subtypeOf(type).hasAny(h -> scope(h)), ", ", - dataNames(MUTABLE).subtypeOf(type).count(), + dataNames(MUTABLE).subtypeOf(type).count(c -> scope(c)), ", {", - String.join(", ", dataNames(MUTABLE).subtypeOf(type).toList().stream().map(DataName::name).toList()), + dataNames(MUTABLE).subtypeOf(type).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), + "}\n", " supertype: ", - dataNames(MUTABLE).supertypeOf(type).hasAny(), + dataNames(MUTABLE).supertypeOf(type).hasAny(h -> scope(h)), ", ", - dataNames(MUTABLE).supertypeOf(type).count(), + dataNames(MUTABLE).supertypeOf(type).count(c -> scope(c)), ", {", - String.join(", ", dataNames(MUTABLE).supertypeOf(type).toList().stream().map(DataName::name).toList()), + dataNames(MUTABLE).supertypeOf(type).toList(list -> scope( + String.join(", ", list.stream().map(DataName::name).toList()) + )), "}\n", "]\n" )); List types = List.of(myClassA, myClassA1, myClassA2, myClassA11, myClassB); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "DataNames:\n", types.stream().map(t -> template1.asToken(t)).toList() )); - var template3 = Template.make("type", (DataName.Type type) -> body( - let("name", dataNames(MUTABLE).subtypeOf(type).sample()), - "Sample #type: #name\n" + var template3 = Template.make("type", (DataName.Type type) -> scope( + dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name1"), + "Sample #type: #name1\n", + dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name2", "type2"), + "Sample #type: #name2 #type2\n", + dataNames(MUTABLE).subtypeOf(type).sample((DataName dn) -> scope( + let("name3", dn.name()), + let("type3", dn.type()), + let("dn", dn), // format the whole DataName with toString + "Sample #type: #name3 #type3 #dn\n" + )) )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( "class $W {\n", template2.asToken(), - hook1.anchor( + hook1.anchor(scope( "Create name for myClassA11, should be visible for the super classes\n", addDataName($("v1"), myClassA11, MUTABLE), template3.asToken(myClassA11), @@ -1345,7 +1740,7 @@ public class TestTemplate { template3.asToken(myClassA11), template3.asToken(myClassA1), template2.asToken() - ), + )), template2.asToken(), "}\n" )); @@ -1381,12 +1776,22 @@ public class TestTemplate { supertype: false, 0, {} ] Create name for myClassA11, should be visible for the super classes - Sample myClassA11: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] - Sample myClassA1: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] - Sample myClassA: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA11: v1_1 + Sample myClassA11: v1_1 myClassA11 + Sample myClassA11: v1_1 myClassA11 DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA1: v1_1 + Sample myClassA1: v1_1 myClassA11 + Sample myClassA1: v1_1 myClassA11 DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA: v1_1 + Sample myClassA: v1_1 myClassA11 + Sample myClassA: v1_1 myClassA11 DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] Create name for myClassA, should never be visible for the sub classes - Sample myClassA11: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] - Sample myClassA1: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA11: v1_1 + Sample myClassA11: v1_1 myClassA11 + Sample myClassA11: v1_1 myClassA11 DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA1: v1_1 + Sample myClassA1: v1_1 myClassA11 + Sample myClassA1: v1_1 myClassA11 DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] DataNames: [myClassA: exact: true, 1, {v2_1} @@ -1449,49 +1854,61 @@ public class TestTemplate { var hook1 = new Hook("Hook1"); var hook2 = new Hook("Hook2"); - // It is safe in separate Hook scopes. - var template1 = Template.make(() -> body( - hook1.anchor( + // It is safe in separate scopes. + var template1 = Template.make(() -> scope( + scope( addDataName("name1", myInt, MUTABLE) ), - hook1.anchor( + scope( addDataName("name1", myInt, MUTABLE) - ) + ), + nameScope( + addDataName("name1", myInt, MUTABLE) + ), + nameScope( + addDataName("name1", myInt, MUTABLE) + ), + hook1.anchor(scope( + addDataName("name1", myInt, MUTABLE) + )), + hook1.anchor(scope( + addDataName("name1", myInt, MUTABLE) + )) )); // It is safe in separate Template scopes. - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( addDataName("name2", myInt, MUTABLE) )); - var template3 = Template.make(() -> body( + var template3 = Template.make(() -> scope( template2.asToken(), template2.asToken() )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( // The following is not safe, it would collide // with (1), because it would be inserted to the // hook1.anchor in template5, and hence be available // inside the scope where (1) is available. // See: testFailingAddNameDuplication8 // addDataName("name", myInt, MUTABLE), - hook2.anchor( + hook2.anchor(scope( // (2) This one is added second. Since it is // inside the hook2.anchor, it does not go // out to the hook1.anchor, and is not // available inside the scope of (1). addDataName("name3", myInt, MUTABLE) - ) + )) )); - var template5 = Template.make(() -> body( - hook1.anchor( + var template5 = Template.make(() -> scope( + hook1.anchor(scope( // (1) this is the first one we add. addDataName("name3", myInt, MUTABLE) - ) + )) )); // Put it all together into a single test. - var template6 = Template.make(() -> body( + var template6 = Template.make(() -> scope( template1.asToken(), template3.asToken(), template5.asToken() @@ -1502,31 +1919,128 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testDataNames6() { + var template = Template.make(() -> scope( + addDataName("x", myInt, IMMUTABLE), + "int x = 5;\n", + // A DataName can be captured, and used to define a new one with the same type. + // It is important that the new DataName can escape the hashtagScope, so we have + // access to it later. + // Using "scope", it does not escape. + dataNames(IMMUTABLE).exactOf(myInt).sample(dn -> scope( + addDataName("a", dn.type(), MUTABLE), + let("v1", "a"), + "int #v1 = x + 1;\n" + )), + // Using "transparentScope", it is available. + dataNames(IMMUTABLE).exactOf(myInt).sample(dn -> transparentScope( + addDataName("b", dn.type(), MUTABLE), + let("v2", "b"), + "int #v2 = x + 2;\n" + )), + // Using "nameScope", it does not escape. + dataNames(IMMUTABLE).exactOf(myInt).sample(dn -> nameScope( + addDataName("c", dn.type(), MUTABLE), + let("v3", "c"), + "int #v3 = x + 3;\n" + )), + // Using "hashtagScope", it is available. + dataNames(IMMUTABLE).exactOf(myInt).sample(dn -> hashtagScope( + addDataName("d", dn.type(), MUTABLE), + let("v4", "d"), + "int #v4 = x + 4;\n" + )), + // Using "setFuelCostScope", it is available. + dataNames(IMMUTABLE).exactOf(myInt).sample(dn -> setFuelCostScope( + addDataName("e", dn.type(), MUTABLE), + let("v5", "e"), + "int #v5 = x + 5;\n" + )), + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).forEach("name", "type", dn -> scope( + "available1: #name #type.\n" + )), + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).forEach("name", "type", dn -> hashtagScope( + "available2: #name #type.\n" + )), + // Check that hashtags escape correctly too. + "hashtag v2: #v2.\n", + "hashtag v3: #v3.\n", + "hashtag v5: #v5.\n", + let("v1", "aaa"), + let("v4", "ddd") + )); + + String code = template.render(); + String expected = + """ + int x = 5; + int a = x + 1; + int b = x + 2; + int c = x + 3; + int d = x + 4; + int e = x + 5; + available1: x int. + available1: b int. + available1: d int. + available1: e int. + available2: x int. + available2: b int. + available2: d int. + available2: e int. + hashtag v2: b. + hashtag v3: c. + hashtag v5: e. + """; + checkEQ(code, expected); + } + + public static void testStructuralNames0() { + var template = Template.make(() -> scope( + // When a StructuralName is added, it is immediately available afterwards. + // This may seem trivial, but it requires that either both "add" and + // "sample" happen in lambda execution, or in token evaluation. + // Otherwise, one can float above the other, and lead to unintuitive + // behavior. + addStructuralName("x", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).sampleAndLetAs("v"), + "sample: #v." + )); + + String code = template.render(); + checkEQ(code, "sample: x."); + } + public static void testStructuralNames1() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("type", (StructuralName.Type type) -> body( + var template1 = Template.make("type", (StructuralName.Type type) -> scope( "[#type:\n", " exact: ", - structuralNames().exactOf(type).hasAny(), + structuralNames().exactOf(type).hasAny(h -> scope(h)), ", ", - structuralNames().exactOf(type).count(), + structuralNames().exactOf(type).count(c -> scope(c)), ", {", - String.join(", ", structuralNames().exactOf(type).toList().stream().map(StructuralName::name).toList()), + structuralNames().exactOf(type).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), "}\n", " subtype: ", - structuralNames().subtypeOf(type).hasAny(), + structuralNames().subtypeOf(type).hasAny(h -> scope(h)), ", ", - structuralNames().subtypeOf(type).count(), + structuralNames().subtypeOf(type).count(c -> scope(c)), ", {", - String.join(", ", structuralNames().subtypeOf(type).toList().stream().map(StructuralName::name).toList()), + structuralNames().subtypeOf(type).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), "}\n", " supertype: ", - structuralNames().supertypeOf(type).hasAny(), + structuralNames().supertypeOf(type).hasAny(h -> scope(h)), ", ", - structuralNames().supertypeOf(type).count(), + structuralNames().supertypeOf(type).count(c -> scope(c)), ", {", - String.join(", ", structuralNames().supertypeOf(type).toList().stream().map(StructuralName::name).toList()), + structuralNames().supertypeOf(type).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), "}\n", "]\n" )); @@ -1536,20 +2050,28 @@ public class TestTemplate { myStructuralTypeA2, myStructuralTypeA11, myStructuralTypeB); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "StructuralNames:\n", types.stream().map(t -> template1.asToken(t)).toList() )); - var template3 = Template.make("type", (StructuralName.Type type) -> body( - let("name", structuralNames().subtypeOf(type).sample()), - "Sample #type: #name\n" + var template3 = Template.make("type", (StructuralName.Type type) -> scope( + structuralNames().subtypeOf(type).sampleAndLetAs("name1"), + "Sample #type: #name1\n", + structuralNames().subtypeOf(type).sampleAndLetAs("name2", "type2"), + "Sample #type: #name2 #type2\n", + structuralNames().subtypeOf(type).sample((StructuralName sn) -> scope( + let("name3", sn.name()), + let("type3", sn.type()), + let("sn", sn), // format the whole StructuralName with toString + "Sample #type: #name3 #type3 #sn\n" + )) )); - var template4 = Template.make(() -> body( + var template4 = Template.make(() -> scope( "class $Q {\n", template2.asToken(), - hook1.anchor( + hook1.anchor(scope( "Create name for myStructuralTypeA11, should be visible for the supertypes\n", addStructuralName($("v1"), myStructuralTypeA11), template3.asToken(myStructuralTypeA11), @@ -1560,7 +2082,7 @@ public class TestTemplate { template3.asToken(myStructuralTypeA11), template3.asToken(myStructuralTypeA1), template2.asToken() - ), + )), template2.asToken(), "}\n" )); @@ -1596,12 +2118,22 @@ public class TestTemplate { supertype: false, 0, {} ] Create name for myStructuralTypeA11, should be visible for the supertypes - Sample StructuralA11: StructuralName[name=v1_1, type=StructuralA11, weight=1] - Sample StructuralA1: StructuralName[name=v1_1, type=StructuralA11, weight=1] - Sample StructuralA: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA11: v1_1 + Sample StructuralA11: v1_1 StructuralA11 + Sample StructuralA11: v1_1 StructuralA11 StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA1: v1_1 + Sample StructuralA1: v1_1 StructuralA11 + Sample StructuralA1: v1_1 StructuralA11 StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA: v1_1 + Sample StructuralA: v1_1 StructuralA11 + Sample StructuralA: v1_1 StructuralA11 StructuralName[name=v1_1, type=StructuralA11, weight=1] Create name for myStructuralTypeA, should never be visible for the subtypes - Sample StructuralA11: StructuralName[name=v1_1, type=StructuralA11, weight=1] - Sample StructuralA1: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA11: v1_1 + Sample StructuralA11: v1_1 StructuralA11 + Sample StructuralA11: v1_1 StructuralA11 StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA1: v1_1 + Sample StructuralA1: v1_1 StructuralA11 + Sample StructuralA1: v1_1 StructuralA11 StructuralName[name=v1_1, type=StructuralA11, weight=1] StructuralNames: [StructuralA: exact: true, 1, {v2_1} @@ -1662,41 +2194,43 @@ public class TestTemplate { public static void testStructuralNames2() { var hook1 = new Hook("Hook1"); - var template1 = Template.make("type", (StructuralName.Type type) -> body( + var template1 = Template.make("type", (StructuralName.Type type) -> scope( "[#type: ", - structuralNames().exactOf(type).hasAny(), + structuralNames().exactOf(type).hasAny(h -> scope(h)), ", ", - structuralNames().exactOf(type).count(), + structuralNames().exactOf(type).count(c -> scope(c)), ", names: {", - String.join(", ", structuralNames().exactOf(type).toList().stream().map(StructuralName::name).toList()), + structuralNames().exactOf(type).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), "}]\n" )); - var template2 = Template.make("name", "type", (String name, StructuralName.Type type) -> body( - addStructuralName(name, type), + var template2 = Template.make("name", "type", (String name, StructuralName.Type type) -> transparentScope( + addStructuralName(name, type), // escapes "define #type #name\n" )); - var template3 = Template.make("type", (StructuralName.Type type) -> body( + var template3 = Template.make("type", (StructuralName.Type type) -> scope( "{ $access\n", hook1.insert(template2.asToken($("name"), type)), "$name = 5\n", "} $access\n" )); - var template4 = Template.make("type", (StructuralName.Type type) -> body( - let("v", structuralNames().exactOf(type).sample().name()), + var template4 = Template.make("type", (StructuralName.Type type) -> scope( + structuralNames().exactOf(type).sampleAndLetAs("v"), "{ $sample\n", "blackhole(#v)\n", "} $sample\n" )); - var template8 = Template.make(() -> body( + var template8 = Template.make(() -> scope( "class $X {\n", template1.asToken(myStructuralTypeA), template1.asToken(myStructuralTypeB), - hook1.anchor( - "begin $body\n", + hook1.anchor(scope( + "begin $scope\n", template1.asToken(myStructuralTypeA), template1.asToken(myStructuralTypeB), "start with A\n", @@ -1711,7 +2245,7 @@ public class TestTemplate { template4.asToken(myStructuralTypeB), template1.asToken(myStructuralTypeA), template1.asToken(myStructuralTypeB) - ), + )), template1.asToken(myStructuralTypeA), template1.asToken(myStructuralTypeB), "}\n" @@ -1725,7 +2259,7 @@ public class TestTemplate { [StructuralB: false, 0, names: {}] define StructuralA name_6 define StructuralB name_11 - begin body_1 + begin scope_1 [StructuralA: false, 0, names: {}] [StructuralB: false, 0, names: {}] start with A @@ -1755,16 +2289,269 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testStructuralNames3() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> scope( + let("name1", sn.name()), + "sn1: #name1.\n", + addStructuralName("scope_garbage1", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> nameScope( + // We cannot use "let" here (at least not easily), otherwise we get + // a duplicate hashtag replacement. It would probably be better style + // to use a "let", but we are just checking that "nameScope" works + // for reuse of names. + "sn2: ", sn.name(), ".\n", + // But for testing, we still do a "let", just with different key. + // (This is probably bad practice, we just do this for testing) + let("name2_" + sn.name(), sn.name()), + addStructuralName("scope_garbage2", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> transparentScope( + // Same issue with hashtags as with "nameScope". + "sn3: ", sn.name(), ".\n", + let("name3_" + sn.name(), sn.name()), + // Using the same name for each would lead to duplicates, + // so we have to modify the name here. + addStructuralName("x_" + sn.name(), myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> hashtagScope( + let("name4", sn.name()), + "sn4: #name4.\n", + // Same issue with duplicate names as with "transparentScope". + addStructuralName("y_" + sn.name(), myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> setFuelCostScope( + // Same issue with hashtags as with "nameScope". + "sn5: ", sn.name(), ".\n", + let("name5_" + sn.name(), sn.name()), + // Same issue with duplicate names as with "transparentScope". + addStructuralName("z_" + sn.name(), myStructuralTypeA) + )), + "sn2: #name2_a #name2_b.\n", // hashtags escaped + "sn3: #name3_a #name3_b.\n", // hashtags escaped + "sn5: #name5_a #name5_b #name5_x_a #name5_x_b.\n", // hashtags escaped + let("name1", "shouldBeOK1"), // hashtag did not escape + let("name4", "shouldBeOk4") // hashtag did not escape + )); + + String code = template.render(); + String expected = + """ + sn1: a. + sn1: b. + sn2: a. + sn2: b. + sn3: a. + sn3: b. + sn4: a. + sn4: b. + sn4: x_a. + sn4: x_b. + sn5: a. + sn5: b. + sn5: x_a. + sn5: x_b. + sn5: y_a. + sn5: y_b. + sn5: y_x_a. + sn5: y_x_b. + sn2: a b. + sn3: a b. + sn5: a b x_a x_b. + """; + checkEQ(code, expected); + } + + public static void testStructuralNames4() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).toList(list -> scope( + let("name1", list.size()), + "list1: #name1.\n", + addStructuralName("scope_garbage1", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).toList(list -> nameScope( + let("name2", list.size()), + "list2: #name2.\n", + addStructuralName("scope_garbage2", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).toList(list -> transparentScope( + let("name3", list.size()), + "list3: #name3.\n", + addStructuralName("x", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).toList(list -> hashtagScope( + let("name4", list.size()), + "list4: #name4.\n", + addStructuralName("y", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).toList(list -> setFuelCostScope( + let("name5", list.size()), + "list5: #name5.\n", + addStructuralName("z", myStructuralTypeA) + )), + "list2: #name2.\n", // hashtag escaped + "list3: #name3.\n", // hashtag escaped + "list5: #name5.\n", // hashtag escaped + let("name1", "shouldBeOk4"), // hashtag did not escape + let("name4", "shouldBeOk4"), // hashtag did not escape + structuralNames().exactOf(myStructuralTypeA).forEach("name", "type", sn -> scope( + "available: #name #type.\n" + )) + )); + + String code = template.render(); + String expected = + """ + list1: 2. + list2: 2. + list3: 2. + list4: 3. + list5: 4. + list2: 2. + list3: 2. + list5: 4. + available: a StructuralA. + available: b StructuralA. + available: x StructuralA. + available: y StructuralA. + available: z StructuralA. + """; + checkEQ(code, expected); + } + + public static void testStructuralNames5() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).count(c -> scope( + let("name1", c), + "list1: #name1.\n", + addStructuralName("scope_garbage1", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).count(c -> nameScope( + let("name2", c), + "list2: #name2.\n", + addStructuralName("scope_garbage2", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).count(c -> transparentScope( + let("name3", c), + "list3: #name3.\n", + addStructuralName("x", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).count(c -> hashtagScope( + let("name4", c), + "list4: #name4.\n", + addStructuralName("y", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).count(c -> setFuelCostScope( + let("name5", c), + "list5: #name5.\n", + addStructuralName("z", myStructuralTypeA) + )), + "list2: #name2.\n", // hashtag escaped + "list3: #name3.\n", // hashtag escaped + "list5: #name5.\n", // hashtag escaped + let("name1", "shouldBeOk4"), // hashtag did not escape + let("name4", "shouldBeOk4"), // hashtag did not escape + structuralNames().exactOf(myStructuralTypeA).forEach("name", "type", sn -> scope( + "available: #name #type.\n" + )) + )); + + String code = template.render(); + String expected = + """ + list1: 2. + list2: 2. + list3: 2. + list4: 3. + list5: 4. + list2: 2. + list3: 2. + list5: 4. + available: a StructuralA. + available: b StructuralA. + available: x StructuralA. + available: y StructuralA. + available: z StructuralA. + """; + checkEQ(code, expected); + } + + public static void testStructuralNames6() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).hasAny(h -> scope( + let("name1", h), + "list1: #name1.\n", + addStructuralName("scope_garbage1", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).hasAny(h -> nameScope( + let("name2", h), + "list2: #name2.\n", + addStructuralName("scope_garbage2", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).hasAny(h -> transparentScope( + let("name3", h), + "list3: #name3.\n", + addStructuralName("x", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).hasAny(h -> hashtagScope( + let("name4", h), + "list4: #name4.\n", + addStructuralName("y", myStructuralTypeA) + )), + structuralNames().exactOf(myStructuralTypeA).hasAny(h -> setFuelCostScope( + let("name5", h), + "list5: #name5.\n", + addStructuralName("z", myStructuralTypeA) + )), + "list2: #name2.\n", // hashtag escaped + "list3: #name3.\n", // hashtag escaped + "list5: #name5.\n", // hashtag escaped + let("name1", "shouldBeOk4"), // hashtag did not escape + let("name4", "shouldBeOk4"), // hashtag did not escape + structuralNames().exactOf(myStructuralTypeA).forEach("name", "type", sn -> scope( + "available: #name #type.\n" + )) + )); + + String code = template.render(); + String expected = + """ + list1: true. + list2: true. + list3: true. + list4: true. + list5: true. + list2: true. + list3: true. + list5: true. + available: a StructuralA. + available: b StructuralA. + available: x StructuralA. + available: y StructuralA. + available: z StructuralA. + """; + checkEQ(code, expected); + } + record MyItem(DataName.Type type, String op) {} public static void testListArgument() { - var template1 = Template.make("item", (MyItem item) -> body( + var template1 = Template.make("item", (MyItem item) -> scope( let("type", item.type()), let("op", item.op()), "#type apply #op\n" )); - var template2 = Template.make("list", (List list) -> body( + var template2 = Template.make("list", (List list) -> scope( "class $Z {\n", // Use template1 for every item in the list. list.stream().map(item -> template1.asToken(item)).toList(), @@ -1797,12 +2584,746 @@ public class TestTemplate { checkEQ(code, expected); } + public static void testNestedScopes1() { + var listDataNames = Template.make(() -> scope( + "dataNames: {", + dataNames(MUTABLE).exactOf(myInt).forEach("name", "type", (DataName dn) -> scope( + "#name #type; " + )), + "}\n" + )); + + var template = Template.make("x", (String x) -> scope( + "$start\n", + addDataName("vx", myInt, MUTABLE), + "x: #x.\n", + listDataNames.asToken(), + // A "transparentScope" nesting essencially does nothing but create + // a list of tokens. It passes through names and hashtags. + "open transparentScope:\n", + transparentScope( + "$transparentScope\n", + let("y", "YYY"), + addDataName("vy", myInt, MUTABLE), + "x: #x.\n", + "y: #y.\n", + listDataNames.asToken() + ), + "close transparentScope.\n", + "x: #x.\n", + "y: #y.\n", + listDataNames.asToken(), + // A "hashtagScope" nesting makes hashtags local, but names + // escape the nesting. + "open hashtagScope:\n", + hashtagScope( + "$hashtagScope\n", + let("z", "ZZZ1"), + "z: #z.\n", + addDataName("vz", myInt, MUTABLE), + listDataNames.asToken() + ), + "close hashtagScope.\n", + let("z", "ZZZ2"), // we can define it again outside. + "z: #z.\n", + listDataNames.asToken(), + // We can also use hashtagScopes for loops. + List.of("a", "b", "c").stream().map(str -> hashtagScope( + "$hashtagScope\n", + let("str", str), // the hashtag is local to every element + "str: #str.\n", + addDataName("v_" + str, myInt, MUTABLE), + listDataNames.asToken() + )).toList(), + "finish str list.\n", + listDataNames.asToken(), + // A "nameScope" nesting makes names local, but hashtags + // escape the nesting. + "open nameScope:\n", + nameScope( + "$nameScope\n", + let("p", "PPP"), + "p: #p.\n", + addDataName("vp", myInt, MUTABLE), + listDataNames.asToken() + ), + "close hashtagScope.\n", + "p: #p.\n", + listDataNames.asToken(), + // A "scope" nesting makes names and hashtags local + "open scope:\n", + scope( + "$scope\n", + let("q", "QQQ1"), + "q: #q.\n", + addDataName("vq", myInt, MUTABLE), + listDataNames.asToken() + ), + "close scope.\n", + let("q", "QQQ2"), + "q: #q.\n", + listDataNames.asToken(), + // A "setFuelCostScope" nesting behaves the same as "transparentScope", as we are not using fuel here. + "open setFuelCostScope:\n", + setFuelCostScope( + "$setFuelCostScope\n", + let("r", "RRR"), + "r: #r.\n", + addDataName("vr", myInt, MUTABLE), + listDataNames.asToken() + ), + "close setFuelCostScope.\n", + "r: #r.\n", + listDataNames.asToken() + + )); + + String code = template.render("XXX"); + String expected = + """ + start_1 + x: XXX. + dataNames: {vx int; } + open transparentScope: + transparentScope_1 + x: XXX. + y: YYY. + dataNames: {vx int; vy int; } + close transparentScope. + x: XXX. + y: YYY. + dataNames: {vx int; vy int; } + open hashtagScope: + hashtagScope_1 + z: ZZZ1. + dataNames: {vx int; vy int; vz int; } + close hashtagScope. + z: ZZZ2. + dataNames: {vx int; vy int; vz int; } + hashtagScope_1 + str: a. + dataNames: {vx int; vy int; vz int; v_a int; } + hashtagScope_1 + str: b. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; } + hashtagScope_1 + str: c. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; } + finish str list. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; } + open nameScope: + nameScope_1 + p: PPP. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; vp int; } + close hashtagScope. + p: PPP. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; } + open scope: + scope_1 + q: QQQ1. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; vq int; } + close scope. + q: QQQ2. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; } + open setFuelCostScope: + setFuelCostScope_1 + r: RRR. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; vr int; } + close setFuelCostScope. + r: RRR. + dataNames: {vx int; vy int; vz int; v_a int; v_b int; v_c int; vr int; } + """; + checkEQ(code, expected); + } + + public static void testNestedScopes2() { + var listDataNames = Template.make(() -> scope( + "dataNames: {", + dataNames(MUTABLE).exactOf(myInt).forEach("name", "type", (DataName dn) -> scope( + "#name #type; " + )), + "}\n" + )); + + var template = Template.make(() -> scope( + // Define some global variables. + List.of("a", "b", "c").stream().map(str -> hashtagScope( + let("var", "g_" + str), + addDataName("g_" + str, myInt, MUTABLE), + "def global #var.\n" + )).toList(), + listDataNames.asToken(), + scope( + "open scope:\n", + // Define some variables. + List.of("i", "j", "k").stream().map(str -> hashtagScope( + let("var", "v_" + str), + addDataName("v_" + str, myInt, MUTABLE), + "def #var.\n" + )).toList(), + listDataNames.asToken(), + scope( + "open inner scope:\n", + addDataName("v_local", myInt, MUTABLE), + "def v_local.\n", + listDataNames.asToken(), + "close inner scope.\n" + ), + listDataNames.asToken(), + "close scope.\n" + ), + listDataNames.asToken() + )); + + String code = template.render(); + String expected = + """ + def global g_a. + def global g_b. + def global g_c. + dataNames: {g_a int; g_b int; g_c int; } + open scope: + def v_i. + def v_j. + def v_k. + dataNames: {g_a int; g_b int; g_c int; v_i int; v_j int; v_k int; } + open inner scope: + def v_local. + dataNames: {g_a int; g_b int; g_c int; v_i int; v_j int; v_k int; v_local int; } + close inner scope. + dataNames: {g_a int; g_b int; g_c int; v_i int; v_j int; v_k int; } + close scope. + dataNames: {g_a int; g_b int; g_c int; } + """; + checkEQ(code, expected); + } + + public static void testTemplateScopes() { + var statusTemplate = Template.make(() -> scope( + "{", + structuralNames().exactOf(myStructuralTypeA).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), + "}\n", + let("fuel", fuel()), + "fuel: #fuel\n" + )); + + var scopeTemplate = Template.make(() -> scope( + "scope:\n", + let("local", "inner scope"), + addStructuralName("x", myStructuralTypeA), + statusTemplate.asToken(), + setFuelCost(50) + )); + + var transparentScopeTemplate = Template.make(() -> transparentScope( + "transparentScope:\n", + let("local", "inner flag"), + addStructuralName("y", myStructuralTypeA), // should escape + statusTemplate.asToken(), + setFuelCost(50) + )); + + var template = Template.make(() -> scope( + setFuelCost(1), + let("local", "root"), + addStructuralName("a", myStructuralTypeA), + statusTemplate.asToken(), + scopeTemplate.asToken(), + statusTemplate.asToken(), + transparentScopeTemplate.asToken(), + statusTemplate.asToken() + )); + + String code = template.render(); + String expected = + """ + {a} + fuel: 99.0f + scope: + {a, x} + fuel: 89.0f + {a} + fuel: 99.0f + transparentScope: + {a, y} + fuel: 89.0f + {a, y} + fuel: 99.0f + """; + checkEQ(code, expected); + } + + public static void testHookAndScopes1() { + Hook hook1 = new Hook("Hook1"); + + var listNamesTemplate = Template.make(() -> scope( + "{", + structuralNames().exactOf(myStructuralTypeA).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), + "}\n" + )); + + var insertScopeTemplate = Template.make("name", (String name) -> scope( + let("local", "insert scope garbage"), + addStructuralName(name, myStructuralTypeA), + "inserted scope: #name\n", + listNamesTemplate.asToken() + )); + + var insertTransparentScopeTemplate = Template.make("name", (String name) -> transparentScope( + let("local", "insert transparentScope garbage"), + addStructuralName(name, myStructuralTypeA), + "inserted transparentScope: #name\n", + listNamesTemplate.asToken() + )); + + var probeTemplate = Template.make(() -> scope( + "inserted probe:\n", + listNamesTemplate.asToken() + )); + + var template = Template.make(() -> scope( + "scope:\n", + hook1.anchor(scope( + let("local", "scope garbage"), + addStructuralName("x1a", myStructuralTypeA), + "scope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertScopeTemplate.asToken("x1b")), + "scope after insert scope:\n", + listNamesTemplate.asToken(), + "scope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertTransparentScopeTemplate.asToken("x1c")), + "scope after insert transparentScope:\n", + listNamesTemplate.asToken(), + "scope insert probe.\n", + hook1.insert(probeTemplate.asToken()) + )), + "after scope:\n", + listNamesTemplate.asToken(), + + "transparentScope:\n", + hook1.anchor(transparentScope( + let("transparentScope2", "abc"), + addStructuralName("x2a", myStructuralTypeA), + "transparentScope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertScopeTemplate.asToken("x2b")), + "transparentScope after insert scope:\n", + listNamesTemplate.asToken(), + "transparentScope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertTransparentScopeTemplate.asToken("x2c")), + "transparentScope after insert transparentScope:\n", + listNamesTemplate.asToken(), + "transparentScope insert probe.\n", + hook1.insert(probeTemplate.asToken()) + )), + "after transparentScope:\n", + listNamesTemplate.asToken(), + "transparentScope2: #transparentScope2\n", + + "hashtagScope:\n", + hook1.anchor(hashtagScope( + let("local", "hashtagScope garbage"), + addStructuralName("x3a", myStructuralTypeA), + "hashtagScope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertScopeTemplate.asToken("x3b")), + "hashtagScope after insert scope:\n", + listNamesTemplate.asToken(), + "hashtagScope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertTransparentScopeTemplate.asToken("x3c")), + "hashtagScope after insert transparentScope:\n", + listNamesTemplate.asToken(), + "hashtagScope insert probe.\n", + hook1.insert(probeTemplate.asToken()) + )), + "after hashtagScope:\n", + listNamesTemplate.asToken(), + + "nameScope:\n", + hook1.anchor(nameScope( + let("transparentScope4", "abcde"), + addStructuralName("x4a", myStructuralTypeA), + "nameScope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertScopeTemplate.asToken("x4b")), + "nameScope after insert scope:\n", + listNamesTemplate.asToken(), + "nameScope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(insertTransparentScopeTemplate.asToken("x4c")), + "nameScope after insert transparentScope:\n", + listNamesTemplate.asToken(), + "nameScope insert probe.\n", + hook1.insert(probeTemplate.asToken()) + )), + "after nameScope:\n", + listNamesTemplate.asToken(), + "transparentScope4: #transparentScope4\n", + + let("local", "outer garbage") + )); + + String code = template.render(); + String expected = + """ + scope: + inserted scope: x1b + {x1b} + inserted transparentScope: x1c + {x1c} + inserted probe: + {x1c} + scope before insert scope: + {x1a} + scope after insert scope: + {x1a} + scope before insert transparentScope: + {x1a} + scope after insert transparentScope: + {x1c, x1a} + scope insert probe. + after scope: + {} + transparentScope: + inserted scope: x2b + {x2a, x2b} + inserted transparentScope: x2c + {x2a, x2c} + inserted probe: + {x2a, x2c} + transparentScope before insert scope: + {x2a} + transparentScope after insert scope: + {x2a} + transparentScope before insert transparentScope: + {x2a} + transparentScope after insert transparentScope: + {x2a, x2c} + transparentScope insert probe. + after transparentScope: + {x2a, x2c} + transparentScope2: abc + hashtagScope: + inserted scope: x3b + {x2a, x2c, x3a, x3b} + inserted transparentScope: x3c + {x2a, x2c, x3a, x3c} + inserted probe: + {x2a, x2c, x3a, x3c} + hashtagScope before insert scope: + {x2a, x2c, x3a} + hashtagScope after insert scope: + {x2a, x2c, x3a} + hashtagScope before insert transparentScope: + {x2a, x2c, x3a} + hashtagScope after insert transparentScope: + {x2a, x2c, x3a, x3c} + hashtagScope insert probe. + after hashtagScope: + {x2a, x2c, x3a, x3c} + nameScope: + inserted scope: x4b + {x2a, x2c, x3a, x3c, x4b} + inserted transparentScope: x4c + {x2a, x2c, x3a, x3c, x4c} + inserted probe: + {x2a, x2c, x3a, x3c, x4c} + nameScope before insert scope: + {x2a, x2c, x3a, x3c, x4a} + nameScope after insert scope: + {x2a, x2c, x3a, x3c, x4a} + nameScope before insert transparentScope: + {x2a, x2c, x3a, x3c, x4a} + nameScope after insert transparentScope: + {x2a, x2c, x3a, x3c, x4c, x4a} + nameScope insert probe. + after nameScope: + {x2a, x2c, x3a, x3c} + transparentScope4: abcde + """; + checkEQ(code, expected); + } + + public static void testHookAndScopes2() { + Hook hook1 = new Hook("Hook1"); + + var listNamesTemplate = Template.make(() -> scope( + "{", + structuralNames().exactOf(myStructuralTypeA).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), + "}\n" + )); + + var template = Template.make(() -> scope( + "scope:\n", + hook1.anchor(scope( + let("local0", "scope garbage"), + let("local1", "LOCAL1"), + addStructuralName("x1a", myStructuralTypeA), + + "scope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(scope( + let("local2", "insert scope garbage"), + let("name", "x1b"), + addStructuralName("x1b", myStructuralTypeA), // does NOT escape to anchor scope + "inserted scope: #name\n", + "local1: #local1\n", + listNamesTemplate.asToken() + )), + "scope after insert scope:\n", + listNamesTemplate.asToken(), + + "scope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(transparentScope( + let("nameTransparentScope", "x1c"), // escapes to caller + addStructuralName("x1c", myStructuralTypeA), // escapes to anchor scope + "inserted transparentScope: #nameTransparentScope\n", + "local1: #local1\n", + listNamesTemplate.asToken() + )), + "scope after insert transparentScope:\n", + "nameTransparentScope: #nameTransparentScope\n", + listNamesTemplate.asToken(), + + "scope before insert nameScope:\n", + listNamesTemplate.asToken(), + hook1.insert(nameScope( + let("nameNameScope", "x1d"), // escapes to caller + addStructuralName("x1d", myStructuralTypeA), // does NOT escape to anchor scope + "inserted nameScope: #nameNameScope\n", + "local1: #local1\n", + listNamesTemplate.asToken() + )), + "scope after insert nameScope:\n", + "nameNameScope: #nameNameScope\n", + listNamesTemplate.asToken(), + + "scope before insert hashtagScope:\n", + listNamesTemplate.asToken(), + hook1.insert(hashtagScope( + let("local2", "insert hashtagScope garbage"), + let("name", "x1e"), // escapes to caller + addStructuralName("x1e", myStructuralTypeA), // escapes to anchor scope + "inserted hashtagScope: #name\n", + "local1: #local1\n", + listNamesTemplate.asToken() + )), + "scope after insert hashtagScope:\n", + listNamesTemplate.asToken(), + + "scope insert probe.\n", + hook1.insert(scope( + "inserted probe:\n", + listNamesTemplate.asToken() + )) + )), + "after scope:\n", + listNamesTemplate.asToken(), + + let("name", "name garbage"), + let("local0", "outer garbage 0"), + let("local1", "outer garbage 1"), + let("local2", "outer garbage 2"), + let("nameTransparentScope", "outer garbage nameTransparentScope"), + let("nameNameScope", "outer garbage nameNameScope") + )); + + String code = template.render(); + String expected = + """ + scope: + inserted scope: x1b + local1: LOCAL1 + {x1b} + inserted transparentScope: x1c + local1: LOCAL1 + {x1c} + inserted nameScope: x1d + local1: LOCAL1 + {x1c, x1d} + inserted hashtagScope: x1e + local1: LOCAL1 + {x1c, x1e} + inserted probe: + {x1c, x1e} + scope before insert scope: + {x1a} + scope after insert scope: + {x1a} + scope before insert transparentScope: + {x1a} + scope after insert transparentScope: + nameTransparentScope: x1c + {x1c, x1a} + scope before insert nameScope: + {x1c, x1a} + scope after insert nameScope: + nameNameScope: x1d + {x1c, x1a} + scope before insert hashtagScope: + {x1c, x1a} + scope after insert hashtagScope: + {x1c, x1e, x1a} + scope insert probe. + after scope: + {} + """; + checkEQ(code, expected); + } + + // Analogue to testHookAndScopes2, but with "transparentScope" instead of "scope". + public static void testHookAndScopes3() { + Hook hook1 = new Hook("Hook1"); + + var listNamesTemplate = Template.make(() -> scope( + "{", + structuralNames().exactOf(myStructuralTypeA).toList(list -> scope( + String.join(", ", list.stream().map(StructuralName::name).toList()) + )), + "}\n" + )); + + var template = Template.make(() -> scope( + "transparentScope:\n", + hook1.anchor(transparentScope( + let("global0", "transparentScope garbage"), + let("global1", "GLOBAL1"), + addStructuralName("x1a", myStructuralTypeA), + + "transparentScope before insert scope:\n", + listNamesTemplate.asToken(), + hook1.insert(scope( + let("local2", "insert scope garbage"), + let("name", "x1b"), + addStructuralName("x1b", myStructuralTypeA), // does NOT escape to anchor scope + "inserted scope: #name\n", + "global1: #global1\n", + listNamesTemplate.asToken() + )), + "transparentScope after insert scope:\n", + listNamesTemplate.asToken(), + + "transparentScope before insert transparentScope:\n", + listNamesTemplate.asToken(), + hook1.insert(transparentScope( + let("nameTransparentScope", "x1c"), // escapes to caller + addStructuralName("x1c", myStructuralTypeA), // escapes to anchor scope + "inserted transparentScope: #nameTransparentScope\n", + "global1: #global1\n", + listNamesTemplate.asToken() + )), + "transparentScope after insert transparentScope:\n", + "nameTransparentScope: #nameTransparentScope\n", + listNamesTemplate.asToken(), + + "transparentScope before insert nameScope:\n", + listNamesTemplate.asToken(), + hook1.insert(nameScope( + let("nameNameScope", "x1d"), // escapes to caller + addStructuralName("x1d", myStructuralTypeA), // does NOT escape to anchor scope + "inserted nameScope: #nameNameScope\n", + "global1: #global1\n", + listNamesTemplate.asToken() + )), + "transparentScope after insert nameScope:\n", + "nameNameScope: #nameNameScope\n", + listNamesTemplate.asToken(), + + "transparentScope before insert hashtagScope:\n", + listNamesTemplate.asToken(), + hook1.insert(hashtagScope( + let("local2", "insert hashtagScope garbage"), + let("name", "x1e"), // escapes to caller + addStructuralName("x1e", myStructuralTypeA), // escapes to anchor scope + "inserted hashtagScope: #name\n", + "global1: #global1\n", + listNamesTemplate.asToken() + )), + "transparentScope after insert hashtagScope:\n", + listNamesTemplate.asToken(), + + "transparentScope insert probe.\n", + hook1.insert(scope( + "inserted probe:\n", + listNamesTemplate.asToken() + )) + )), + "after transparentScope:\n", + listNamesTemplate.asToken(), + """ + global0: #global0 + global1: #global1 + nameTransparentScope: #nameTransparentScope + nameNameScope: #nameNameScope + """, + let("name", "name garbage"), + let("local2", "outer garbage 2") + )); + + String code = template.render(); + String expected = + """ + transparentScope: + inserted scope: x1b + global1: GLOBAL1 + {x1a, x1b} + inserted transparentScope: x1c + global1: GLOBAL1 + {x1a, x1c} + inserted nameScope: x1d + global1: GLOBAL1 + {x1a, x1c, x1d} + inserted hashtagScope: x1e + global1: GLOBAL1 + {x1a, x1c, x1e} + inserted probe: + {x1a, x1c, x1e} + transparentScope before insert scope: + {x1a} + transparentScope after insert scope: + {x1a} + transparentScope before insert transparentScope: + {x1a} + transparentScope after insert transparentScope: + nameTransparentScope: x1c + {x1a, x1c} + transparentScope before insert nameScope: + {x1a, x1c} + transparentScope after insert nameScope: + nameNameScope: x1d + {x1a, x1c} + transparentScope before insert hashtagScope: + {x1a, x1c} + transparentScope after insert hashtagScope: + {x1a, x1c, x1e} + transparentScope insert probe. + after transparentScope: + {x1a, x1c, x1e} + global0: transparentScope garbage + global1: GLOBAL1 + nameTransparentScope: x1c + nameNameScope: x1d + """; + checkEQ(code, expected); + } + public static void testFailingNestedRendering() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "alpha\n" )); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "beta\n", // Nested "render" call not allowed! template1.render(), @@ -1813,63 +3334,63 @@ public class TestTemplate { } public static void testFailingDollarName1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("x", $("")) // empty string not allowed )); String code = template1.render(); } public static void testFailingDollarName2() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("x", $("#abc")) // "#" character not allowed )); String code = template1.render(); } public static void testFailingDollarName3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("x", $("abc#")) // "#" character not allowed )); String code = template1.render(); } public static void testFailingDollarName4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("x", $(null)) // Null input to dollar )); String code = template1.render(); } public static void testFailingDollarName5() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "$" // empty dollar name )); String code = template1.render(); } public static void testFailingDollarName6() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "asdf$" // empty dollar name )); String code = template1.render(); } public static void testFailingDollarName7() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "asdf$1" // Bad pattern after dollar )); String code = template1.render(); } public static void testFailingDollarName8() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "abc$$abc" // empty dollar name )); String code = template1.render(); } public static void testFailingLetName1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let(null, $("abc")) // Null input for hashtag name )); String code = template1.render(); @@ -1877,20 +3398,20 @@ public class TestTemplate { public static void testFailingHashtagName1() { // Empty Template argument - var template1 = Template.make("", (String x) -> body( + var template1 = Template.make("", (String x) -> scope( )); String code = template1.render("abc"); } public static void testFailingHashtagName2() { // "#" character not allowed in template argument - var template1 = Template.make("abc#abc", (String x) -> body( + var template1 = Template.make("abc#abc", (String x) -> scope( )); String code = template1.render("abc"); } public static void testFailingHashtagName3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // Empty let hashtag name not allowed let("", "abc") )); @@ -1898,7 +3419,7 @@ public class TestTemplate { } public static void testFailingHashtagName4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // "#" character not allowed in let hashtag name let("xyz#xyz", "abc") )); @@ -1906,56 +3427,56 @@ public class TestTemplate { } public static void testFailingHashtagName5() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "#" // empty hashtag name )); String code = template1.render(); } public static void testFailingHashtagName6() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "asdf#" // empty hashtag name )); String code = template1.render(); } public static void testFailingHashtagName7() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "asdf#1" // Bad pattern after hashtag )); String code = template1.render(); } public static void testFailingHashtagName8() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "abc##abc" // empty hashtag name )); String code = template1.render(); } public static void testFailingDollarHashtagName1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "#$" // empty hashtag name )); String code = template1.render(); } public static void testFailingDollarHashtagName2() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "$#" // empty dollar name )); String code = template1.render(); } public static void testFailingDollarHashtagName3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "#$name" // empty hashtag name )); String code = template1.render(); } public static void testFailingDollarHashtagName4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "$#name" // empty dollar name )); String code = template1.render(); @@ -1964,11 +3485,11 @@ public class TestTemplate { public static void testFailingHook() { var hook1 = new Hook("Hook1"); - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "alpha\n" )); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( "beta\n", // Use hook without hook1.anchor hook1.insert(template1.asToken()), @@ -1978,20 +3499,40 @@ public class TestTemplate { String code = template2.render(); } - public static void testFailingSample1() { - var template1 = Template.make(() -> body( - // No variable added yet. - let("v", dataNames(MUTABLE).exactOf(myInt).sample().name()), + public static void testFailingSample1a() { + var template1 = Template.make(() -> scope( + // No DataName added yet. + dataNames(MUTABLE).exactOf(myInt).sampleAndLetAs("v"), "v is #v\n" )); String code = template1.render(); } - public static void testFailingSample2() { - var template1 = Template.make(() -> body( + public static void testFailingSample1b() { + var template1 = Template.make(() -> scope( + // No StructuralName added yet. + structuralNames().exactOf(myStructuralTypeA).sampleAndLetAs("v"), + "v is #v\n" + )); + + String code = template1.render(); + } + + public static void testFailingSample2a() { + var template1 = Template.make(() -> scope( // no type restriction - let("v", dataNames(MUTABLE).sample().name()), + dataNames(MUTABLE).sampleAndLetAs("v"), + "v is #v\n" + )); + + String code = template1.render(); + } + + public static void testFailingSample2b() { + var template1 = Template.make(() -> scope( + // no type restriction + structuralNames().sampleAndLetAs("v"), "v is #v\n" )); @@ -2000,7 +3541,7 @@ public class TestTemplate { public static void testFailingHashtag1() { // Duplicate hashtag definition from arguments. - var template1 = Template.make("a", "a", (String _, String _) -> body( + var template1 = Template.make("a", "a", (String _, String _) -> scope( "nothing\n" )); @@ -2008,7 +3549,7 @@ public class TestTemplate { } public static void testFailingHashtag2() { - var template1 = Template.make("a", (String _) -> body( + var template1 = Template.make("a", (String _) -> scope( // Duplicate hashtag name let("a", "x"), "nothing\n" @@ -2018,7 +3559,7 @@ public class TestTemplate { } public static void testFailingHashtag3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( let("a", "x"), // Duplicate hashtag name let("a", "y"), @@ -2029,7 +3570,7 @@ public class TestTemplate { } public static void testFailingHashtag4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // Missing hashtag name definition "#a\n" )); @@ -2037,9 +3578,20 @@ public class TestTemplate { String code = template1.render(); } + public static void testFailingHashtag5() { + var template1 = Template.make(() -> scope( + "use before definition: #a\n", + // let is a token, and is only evaluated after + // the string above, and so the string above fails. + let("a", "x") + )); + + String code = template1.render(); + } + public static void testFailingBinding1() { var binding = new TemplateBinding(); - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "nothing\n" )); binding.bind(template1); @@ -2049,7 +3601,7 @@ public class TestTemplate { public static void testFailingBinding2() { var binding = new TemplateBinding(); - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( "nothing\n", // binding was never bound. binding.get() @@ -2059,7 +3611,7 @@ public class TestTemplate { } public static void testFailingAddDataName1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // Must pick either MUTABLE or IMMUTABLE. addDataName("name", myInt, MUTABLE_OR_IMMUTABLE) )); @@ -2067,7 +3619,7 @@ public class TestTemplate { } public static void testFailingAddDataName2() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addDataName("name", myInt, MUTABLE, 0) )); @@ -2075,7 +3627,7 @@ public class TestTemplate { } public static void testFailingAddDataName3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addDataName("name", myInt, MUTABLE, -1) )); @@ -2083,7 +3635,7 @@ public class TestTemplate { } public static void testFailingAddDataName4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addDataName("name", myInt, MUTABLE, 1001) )); @@ -2091,7 +3643,7 @@ public class TestTemplate { } public static void testFailingAddStructuralName1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addStructuralName("name", myStructuralTypeA, 0) )); @@ -2099,7 +3651,7 @@ public class TestTemplate { } public static void testFailingAddStructuralName2() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addStructuralName("name", myStructuralTypeA, -1) )); @@ -2107,7 +3659,7 @@ public class TestTemplate { } public static void testFailingAddStructuralName3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( // weight out of bounds [0..1000] addStructuralName("name", myStructuralTypeA, 1001) )); @@ -2116,7 +3668,7 @@ public class TestTemplate { // Duplicate name in the same scope, name identical -> expect RendererException. public static void testFailingAddNameDuplication1() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE), addDataName("name", myInt, MUTABLE) )); @@ -2125,7 +3677,7 @@ public class TestTemplate { // Duplicate name in the same scope, names have different mutability -> expect RendererException. public static void testFailingAddNameDuplication2() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE), addDataName("name", myInt, IMMUTABLE) )); @@ -2134,7 +3686,7 @@ public class TestTemplate { // Duplicate name in the same scope, names have different type -> expect RendererException. public static void testFailingAddNameDuplication3() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE), addDataName("name", myLong, MUTABLE) )); @@ -2143,7 +3695,7 @@ public class TestTemplate { // Duplicate name in the same scope, name identical -> expect RendererException. public static void testFailingAddNameDuplication4() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addStructuralName("name", myStructuralTypeA), addStructuralName("name", myStructuralTypeA) )); @@ -2152,7 +3704,7 @@ public class TestTemplate { // Duplicate name in the same scope, names have different type -> expect RendererException. public static void testFailingAddNameDuplication5() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addStructuralName("name", myStructuralTypeA), addStructuralName("name", myStructuralTypeB) )); @@ -2161,10 +3713,10 @@ public class TestTemplate { // Duplicate name in inner Template, name identical -> expect RendererException. public static void testFailingAddNameDuplication6() { - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE) )); - var template2 = Template.make(() -> body( + var template2 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE), template1.asToken() )); @@ -2175,11 +3727,11 @@ public class TestTemplate { public static void testFailingAddNameDuplication7() { var hook1 = new Hook("Hook1"); - var template1 = Template.make(() -> body( + var template1 = Template.make(() -> scope( addDataName("name", myInt, MUTABLE), - hook1.anchor( + hook1.anchor(scope( addDataName("name", myInt, MUTABLE) - ) + )) )); String code = template1.render(); } @@ -2188,19 +3740,94 @@ public class TestTemplate { public static void testFailingAddNameDuplication8() { var hook1 = new Hook("Hook1"); - var template1 = Template.make(() -> body( - addDataName("name", myInt, MUTABLE) + var template1 = Template.make(() -> transparentScope( + addDataName("name", myInt, MUTABLE) // escapes )); - var template2 = Template.make(() -> body( - hook1.anchor( + var template2 = Template.make(() -> scope( + hook1.anchor(scope( addDataName("name", myInt, MUTABLE), hook1.insert(template1.asToken()) - ) + )) )); String code = template2.render(); } + public static void testFailingScope1() { + var template = Template.make(() -> scope( + transparentScope( + let("x", "x1") // escapes + ), + let("x", "x2") // second definition + )); + String code = template.render(); + } + + public static void testFailingScope2() { + var template = Template.make(() -> scope( + nameScope( + let("x", "x1") // escapes + ), + let("x", "x2") // second definition + )); + String code = template.render(); + } + + public static void testFailingScope3() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> transparentScope( + let("x", sn.name()) // leads to duplicate hashtag + )) + )); + String code = template.render(); + } + + public static void testFailingScope4() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> nameScope( + let("x", sn.name()) // leads to duplicate hashtag + )) + )); + String code = template.render(); + } + + public static void testFailingScope5() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> transparentScope( + addStructuralName("x", myStructuralTypeA) // leads to duplicate name + )) + )); + String code = template.render(); + } + + public static void testFailingScope6() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> hashtagScope( + addStructuralName("x", myStructuralTypeA) // leads to duplicate name + )) + )); + String code = template.render(); + } + + public static void testFailingScope7() { + var template = Template.make(() -> scope( + addStructuralName("a", myStructuralTypeA), + addStructuralName("b", myStructuralTypeA), + structuralNames().exactOf(myStructuralTypeA).forEach(sn -> setFuelCostScope( + addStructuralName("x", myStructuralTypeA) // leads to duplicate name + )) + )); + String code = template.render(); + } + public static void expectRendererException(FailingTest test, String errorPrefix) { try { test.run(); From ad38a1253ae3ff92f7e0cf0fbc4d4726957b1443 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Thu, 20 Nov 2025 10:19:57 +0000 Subject: [PATCH 003/616] 8371557: java/net/httpclient/http3/H3RequestRejectedTest.java: javax.net.ssl.SSLHandshakeException: local endpoint (wildcard) and remote endpoint (loopback) ports conflict Reviewed-by: jpai --- .../jdk/java/net/httpclient/http3/H3RequestRejectedTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/jdk/java/net/httpclient/http3/H3RequestRejectedTest.java b/test/jdk/java/net/httpclient/http3/H3RequestRejectedTest.java index 361a78fdd47..1731ddae833 100644 --- a/test/jdk/java/net/httpclient/http3/H3RequestRejectedTest.java +++ b/test/jdk/java/net/httpclient/http3/H3RequestRejectedTest.java @@ -50,6 +50,7 @@ import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static java.net.http.HttpRequest.BodyPublishers; import static java.nio.charset.StandardCharsets.US_ASCII; +import static jdk.httpclient.test.lib.common.HttpServerAdapters.createClientBuilderForH3; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -100,7 +101,7 @@ class H3RequestRejectedTest { */ @Test void testAlwaysRejected() throws Exception { - try (final HttpClient client = HttpClient.newBuilder() + try (final HttpClient client = createClientBuilderForH3() .sslContext(sslContext).proxy(NO_PROXY).version(HTTP_3) .build()) { @@ -134,7 +135,7 @@ class H3RequestRejectedTest { */ @Test void testRejectedRequest() throws Exception { - try (final HttpClient client = HttpClient.newBuilder().sslContext(sslContext) + try (final HttpClient client = createClientBuilderForH3().sslContext(sslContext) .proxy(NO_PROXY).version(HTTP_3) .build()) { From c419dda4e99c3b72fbee95b93159db2e23b994b6 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 20 Nov 2025 11:37:07 +0000 Subject: [PATCH 004/616] 8372163: G1: Remove unused G1HeapRegion::remove_code_root Reviewed-by: tschatzl --- src/hotspot/share/gc/g1/g1HeapRegion.cpp | 4 ---- src/hotspot/share/gc/g1/g1HeapRegion.hpp | 1 - 2 files changed, 5 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.cpp b/src/hotspot/share/gc/g1/g1HeapRegion.cpp index b1eeb333d8d..361e19d4be5 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.cpp @@ -307,10 +307,6 @@ void G1HeapRegion::add_code_root(nmethod* nm) { rem_set()->add_code_root(nm); } -void G1HeapRegion::remove_code_root(nmethod* nm) { - rem_set()->remove_code_root(nm); -} - void G1HeapRegion::code_roots_do(NMethodClosure* blk) const { rem_set()->code_roots_do(blk); } diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.hpp b/src/hotspot/share/gc/g1/g1HeapRegion.hpp index 17ec3055b52..fe915b0dafe 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.hpp @@ -543,7 +543,6 @@ public: // Routines for managing a list of code roots (attached to the // this region's RSet) that point into this heap region. void add_code_root(nmethod* nm); - void remove_code_root(nmethod* nm); // Applies blk->do_nmethod() to each of the entries in // the code roots list for this region From 7b11bd1b1d8dbc9bedcd8cf14e78c8f5eb06a71f Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Thu, 20 Nov 2025 13:39:49 +0000 Subject: [PATCH 005/616] 8372047: ClassTransform.transformingMethodBodies andThen composes incorrectly Reviewed-by: asotona --- .../classfile/impl/TransformImpl.java | 18 ++--- test/jdk/jdk/classfile/TransformTests.java | 74 +++++++++++++++---- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/TransformImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/TransformImpl.java index 23387fcb098..4cb0517ec76 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/TransformImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/TransformImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, 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 @@ -120,9 +120,9 @@ public final class TransformImpl { @Override public ClassTransform andThen(ClassTransform next) { - if (next instanceof ClassMethodTransform cmt) - return new ClassMethodTransform(transform.andThen(cmt.transform), - mm -> filter.test(mm) && cmt.filter.test(mm)); + // Optimized for shared _ -> true filter in ClassTransform.transformingMethods(MethodTransform) + if (next instanceof ClassMethodTransform(var nextTransform, var nextFilter) && filter == nextFilter) + return new ClassMethodTransform(transform.andThen(nextTransform), filter); else return UnresolvedClassTransform.super.andThen(next); } @@ -143,9 +143,9 @@ public final class TransformImpl { @Override public ClassTransform andThen(ClassTransform next) { - if (next instanceof ClassFieldTransform cft) - return new ClassFieldTransform(transform.andThen(cft.transform), - mm -> filter.test(mm) && cft.filter.test(mm)); + // Optimized for shared _ -> true filter in ClassTransform.transformingFields(FieldTransform) + if (next instanceof ClassFieldTransform(var nextTransform, var nextFilter) && filter == nextFilter) + return new ClassFieldTransform(transform.andThen(nextTransform), filter); else return UnresolvedClassTransform.super.andThen(next); } @@ -208,8 +208,8 @@ public final class TransformImpl { @Override public MethodTransform andThen(MethodTransform next) { - return (next instanceof TransformImpl.MethodCodeTransform mct) - ? new TransformImpl.MethodCodeTransform(xform.andThen(mct.xform)) + return (next instanceof MethodCodeTransform(var nextXform)) + ? new TransformImpl.MethodCodeTransform(xform.andThen(nextXform)) : UnresolvedMethodTransform.super.andThen(next); } diff --git a/test/jdk/jdk/classfile/TransformTests.java b/test/jdk/jdk/classfile/TransformTests.java index b78da8b4311..351aa1ae35a 100644 --- a/test/jdk/jdk/classfile/TransformTests.java +++ b/test/jdk/jdk/classfile/TransformTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, 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 @@ -23,25 +23,15 @@ /* * @test - * @bug 8335935 8336588 + * @bug 8335935 8336588 8372047 * @summary Testing ClassFile transformations. * @run junit TransformTests */ -import java.lang.classfile.ClassBuilder; -import java.lang.classfile.ClassElement; -import java.lang.classfile.ClassFile; -import java.lang.classfile.ClassModel; -import java.lang.classfile.ClassTransform; -import java.lang.classfile.CodeBuilder; -import java.lang.classfile.CodeElement; -import java.lang.classfile.CodeModel; -import java.lang.classfile.CodeTransform; -import java.lang.classfile.FieldModel; -import java.lang.classfile.FieldTransform; -import java.lang.classfile.Label; -import java.lang.classfile.MethodModel; -import java.lang.classfile.MethodTransform; +import java.lang.classfile.*; +import java.lang.classfile.attribute.AnnotationDefaultAttribute; +import java.lang.classfile.attribute.ConstantValueAttribute; +import java.lang.classfile.attribute.SourceDebugExtensionAttribute; import java.lang.classfile.instruction.BranchInstruction; import java.lang.classfile.instruction.ConstantInstruction; import java.lang.classfile.instruction.LabelTarget; @@ -54,8 +44,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import helpers.ByteArrayClassLoader; +import jdk.internal.classfile.impl.TransformImpl; import org.junit.jupiter.api.Test; import static java.lang.classfile.ClassFile.*; @@ -334,4 +326,54 @@ class TransformTests { return "foo"; } } + + @Test + void testFilteringTransformChaining() { + var cf = ClassFile.of(); + var clazz = cf.parse(cf.build(ClassDesc.of("Test"), clb -> clb + .withField("one", CD_int, fb -> fb.with(ConstantValueAttribute.of(1))) + .withField("two", CD_int, fb -> fb.with(ConstantValueAttribute.of(2))) + .withMethod("one", MTD_void, 0, mb -> mb.with(AnnotationDefaultAttribute.of(AnnotationValue.ofInt(1))).withCode(CodeBuilder::return_)) + .withMethod("two", MTD_void, 0, mb -> mb.with(AnnotationDefaultAttribute.of(AnnotationValue.ofInt(2))).withCode(CodeBuilder::return_)))); + + AtomicBoolean oneFieldCalled = new AtomicBoolean(false); + var oneFieldTransform = new TransformImpl.ClassFieldTransform((fb, fe) -> { + if (fe instanceof ConstantValueAttribute cv) { + assertEquals(1, ((Integer) cv.constant().constantValue()), "Should only transform one"); + } + oneFieldCalled.set(true); + fb.with(fe); + }, fm -> fm.fieldName().equalsString("one")); + AtomicBoolean twoFieldCalled = new AtomicBoolean(false); + var twoFieldTransform = new TransformImpl.ClassFieldTransform((fb, fe) -> { + if (fe instanceof ConstantValueAttribute cv) { + assertEquals(2, ((Integer) cv.constant().constantValue()), "Should only transform two"); + } + twoFieldCalled.set(true); + fb.with(fe); + }, fm -> fm.fieldName().equalsString("two")); + cf.transformClass(clazz, oneFieldTransform.andThen(twoFieldTransform)); + assertTrue(oneFieldCalled.get(), "Field one not transformed"); + assertTrue(twoFieldCalled.get(), "Field two not transformed"); + + AtomicBoolean oneMethodCalled = new AtomicBoolean(false); + var oneMethodTransform = ClassTransform.transformingMethods(mm -> mm.methodName().equalsString("one"), (mb, me) -> { + if (me instanceof AnnotationDefaultAttribute ada) { + assertEquals(1, ((AnnotationValue.OfInt) ada.defaultValue()).intValue(), "Should only transform one"); + } + oneMethodCalled.set(true); + mb.with(me); + }); + AtomicBoolean twoMethodCalled = new AtomicBoolean(false); + var twoMethodTransform = ClassTransform.transformingMethods(mm -> mm.methodName().equalsString("two"), (mb, me) -> { + if (me instanceof AnnotationDefaultAttribute ada) { + assertEquals(2, ((AnnotationValue.OfInt) ada.defaultValue()).intValue(), "Should only transform two"); + } + twoMethodCalled.set(true); + mb.with(me); + }); + cf.transformClass(clazz, oneMethodTransform.andThen(twoMethodTransform)); + assertTrue(oneMethodCalled.get(), "Method one not transformed"); + assertTrue(twoMethodCalled.get(), "Method two not transformed"); + } } From f125c76f5b53d90a09f58c22d6def7d843feaa50 Mon Sep 17 00:00:00 2001 From: Matthew Donovan Date: Thu, 20 Nov 2025 14:09:55 +0000 Subject: [PATCH 006/616] 8247690: RunTest does not support running of JTREG manual tests Reviewed-by: erikj --- doc/testing.html | 2 ++ doc/testing.md | 4 ++++ make/RunTests.gmk | 10 ++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/testing.html b/doc/testing.html index b9838735e4f..31f4fbd1778 100644 --- a/doc/testing.html +++ b/doc/testing.html @@ -535,6 +535,8 @@ failure. This helps to reproduce intermittent test failures. Defaults to

REPORT

Use this report style when reporting test results (sent to JTReg as -report). Defaults to files.

+

MANUAL

+

Set to true to execute manual tests only.

Gtest keywords

REPEAT

The number of times to repeat the tests diff --git a/doc/testing.md b/doc/testing.md index 0144610a5bf..b95f59de9fd 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -512,6 +512,10 @@ helps to reproduce intermittent test failures. Defaults to 0. Use this report style when reporting test results (sent to JTReg as `-report`). Defaults to `files`. +#### MANUAL + +Set to `true` to execute manual tests only. + ### Gtest keywords #### REPEAT diff --git a/make/RunTests.gmk b/make/RunTests.gmk index 947389f64f9..21eb178343e 100644 --- a/make/RunTests.gmk +++ b/make/RunTests.gmk @@ -206,7 +206,7 @@ $(eval $(call ParseKeywordVariable, JTREG, \ SINGLE_KEYWORDS := JOBS TIMEOUT_FACTOR FAILURE_HANDLER_TIMEOUT \ TEST_MODE ASSERT VERBOSE RETAIN TEST_THREAD_FACTORY JVMTI_STRESS_AGENT \ MAX_MEM RUN_PROBLEM_LISTS RETRY_COUNT REPEAT_COUNT MAX_OUTPUT REPORT \ - AOT_JDK $(CUSTOM_JTREG_SINGLE_KEYWORDS), \ + AOT_JDK MANUAL $(CUSTOM_JTREG_SINGLE_KEYWORDS), \ STRING_KEYWORDS := OPTIONS JAVA_OPTIONS VM_OPTIONS KEYWORDS \ EXTRA_PROBLEM_LISTS LAUNCHER_OPTIONS \ $(CUSTOM_JTREG_STRING_KEYWORDS), \ @@ -911,7 +911,13 @@ define SetupRunJtregTestBody -vmoption:-Dtest.boot.jdk="$$(BOOT_JDK)" \ -vmoption:-Djava.io.tmpdir="$$($1_TEST_TMP_DIR)" - $1_JTREG_BASIC_OPTIONS += -automatic -ignore:quiet + $1_JTREG_BASIC_OPTIONS += -ignore:quiet + + ifeq ($$(JTREG_MANUAL), true) + $1_JTREG_BASIC_OPTIONS += -manual + else + $1_JTREG_BASIC_OPTIONS += -automatic + endif # Make it possible to specify the JIB_DATA_DIR for tests using the # JIB Artifact resolver From b9ee9541cffb6c5a737b08a69ae04472b3bcab3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20=C3=96sterlund?= Date: Thu, 20 Nov 2025 14:33:40 +0000 Subject: [PATCH 007/616] 8371200: ZGC: C2 allocation deopt race Reviewed-by: aboldtch, stefank --- src/hotspot/share/gc/z/zBarrier.inline.hpp | 4 -- src/hotspot/share/gc/z/zBarrierSet.cpp | 20 ---------- src/hotspot/share/gc/z/zGeneration.cpp | 42 ++++++++++++-------- src/hotspot/share/gc/z/zRelocate.cpp | 45 ++++++++++++++++------ src/hotspot/share/gc/z/zRelocate.hpp | 1 + 5 files changed, 60 insertions(+), 52 deletions(-) diff --git a/src/hotspot/share/gc/z/zBarrier.inline.hpp b/src/hotspot/share/gc/z/zBarrier.inline.hpp index b5923f01628..766a6eb8e4c 100644 --- a/src/hotspot/share/gc/z/zBarrier.inline.hpp +++ b/src/hotspot/share/gc/z/zBarrier.inline.hpp @@ -86,10 +86,6 @@ inline void ZBarrier::self_heal(ZBarrierFastPath fast_path, volatile zpointer* p assert(ZPointer::is_remapped(heal_ptr), "invariant"); for (;;) { - if (ptr == zpointer::null) { - assert(!ZVerifyOops || !ZHeap::heap()->is_in(uintptr_t(p)) || !ZHeap::heap()->is_old(p), "No raw null in old"); - } - assert_transition_monotonicity(ptr, heal_ptr); // Heal diff --git a/src/hotspot/share/gc/z/zBarrierSet.cpp b/src/hotspot/share/gc/z/zBarrierSet.cpp index 87f93043bdf..643eba1947e 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.cpp +++ b/src/hotspot/share/gc/z/zBarrierSet.cpp @@ -223,27 +223,7 @@ void ZBarrierSet::on_slowpath_allocation_exit(JavaThread* thread, oop new_obj) { // breaks that promise. Take a few steps in the interpreter instead, which has // no such assumptions about where an object resides. deoptimize_allocation(thread); - return; } - - if (!ZGeneration::young()->is_phase_mark_complete()) { - return; - } - - if (!page->is_relocatable()) { - return; - } - - if (ZRelocate::compute_to_age(age) != ZPageAge::old) { - return; - } - - // If the object is young, we have to still be careful that it isn't racingly - // about to get promoted to the old generation. That causes issues when null - // pointers are supposed to be coloured, but the JIT is a bit sloppy and - // reinitializes memory with raw nulls. We detect this situation and detune - // rather than relying on the JIT to never be sloppy with redundant initialization. - deoptimize_allocation(thread); } void ZBarrierSet::print_on(outputStream* st) const { diff --git a/src/hotspot/share/gc/z/zGeneration.cpp b/src/hotspot/share/gc/z/zGeneration.cpp index d1680b6c336..2b632ef29a9 100644 --- a/src/hotspot/share/gc/z/zGeneration.cpp +++ b/src/hotspot/share/gc/z/zGeneration.cpp @@ -111,6 +111,16 @@ static const ZStatSampler ZSamplerJavaThreads("System", "Java Threads", ZStatUni ZGenerationYoung* ZGeneration::_young; ZGenerationOld* ZGeneration::_old; +class ZRendezvousHandshakeClosure : public HandshakeClosure { +public: + ZRendezvousHandshakeClosure() + : HandshakeClosure("ZRendezvous") {} + + void do_thread(Thread* thread) { + // Does nothing + } +}; + ZGeneration::ZGeneration(ZGenerationId id, ZPageTable* page_table, ZPageAllocator* page_allocator) : _id(id), _page_allocator(page_allocator), @@ -168,11 +178,19 @@ void ZGeneration::free_empty_pages(ZRelocationSetSelector* selector, int bulk) { } void ZGeneration::flip_age_pages(const ZRelocationSetSelector* selector) { - if (is_young()) { - _relocate.flip_age_pages(selector->not_selected_small()); - _relocate.flip_age_pages(selector->not_selected_medium()); - _relocate.flip_age_pages(selector->not_selected_large()); - } + _relocate.flip_age_pages(selector->not_selected_small()); + _relocate.flip_age_pages(selector->not_selected_medium()); + _relocate.flip_age_pages(selector->not_selected_large()); + + // Perform a handshake between flip promotion and running the promotion barrier. This ensures + // that ZBarrierSet::on_slowpath_allocation_exit() observing a young page that was then racingly + // flip promoted, will run any stores without barriers to completion before responding to the + // handshake at the subsequent safepoint poll. This ensures that the flip promotion barriers always + // run after compiled code missing barriers, but before relocate start. + ZRendezvousHandshakeClosure cl; + Handshake::execute(&cl); + + _relocate.barrier_flip_promoted_pages(_relocation_set.flip_promoted_pages()); } static double fragmentation_limit(ZGenerationId generation) { @@ -235,7 +253,9 @@ void ZGeneration::select_relocation_set(bool promote_all) { _relocation_set.install(&selector); // Flip age young pages that were not selected - flip_age_pages(&selector); + if (is_young()) { + flip_age_pages(&selector); + } // Setup forwarding table ZRelocationSetIterator rs_iter(&_relocation_set); @@ -1280,16 +1300,6 @@ bool ZGenerationOld::uses_clear_all_soft_reference_policy() const { return _reference_processor.uses_clear_all_soft_reference_policy(); } -class ZRendezvousHandshakeClosure : public HandshakeClosure { -public: - ZRendezvousHandshakeClosure() - : HandshakeClosure("ZRendezvous") {} - - void do_thread(Thread* thread) { - // Does nothing - } -}; - class ZRendezvousGCThreads: public VM_Operation { public: VMOp_Type type() const { return VMOp_ZRendezvousGCThreads; } diff --git a/src/hotspot/share/gc/z/zRelocate.cpp b/src/hotspot/share/gc/z/zRelocate.cpp index 69233da6f54..180ce22b041 100644 --- a/src/hotspot/share/gc/z/zRelocate.cpp +++ b/src/hotspot/share/gc/z/zRelocate.cpp @@ -1322,7 +1322,7 @@ private: public: ZFlipAgePagesTask(const ZArray* pages) - : ZTask("ZPromotePagesTask"), + : ZTask("ZFlipAgePagesTask"), _iter(pages) {} virtual void work() { @@ -1337,16 +1337,6 @@ public: // Figure out if this is proper promotion const bool promotion = to_age == ZPageAge::old; - if (promotion) { - // Before promoting an object (and before relocate start), we must ensure that all - // contained zpointers are store good. The marking code ensures that for non-null - // pointers, but null pointers are ignored. This code ensures that even null pointers - // are made store good, for the promoted objects. - prev_page->object_iterate([&](oop obj) { - ZIterator::basic_oop_iterate_safe(obj, ZBarrier::promote_barrier_on_young_oop_field); - }); - } - // Logging prev_page->log_msg(promotion ? " (flip promoted)" : " (flip survived)"); @@ -1360,7 +1350,7 @@ public: if (promotion) { ZGeneration::young()->flip_promote(prev_page, new_page); - // Defer promoted page registration times the lock is taken + // Defer promoted page registration promoted_pages.push(prev_page); } @@ -1371,11 +1361,42 @@ public: } }; +class ZPromoteBarrierTask : public ZTask { +private: + ZArrayParallelIterator _iter; + +public: + ZPromoteBarrierTask(const ZArray* pages) + : ZTask("ZPromoteBarrierTask"), + _iter(pages) {} + + virtual void work() { + SuspendibleThreadSetJoiner sts_joiner; + + for (ZPage* page; _iter.next(&page);) { + // When promoting an object (and before relocate start), we must ensure that all + // contained zpointers are store good. The marking code ensures that for non-null + // pointers, but null pointers are ignored. This code ensures that even null pointers + // are made store good, for the promoted objects. + page->object_iterate([&](oop obj) { + ZIterator::basic_oop_iterate_safe(obj, ZBarrier::promote_barrier_on_young_oop_field); + }); + + SuspendibleThreadSet::yield(); + } + } +}; + void ZRelocate::flip_age_pages(const ZArray* pages) { ZFlipAgePagesTask flip_age_task(pages); workers()->run(&flip_age_task); } +void ZRelocate::barrier_flip_promoted_pages(const ZArray* pages) { + ZPromoteBarrierTask promote_barrier_task(pages); + workers()->run(&promote_barrier_task); +} + void ZRelocate::synchronize() { _queue.synchronize(); } diff --git a/src/hotspot/share/gc/z/zRelocate.hpp b/src/hotspot/share/gc/z/zRelocate.hpp index d0ddf7deecf..50111f24ee5 100644 --- a/src/hotspot/share/gc/z/zRelocate.hpp +++ b/src/hotspot/share/gc/z/zRelocate.hpp @@ -119,6 +119,7 @@ public: void relocate(ZRelocationSet* relocation_set); void flip_age_pages(const ZArray* pages); + void barrier_flip_promoted_pages(const ZArray* pages); void synchronize(); void desynchronize(); From 45a2fd37f0ebda35789006b4e607422f7c369017 Mon Sep 17 00:00:00 2001 From: Weijun Wang Date: Thu, 20 Nov 2025 15:15:41 +0000 Subject: [PATCH 008/616] 8325448: Hybrid Public Key Encryption Reviewed-by: mullan, ascarpino, abarashev --- .../com/sun/crypto/provider/DHKEM.java | 361 +++++++---- .../classes/com/sun/crypto/provider/HPKE.java | 588 ++++++++++++++++++ .../com/sun/crypto/provider/SunJCE.java | 2 + .../javax/crypto/spec/HPKEParameterSpec.java | 443 +++++++++++++ .../spec/snippet-files/PackageSnippets.java | 76 +++ .../sun/security/util/SliceableSecretKey.java | 51 ++ .../provider/Cipher/HPKE/Compliance.java | 289 +++++++++ .../provider/Cipher/HPKE/Functions.java | 113 ++++ .../crypto/provider/Cipher/HPKE/KAT9180.java | 126 ++++ .../sun/crypto/provider/DHKEM/Compliance.java | 136 ++-- .../security/provider/all/Deterministic.java | 10 +- .../SliceableSecretKey/SoftSliceable.java | 153 +++++ 12 files changed, 2119 insertions(+), 229 deletions(-) create mode 100644 src/java.base/share/classes/com/sun/crypto/provider/HPKE.java create mode 100644 src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java create mode 100644 src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java create mode 100644 src/java.base/share/classes/sun/security/util/SliceableSecretKey.java create mode 100644 test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java create mode 100644 test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java create mode 100644 test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java create mode 100644 test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java diff --git a/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java index b27320ed24b..c7372a4c2c8 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/DHKEM.java @@ -26,26 +26,51 @@ package com.sun.crypto.provider; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.Serial; import java.math.BigInteger; -import java.security.*; -import java.security.interfaces.ECKey; +import java.security.AsymmetricKey; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; -import java.security.interfaces.XECKey; import java.security.interfaces.XECPublicKey; -import java.security.spec.*; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.NamedParameterSpec; +import java.security.spec.XECPrivateKeySpec; +import java.security.spec.XECPublicKeySpec; import java.util.Arrays; import java.util.Objects; -import javax.crypto.*; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.DecapsulateException; +import javax.crypto.KDF; +import javax.crypto.KEM; +import javax.crypto.KEMSpi; +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; import sun.security.jca.JCAUtil; -import sun.security.util.*; - -import jdk.internal.access.SharedSecrets; +import sun.security.util.ArrayUtil; +import sun.security.util.CurveDB; +import sun.security.util.ECUtil; +import sun.security.util.InternalPrivateKey; +import sun.security.util.NamedCurve; +import sun.security.util.SliceableSecretKey; // Implementing DHKEM defined inside https://www.rfc-editor.org/rfc/rfc9180.html, -// without the AuthEncap and AuthDecap functions public class DHKEM implements KEMSpi { private static final byte[] KEM = new byte[] @@ -65,80 +90,86 @@ public class DHKEM implements KEMSpi { private static final byte[] EMPTY = new byte[0]; private record Handler(Params params, SecureRandom secureRandom, - PrivateKey skR, PublicKey pkR) + PrivateKey skS, PublicKey pkS, // sender keys + PrivateKey skR, PublicKey pkR) // receiver keys implements EncapsulatorSpi, DecapsulatorSpi { @Override public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { - Objects.checkFromToIndex(from, to, params.Nsecret); + Objects.checkFromToIndex(from, to, params.nsecret); Objects.requireNonNull(algorithm, "null algorithm"); KeyPair kpE = params.generateKeyPair(secureRandom); PrivateKey skE = kpE.getPrivate(); PublicKey pkE = kpE.getPublic(); - byte[] pkEm = params.SerializePublicKey(pkE); - byte[] pkRm = params.SerializePublicKey(pkR); - byte[] kem_context = concat(pkEm, pkRm); - byte[] key = null; + byte[] pkEm = params.serializePublicKey(pkE); + byte[] pkRm = params.serializePublicKey(pkR); try { - byte[] dh = params.DH(skE, pkR); - key = params.ExtractAndExpand(dh, kem_context); - return new KEM.Encapsulated( - new SecretKeySpec(key, from, to - from, algorithm), - pkEm, null); + SecretKey key; + if (skS == null) { + byte[] kem_context = concat(pkEm, pkRm); + key = params.deriveKey(algorithm, from, to, kem_context, + params.dh(skE, pkR)); + } else { + byte[] pkSm = params.serializePublicKey(pkS); + byte[] kem_context = concat(pkEm, pkRm, pkSm); + key = params.deriveKey(algorithm, from, to, kem_context, + params.dh(skE, pkR), params.dh(skS, pkR)); + } + return new KEM.Encapsulated(key, pkEm, null); + } catch (UnsupportedOperationException e) { + throw e; } catch (Exception e) { throw new ProviderException("internal error", e); - } finally { - // `key` has been cloned into the `SecretKeySpec` within the - // returned `KEM.Encapsulated`, so it can now be cleared. - if (key != null) { - Arrays.fill(key, (byte)0); - } } } @Override public SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) throws DecapsulateException { - Objects.checkFromToIndex(from, to, params.Nsecret); + Objects.checkFromToIndex(from, to, params.nsecret); Objects.requireNonNull(algorithm, "null algorithm"); Objects.requireNonNull(encapsulation, "null encapsulation"); - if (encapsulation.length != params.Npk) { + if (encapsulation.length != params.npk) { throw new DecapsulateException("incorrect encapsulation size"); } - byte[] key = null; try { - PublicKey pkE = params.DeserializePublicKey(encapsulation); - byte[] dh = params.DH(skR, pkE); - byte[] pkRm = params.SerializePublicKey(pkR); - byte[] kem_context = concat(encapsulation, pkRm); - key = params.ExtractAndExpand(dh, kem_context); - return new SecretKeySpec(key, from, to - from, algorithm); + PublicKey pkE = params.deserializePublicKey(encapsulation); + byte[] pkRm = params.serializePublicKey(pkR); + if (pkS == null) { + byte[] kem_context = concat(encapsulation, pkRm); + return params.deriveKey(algorithm, from, to, kem_context, + params.dh(skR, pkE)); + } else { + byte[] pkSm = params.serializePublicKey(pkS); + byte[] kem_context = concat(encapsulation, pkRm, pkSm); + return params.deriveKey(algorithm, from, to, kem_context, + params.dh(skR, pkE), params.dh(skR, pkS)); + } + } catch (UnsupportedOperationException e) { + throw e; } catch (IOException | InvalidKeyException e) { throw new DecapsulateException("Cannot decapsulate", e); } catch (Exception e) { throw new ProviderException("internal error", e); - } finally { - if (key != null) { - Arrays.fill(key, (byte)0); - } } } @Override public int engineSecretSize() { - return params.Nsecret; + return params.nsecret; } @Override public int engineEncapsulationSize() { - return params.Npk; + return params.npk; } } // Not really a random. For KAT test only. It generates key pair from ikm. public static class RFC9180DeriveKeyPairSR extends SecureRandom { - static final long serialVersionUID = 0L; + @Serial + private static final long serialVersionUID = 0L; private final byte[] ikm; @@ -147,7 +178,7 @@ public class DHKEM implements KEMSpi { this.ikm = ikm; } - public KeyPair derive(Params params) { + private KeyPair derive(Params params) { try { return params.deriveKeyPair(ikm); } catch (Exception e) { @@ -183,9 +214,9 @@ public class DHKEM implements KEMSpi { ; private final int kem_id; - private final int Nsecret; - private final int Nsk; - private final int Npk; + private final int nsecret; + private final int nsk; + private final int npk; private final String kaAlgorithm; private final String keyAlgorithm; private final AlgorithmParameterSpec spec; @@ -193,18 +224,18 @@ public class DHKEM implements KEMSpi { private final byte[] suiteId; - Params(int kem_id, int Nsecret, int Nsk, int Npk, + Params(int kem_id, int nsecret, int nsk, int npk, String kaAlgorithm, String keyAlgorithm, AlgorithmParameterSpec spec, String hkdfAlgorithm) { this.kem_id = kem_id; this.spec = spec; - this.Nsecret = Nsecret; - this.Nsk = Nsk; - this.Npk = Npk; + this.nsecret = nsecret; + this.nsk = nsk; + this.npk = npk; this.kaAlgorithm = kaAlgorithm; this.keyAlgorithm = keyAlgorithm; this.hkdfAlgorithm = hkdfAlgorithm; - suiteId = concat(KEM, I2OSP(kem_id, 2)); + suiteId = concat(KEM, i2OSP(kem_id, 2)); } private boolean isEC() { @@ -224,18 +255,18 @@ public class DHKEM implements KEMSpi { } } - private byte[] SerializePublicKey(PublicKey k) { + private byte[] serializePublicKey(PublicKey k) { if (isEC()) { ECPoint w = ((ECPublicKey) k).getW(); return ECUtil.encodePoint(w, ((NamedCurve) spec).getCurve()); } else { byte[] uArray = ((XECPublicKey) k).getU().toByteArray(); ArrayUtil.reverse(uArray); - return Arrays.copyOf(uArray, Npk); + return Arrays.copyOf(uArray, npk); } } - private PublicKey DeserializePublicKey(byte[] data) + private PublicKey deserializePublicKey(byte[] data) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { KeySpec keySpec; if (isEC()) { @@ -251,29 +282,59 @@ public class DHKEM implements KEMSpi { return KeyFactory.getInstance(keyAlgorithm).generatePublic(keySpec); } - private byte[] DH(PrivateKey skE, PublicKey pkR) + private SecretKey dh(PrivateKey skE, PublicKey pkR) throws NoSuchAlgorithmException, InvalidKeyException { KeyAgreement ka = KeyAgreement.getInstance(kaAlgorithm); ka.init(skE); ka.doPhase(pkR, true); - return ka.generateSecret(); + return ka.generateSecret("Generic"); } - private byte[] ExtractAndExpand(byte[] dh, byte[] kem_context) - throws NoSuchAlgorithmException, InvalidKeyException { - KDF hkdf = KDF.getInstance(hkdfAlgorithm); - SecretKey eae_prk = LabeledExtract(hkdf, suiteId, EAE_PRK, dh); - try { - return LabeledExpand(hkdf, suiteId, eae_prk, SHARED_SECRET, - kem_context, Nsecret); - } finally { - if (eae_prk instanceof SecretKeySpec s) { - SharedSecrets.getJavaxCryptoSpecAccess() - .clearSecretKeySpec(s); + // The final shared secret derivation of either the encapsulator + // or the decapsulator. The key slicing is implemented inside. + // Throws UOE if a slice of the key cannot be found. + private SecretKey deriveKey(String alg, int from, int to, + byte[] kem_context, SecretKey... dhs) + throws NoSuchAlgorithmException { + if (from == 0 && to == nsecret) { + return extractAndExpand(kem_context, alg, dhs); + } else { + // First get shared secrets in "Generic" and then get a slice + // of it in the requested algorithm. + var fullKey = extractAndExpand(kem_context, "Generic", dhs); + if ("RAW".equalsIgnoreCase(fullKey.getFormat())) { + byte[] km = fullKey.getEncoded(); + if (km == null) { + // Should not happen if format is "RAW" + throw new UnsupportedOperationException("Key extract failed"); + } else { + try { + return new SecretKeySpec(km, from, to - from, alg); + } finally { + Arrays.fill(km, (byte)0); + } + } + } else if (fullKey instanceof SliceableSecretKey ssk) { + return ssk.slice(alg, from, to); + } else { + throw new UnsupportedOperationException("Cannot extract key"); } } } + private SecretKey extractAndExpand(byte[] kem_context, String alg, SecretKey... dhs) + throws NoSuchAlgorithmException { + var kdf = KDF.getInstance(hkdfAlgorithm); + var builder = labeledExtract(suiteId, EAE_PRK); + for (var dh : dhs) builder.addIKM(dh); + try { + return kdf.deriveKey(alg, + labeledExpand(builder, suiteId, SHARED_SECRET, kem_context, nsecret)); + } catch (InvalidAlgorithmParameterException e) { + throw new ProviderException(e); + } + } + private PublicKey getPublicKey(PrivateKey sk) throws InvalidKeyException { if (!(sk instanceof InternalPrivateKey)) { @@ -298,45 +359,37 @@ public class DHKEM implements KEMSpi { // For KAT tests only. See RFC9180DeriveKeyPairSR. public KeyPair deriveKeyPair(byte[] ikm) throws Exception { - KDF hkdf = KDF.getInstance(hkdfAlgorithm); - SecretKey dkp_prk = LabeledExtract(hkdf, suiteId, DKP_PRK, ikm); - try { - if (isEC()) { - NamedCurve curve = (NamedCurve) spec; - BigInteger sk = BigInteger.ZERO; - int counter = 0; - while (sk.signum() == 0 || - sk.compareTo(curve.getOrder()) >= 0) { - if (counter > 255) { - throw new RuntimeException(); - } - byte[] bytes = LabeledExpand(hkdf, suiteId, dkp_prk, - CANDIDATE, I2OSP(counter, 1), Nsk); - // bitmask is defined to be 0xFF for P-256 and P-384, - // and 0x01 for P-521 - if (this == Params.P521) { - bytes[0] = (byte) (bytes[0] & 0x01); - } - sk = new BigInteger(1, (bytes)); - counter = counter + 1; + var kdf = KDF.getInstance(hkdfAlgorithm); + var builder = labeledExtract(suiteId, DKP_PRK).addIKM(ikm); + if (isEC()) { + NamedCurve curve = (NamedCurve) spec; + BigInteger sk = BigInteger.ZERO; + int counter = 0; + while (sk.signum() == 0 || sk.compareTo(curve.getOrder()) >= 0) { + if (counter > 255) { + // So unlucky and should not happen + throw new ProviderException("DeriveKeyPairError"); } - PrivateKey k = DeserializePrivateKey(sk.toByteArray()); - return new KeyPair(getPublicKey(k), k); - } else { - byte[] sk = LabeledExpand(hkdf, suiteId, dkp_prk, SK, EMPTY, - Nsk); - PrivateKey k = DeserializePrivateKey(sk); - return new KeyPair(getPublicKey(k), k); - } - } finally { - if (dkp_prk instanceof SecretKeySpec s) { - SharedSecrets.getJavaxCryptoSpecAccess() - .clearSecretKeySpec(s); + byte[] bytes = kdf.deriveData(labeledExpand(builder, + suiteId, CANDIDATE, i2OSP(counter, 1), nsk)); + // bitmask is defined to be 0xFF for P-256 and P-384, and 0x01 for P-521 + if (this == Params.P521) { + bytes[0] = (byte) (bytes[0] & 0x01); + } + sk = new BigInteger(1, (bytes)); + counter = counter + 1; } + PrivateKey k = deserializePrivateKey(sk.toByteArray()); + return new KeyPair(getPublicKey(k), k); + } else { + byte[] sk = kdf.deriveData(labeledExpand(builder, + suiteId, SK, EMPTY, nsk)); + PrivateKey k = deserializePrivateKey(sk); + return new KeyPair(getPublicKey(k), k); } } - private PrivateKey DeserializePrivateKey(byte[] data) throws Exception { + private PrivateKey deserializePrivateKey(byte[] data) throws Exception { KeySpec keySpec = isEC() ? new ECPrivateKeySpec(new BigInteger(1, (data)), (NamedCurve) spec) : new XECPrivateKeySpec(spec, data); @@ -359,7 +412,22 @@ public class DHKEM implements KEMSpi { throw new InvalidAlgorithmParameterException("no spec needed"); } Params params = paramsFromKey(pk); - return new Handler(params, getSecureRandom(secureRandom), null, pk); + return new Handler(params, getSecureRandom(secureRandom), null, null, null, pk); + } + + // AuthEncap is not public KEM API + public EncapsulatorSpi engineNewAuthEncapsulator(PublicKey pkR, PrivateKey skS, + AlgorithmParameterSpec spec, SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (pkR == null || skS == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(pkR); + return new Handler(params, getSecureRandom(secureRandom), + skS, params.getPublicKey(skS), null, pkR); } @Override @@ -372,20 +440,34 @@ public class DHKEM implements KEMSpi { throw new InvalidAlgorithmParameterException("no spec needed"); } Params params = paramsFromKey(sk); - return new Handler(params, null, sk, params.getPublicKey(sk)); + return new Handler(params, null, null, null, sk, params.getPublicKey(sk)); } - private Params paramsFromKey(Key k) throws InvalidKeyException { - if (k instanceof ECKey eckey) { - if (ECUtil.equals(eckey.getParams(), CurveDB.P_256)) { + // AuthDecap is not public KEM API + public DecapsulatorSpi engineNewAuthDecapsulator( + PrivateKey skR, PublicKey pkS, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (skR == null || pkS == null) { + throw new InvalidKeyException("input key is null"); + } + if (spec != null) { + throw new InvalidAlgorithmParameterException("no spec needed"); + } + Params params = paramsFromKey(skR); + return new Handler(params, null, null, pkS, skR, params.getPublicKey(skR)); + } + + private Params paramsFromKey(AsymmetricKey k) throws InvalidKeyException { + var p = k.getParams(); + if (p instanceof ECParameterSpec ecp) { + if (ECUtil.equals(ecp, CurveDB.P_256)) { return Params.P256; - } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_384)) { + } else if (ECUtil.equals(ecp, CurveDB.P_384)) { return Params.P384; - } else if (ECUtil.equals(eckey.getParams(), CurveDB.P_521)) { + } else if (ECUtil.equals(ecp, CurveDB.P_521)) { return Params.P521; } - } else if (k instanceof XECKey xkey - && xkey.getParams() instanceof NamedParameterSpec ns) { + } else if (p instanceof NamedParameterSpec ns) { if (ns.getName().equalsIgnoreCase("X25519")) { return Params.X25519; } else if (ns.getName().equalsIgnoreCase("X448")) { @@ -401,8 +483,11 @@ public class DHKEM implements KEMSpi { return o.toByteArray(); } - private static byte[] I2OSP(int n, int w) { - assert n < 256; + // I2OSP(n, w) as defined in RFC 9180 Section 3. + // In DHKEM and HPKE, number is always <65536 + // and converted to at most 2 bytes. + public static byte[] i2OSP(int n, int w) { + assert n < 65536; assert w == 1 || w == 2; if (w == 1) { return new byte[] { (byte) n }; @@ -411,32 +496,32 @@ public class DHKEM implements KEMSpi { } } - private static SecretKey LabeledExtract(KDF hkdf, byte[] suite_id, - byte[] label, byte[] ikm) throws InvalidKeyException { - SecretKeySpec s = new SecretKeySpec(concat(HPKE_V1, suite_id, label, - ikm), "IKM"); - try { - HKDFParameterSpec spec = - HKDFParameterSpec.ofExtract().addIKM(s).extractOnly(); - return hkdf.deriveKey("Generic", spec); - } catch (InvalidAlgorithmParameterException | - NoSuchAlgorithmException e) { - throw new InvalidKeyException(e.getMessage(), e); - } finally { - SharedSecrets.getJavaxCryptoSpecAccess().clearSecretKeySpec(s); - } + // Create a LabeledExtract builder with labels. + // You can add more IKM and salt into the result. + public static HKDFParameterSpec.Builder labeledExtract( + byte[] suiteId, byte[] label) { + return HKDFParameterSpec.ofExtract() + .addIKM(HPKE_V1).addIKM(suiteId).addIKM(label); } - private static byte[] LabeledExpand(KDF hkdf, byte[] suite_id, - SecretKey prk, byte[] label, byte[] info, int L) - throws InvalidKeyException { - byte[] labeled_info = concat(I2OSP(L, 2), HPKE_V1, suite_id, label, - info); - try { - return hkdf.deriveData(HKDFParameterSpec.expandOnly( - prk, labeled_info, L)); - } catch (InvalidAlgorithmParameterException iape) { - throw new InvalidKeyException(iape.getMessage(), iape); - } + // Create a labeled info from info and labels + private static byte[] labeledInfo( + byte[] suiteId, byte[] label, byte[] info, int length) { + return concat(i2OSP(length, 2), HPKE_V1, suiteId, label, info); + } + + // LabeledExpand from a builder + public static HKDFParameterSpec labeledExpand( + HKDFParameterSpec.Builder builder, + byte[] suiteId, byte[] label, byte[] info, int length) { + return builder.thenExpand( + labeledInfo(suiteId, label, info, length), length); + } + + // LabeledExpand from a prk + public static HKDFParameterSpec labeledExpand( + SecretKey prk, byte[] suiteId, byte[] label, byte[] info, int length) { + return HKDFParameterSpec.expandOnly( + prk, labeledInfo(suiteId, label, info, length), length); } } diff --git a/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java b/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java new file mode 100644 index 00000000000..eee5f59cc75 --- /dev/null +++ b/src/java.base/share/classes/com/sun/crypto/provider/HPKE.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 com.sun.crypto.provider; + +import sun.security.util.CurveDB; +import sun.security.util.ECUtil; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.DecapsulateException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KDF; +import javax.crypto.KEM; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HPKEParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.AsymmetricKey; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.NamedParameterSpec; +import java.util.Arrays; + +public class HPKE extends CipherSpi { + + private static final byte[] HPKE = new byte[] + {'H', 'P', 'K', 'E'}; + private static final byte[] SEC = new byte[] + {'s', 'e', 'c'}; + private static final byte[] PSK_ID_HASH = new byte[] + {'p', 's', 'k', '_', 'i', 'd', '_', 'h', 'a', 's', 'h'}; + private static final byte[] INFO_HASH = new byte[] + {'i', 'n', 'f', 'o', '_', 'h', 'a', 's', 'h'}; + private static final byte[] SECRET = new byte[] + {'s', 'e', 'c', 'r', 'e', 't'}; + private static final byte[] EXP = new byte[] + {'e', 'x', 'p'}; + private static final byte[] KEY = new byte[] + {'k', 'e', 'y'}; + private static final byte[] BASE_NONCE = new byte[] + {'b', 'a', 's', 'e', '_', 'n', 'o', 'n', 'c', 'e'}; + + private static final int BEGIN = 1; + private static final int EXPORT_ONLY = 2; // init done with aead_id == 65535 + private static final int ENCRYPT_AND_EXPORT = 3; // int done with AEAD + private static final int AFTER_FINAL = 4; // after doFinal, need reinit internal cipher + + private int state = BEGIN; + private Impl impl; + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException(mode); + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + throw new NoSuchPaddingException(padding); + } + + @Override + protected int engineGetBlockSize() { + if (state == ENCRYPT_AND_EXPORT || state == AFTER_FINAL) { + return impl.aead.cipher.getBlockSize(); + } else { + return 0; + } + } + + @Override + protected int engineGetOutputSize(int inputLen) { + if (state == ENCRYPT_AND_EXPORT || state == AFTER_FINAL) { + return impl.aead.cipher.getOutputSize(inputLen); + } else { + return 0; + } + } + + @Override + protected byte[] engineGetIV() { + return (state == BEGIN || impl.kemEncaps == null) + ? null : impl.kemEncaps.clone(); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + throw new InvalidKeyException("HPKEParameterSpec must be provided"); + } + + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + impl = new Impl(opmode); + if (!(key instanceof AsymmetricKey ak)) { + throw new InvalidKeyException("Not an asymmetric key"); + } + if (params == null) { + throw new InvalidAlgorithmParameterException( + "HPKEParameterSpec must be provided"); + } else if (params instanceof HPKEParameterSpec hps) { + impl.init(ak, hps, random); + } else { + throw new InvalidAlgorithmParameterException( + "Unsupported params type: " + params.getClass()); + } + if (impl.hasEncrypt()) { + impl.aead.start(impl.opmode, impl.context.k, impl.context.computeNonce()); + state = ENCRYPT_AND_EXPORT; + } else { + state = EXPORT_ONLY; + } + } + + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + throw new InvalidKeyException("HPKEParameterSpec must be provided"); + } + + // state is ENCRYPT_AND_EXPORT after this call succeeds + private void maybeReinitInternalCipher() { + if (state == BEGIN) { + throw new IllegalStateException("Illegal state: " + state); + } + if (state == EXPORT_ONLY) { + throw new UnsupportedOperationException(); + } + if (state == AFTER_FINAL) { + impl.aead.start(impl.opmode, impl.context.k, impl.context.computeNonce()); + state = ENCRYPT_AND_EXPORT; + } + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + maybeReinitInternalCipher(); + return impl.aead.cipher.update(input, inputOffset, inputLen); + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException { + maybeReinitInternalCipher(); + return impl.aead.cipher.update( + input, inputOffset, inputLen, output, outputOffset); + } + + @Override + protected void engineUpdateAAD(byte[] src, int offset, int len) { + maybeReinitInternalCipher(); + impl.aead.cipher.updateAAD(src, offset, len); + } + + @Override + protected void engineUpdateAAD(ByteBuffer src) { + maybeReinitInternalCipher(); + impl.aead.cipher.updateAAD(src); + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + maybeReinitInternalCipher(); + impl.context.IncrementSeq(); + state = AFTER_FINAL; + if (input == null) { // a bug in doFinal(null, ?, ?) + return impl.aead.cipher.doFinal(); + } else { + return impl.aead.cipher.doFinal(input, inputOffset, inputLen); + } + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException, + IllegalBlockSizeException, BadPaddingException { + maybeReinitInternalCipher(); + impl.context.IncrementSeq(); + state = AFTER_FINAL; + return impl.aead.cipher.doFinal( + input, inputOffset, inputLen, output, outputOffset); + } + + //@Override + protected SecretKey engineExportKey(String algorithm, byte[] context, int length) { + if (state == BEGIN) { + throw new IllegalStateException("State: " + state); + } else { + return impl.context.exportKey(algorithm, context, length); + } + } + + //@Override + protected byte[] engineExportData(byte[] context, int length) { + if (state == BEGIN) { + throw new IllegalStateException("State: " + state); + } else { + return impl.context.exportData(context, length); + } + } + + private static class AEAD { + final Cipher cipher; + final int nk, nn, nt; + final int id; + public AEAD(int id) throws InvalidAlgorithmParameterException { + this.id = id; + try { + switch (id) { + case HPKEParameterSpec.AEAD_AES_128_GCM -> { + cipher = Cipher.getInstance("AES/GCM/NoPadding"); + nk = 16; + } + case HPKEParameterSpec.AEAD_AES_256_GCM -> { + cipher = Cipher.getInstance("AES/GCM/NoPadding"); + nk = 32; + } + case HPKEParameterSpec.AEAD_CHACHA20_POLY1305 -> { + cipher = Cipher.getInstance("ChaCha20-Poly1305"); + nk = 32; + } + case HPKEParameterSpec.EXPORT_ONLY -> { + cipher = null; + nk = -1; + } + default -> throw new InvalidAlgorithmParameterException( + "Unknown aead_id: " + id); + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new ProviderException("Internal error", e); + } + nn = 12; nt = 16; + } + + void start(int opmode, SecretKey key, byte[] nonce) { + try { + if (id == HPKEParameterSpec.AEAD_CHACHA20_POLY1305) { + cipher.init(opmode, key, new IvParameterSpec(nonce)); + } else { + cipher.init(opmode, key, new GCMParameterSpec(nt * 8, nonce)); + } + } catch (InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new ProviderException("Internal error", e); + } + } + } + + private static class Impl { + + final int opmode; + + HPKEParameterSpec params; + Context context; + AEAD aead; + + byte[] suite_id; + String kdfAlg; + int kdfNh; + + // only used on sender side + byte[] kemEncaps; + + class Context { + final SecretKey k; // null if only export + final byte[] base_nonce; + final SecretKey exporter_secret; + + byte[] seq = new byte[aead.nn]; + + public Context(SecretKey sk, byte[] base_nonce, + SecretKey exporter_secret) { + this.k = sk; + this.base_nonce = base_nonce; + this.exporter_secret = exporter_secret; + } + + SecretKey exportKey(String algorithm, byte[] exporter_context, int length) { + if (exporter_context == null) { + throw new IllegalArgumentException("Null exporter_context"); + } + try { + var kdf = KDF.getInstance(kdfAlg); + return kdf.deriveKey(algorithm, DHKEM.labeledExpand( + exporter_secret, suite_id, SEC, exporter_context, length)); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + // algorithm not accepted by HKDF, length too big or too small + throw new IllegalArgumentException("Invalid input", e); + } + } + + byte[] exportData(byte[] exporter_context, int length) { + if (exporter_context == null) { + throw new IllegalArgumentException("Null exporter_context"); + } + try { + var kdf = KDF.getInstance(kdfAlg); + return kdf.deriveData(DHKEM.labeledExpand( + exporter_secret, suite_id, SEC, exporter_context, length)); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + // algorithm not accepted by HKDF, length too big or too small + throw new IllegalArgumentException("Invalid input", e); + } + } + + private byte[] computeNonce() { + var result = new byte[aead.nn]; + for (var i = 0; i < result.length; i++) { + result[i] = (byte)(seq[i] ^ base_nonce[i]); + } + return result; + } + + private void IncrementSeq() { + for (var i = seq.length - 1; i >= 0; i--) { + if ((seq[i] & 0xff) == 0xff) { + seq[i] = 0; + } else { + seq[i]++; + return; + } + } + // seq >= (1 << (8*aead.Nn)) - 1 when this method is called + throw new ProviderException("MessageLimitReachedError"); + } + } + + public Impl(int opmode) { + this.opmode = opmode; + } + + public boolean hasEncrypt() { + return params.aead_id() != 65535; + } + + // Section 7.2.1 of RFC 9180 has restrictions on size of psk, psk_id, + // info, and exporter_context (~2^61 for HMAC-SHA256 and ~2^125 for + // HMAC-SHA384 and HMAC-SHA512). This method does not pose any + // restrictions. + public void init(AsymmetricKey key, HPKEParameterSpec p, SecureRandom rand) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (opmode != Cipher.ENCRYPT_MODE && opmode != Cipher.DECRYPT_MODE) { + throw new UnsupportedOperationException( + "Can only be used for encryption and decryption"); + } + setParams(p); + SecretKey shared_secret; + if (opmode == Cipher.ENCRYPT_MODE) { + if (!(key instanceof PublicKey pk)) { + throw new InvalidKeyException( + "Cannot encrypt with private key"); + } + if (p.encapsulation() != null) { + throw new InvalidAlgorithmParameterException( + "Must not provide key encapsulation message on sender side"); + } + checkMatch(false, pk, params.kem_id()); + KEM.Encapsulated enc; + switch (p.authKey()) { + case null -> { + var e = kem().newEncapsulator(pk, rand); + enc = e.encapsulate(); + } + case PrivateKey skS -> { + checkMatch(true, skS, params.kem_id()); + // AuthEncap not public KEM API but it's internally supported + var e = new DHKEM().engineNewAuthEncapsulator(pk, skS, null, rand); + enc = e.engineEncapsulate(0, e.engineSecretSize(), "Generic"); + } + default -> throw new InvalidAlgorithmParameterException( + "Cannot auth with public key"); + } + kemEncaps = enc.encapsulation(); + shared_secret = enc.key(); + } else { + if (!(key instanceof PrivateKey sk)) { + throw new InvalidKeyException("Cannot decrypt with public key"); + } + checkMatch(false, sk, params.kem_id()); + try { + var encap = p.encapsulation(); + if (encap == null) { + throw new InvalidAlgorithmParameterException( + "Must provide key encapsulation message on recipient side"); + } + switch (p.authKey()) { + case null -> { + var d = kem().newDecapsulator(sk); + shared_secret = d.decapsulate(encap); + } + case PublicKey pkS -> { + checkMatch(true, pkS, params.kem_id()); + // AuthDecap not public KEM API but it's internally supported + var d = new DHKEM().engineNewAuthDecapsulator(sk, pkS, null); + shared_secret = d.engineDecapsulate( + encap, 0, d.engineSecretSize(), "Generic"); + } + default -> throw new InvalidAlgorithmParameterException( + "Cannot auth with private key"); + } + } catch (DecapsulateException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + var usePSK = usePSK(params.psk()); + int mode = params.authKey() == null ? (usePSK ? 1 : 0) : (usePSK ? 3 : 2); + context = keySchedule(mode, shared_secret, + params.info(), + params.psk(), + params.psk_id()); + } + + private static void checkMatch(boolean inSpec, AsymmetricKey k, int kem_id) + throws InvalidKeyException, InvalidAlgorithmParameterException { + var p = k.getParams(); + switch (p) { + case ECParameterSpec ecp -> { + if ((!ECUtil.equals(ecp, CurveDB.P_256) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_256_HKDF_SHA256) + && (!ECUtil.equals(ecp, CurveDB.P_384) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_384_HKDF_SHA384) + && (!ECUtil.equals(ecp, CurveDB.P_521) + || kem_id != HPKEParameterSpec.KEM_DHKEM_P_521_HKDF_SHA512)) { + var name = ECUtil.getCurveName(ecp); + throw new InvalidAlgorithmParameterException( + name + " does not match " + kem_id); + } + } + case NamedParameterSpec ns -> { + var name = ns.getName(); + if ((!name.equalsIgnoreCase("x25519") + || kem_id != HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256) + && (!name.equalsIgnoreCase("x448") + || kem_id != HPKEParameterSpec.KEM_DHKEM_X448_HKDF_SHA512)) { + throw new InvalidAlgorithmParameterException( + name + " does not match " + kem_id); + } + } + case null, default -> { + var msg = k.getClass() + " does not match " + kem_id; + if (inSpec) { + throw new InvalidAlgorithmParameterException(msg); + } else { + throw new InvalidKeyException(msg); + } + } + } + } + + private KEM kem() { + try { + return KEM.getInstance("DHKEM"); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException("Internal error", e); + } + } + + private void setParams(HPKEParameterSpec p) + throws InvalidAlgorithmParameterException { + params = p; + suite_id = concat( + HPKE, + DHKEM.i2OSP(params.kem_id(), 2), + DHKEM.i2OSP(params.kdf_id(), 2), + DHKEM.i2OSP(params.aead_id(), 2)); + switch (params.kdf_id()) { + case HPKEParameterSpec.KDF_HKDF_SHA256 -> { + kdfAlg = "HKDF-SHA256"; + kdfNh = 32; + } + case HPKEParameterSpec.KDF_HKDF_SHA384 -> { + kdfAlg = "HKDF-SHA384"; + kdfNh = 48; + } + case HPKEParameterSpec.KDF_HKDF_SHA512 -> { + kdfAlg = "HKDF-SHA512"; + kdfNh = 64; + } + default -> throw new InvalidAlgorithmParameterException( + "Unsupported kdf_id: " + params.kdf_id()); + } + aead = new AEAD(params.aead_id()); + } + + private Context keySchedule(int mode, + SecretKey shared_secret, + byte[] info, + SecretKey psk, + byte[] psk_id) { + try { + var psk_id_hash_x = DHKEM.labeledExtract(suite_id, PSK_ID_HASH) + .addIKM(psk_id).extractOnly(); + var info_hash_x = DHKEM.labeledExtract(suite_id, INFO_HASH) + .addIKM(info).extractOnly(); + + // deriveData must and can be called because all info to + // thw builder are just byte arrays. Any KDF impl can handle this. + var kdf = KDF.getInstance(kdfAlg); + var key_schedule_context = concat(new byte[]{(byte) mode}, + kdf.deriveData(psk_id_hash_x), + kdf.deriveData(info_hash_x)); + + var secret_x_builder = DHKEM.labeledExtract(suite_id, SECRET); + if (psk != null) { + secret_x_builder.addIKM(psk); + } + secret_x_builder.addSalt(shared_secret); + var secret_x = kdf.deriveKey("Generic", secret_x_builder.extractOnly()); + + // A new KDF object must be created because secret_x_builder + // might contain provider-specific keys which the previous + // KDF (provider already chosen) cannot handle. + kdf = KDF.getInstance(kdfAlg); + var exporter_secret = kdf.deriveKey("Generic", DHKEM.labeledExpand( + secret_x, suite_id, EXP, key_schedule_context, kdfNh)); + + if (hasEncrypt()) { + // ChaCha20-Poly1305 does not care about algorithm name + var key = kdf.deriveKey("AES", DHKEM.labeledExpand(secret_x, + suite_id, KEY, key_schedule_context, aead.nk)); + // deriveData must be called because we need to increment nonce + var base_nonce = kdf.deriveData(DHKEM.labeledExpand(secret_x, + suite_id, BASE_NONCE, key_schedule_context, aead.nn)); + return new Context(key, base_nonce, exporter_secret); + } else { + return new Context(null, null, exporter_secret); + } + } catch (InvalidAlgorithmParameterException + | NoSuchAlgorithmException | UnsupportedOperationException e) { + throw new ProviderException("Internal error", e); + } + } + } + + private static boolean usePSK(SecretKey psk) { + return psk != null; + } + + private static byte[] concat(byte[]... inputs) { + var o = new ByteArrayOutputStream(); + Arrays.stream(inputs).forEach(o::writeBytes); + return o.toByteArray(); + } +} diff --git a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java index 22d5f17c6e0..4b38bd55809 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java @@ -371,6 +371,8 @@ public final class SunJCE extends Provider { ps("Cipher", "PBEWithHmacSHA512/256AndAES_256", "com.sun.crypto.provider.PBES2Core$HmacSHA512_256AndAES_256"); + ps("Cipher", "HPKE", "com.sun.crypto.provider.HPKE"); + /* * Key(pair) Generator engines */ diff --git a/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java b/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java new file mode 100644 index 00000000000..6776ddcdb75 --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/spec/HPKEParameterSpec.java @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 javax.crypto.spec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.AsymmetricKey; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Objects; + +/** + * This immutable class specifies the set of parameters used with a {@code Cipher} for the + * Hybrid Public Key Encryption + * (HPKE) algorithm. HPKE is a public key encryption scheme for encrypting + * arbitrary-sized plaintexts with a recipient's public key. It combines a key + * encapsulation mechanism (KEM), a key derivation function (KDF), and an + * authenticated encryption with additional data (AEAD) cipher. + *

+ * The + * standard algorithm name for the cipher is "HPKE". Unlike most other + * ciphers, HPKE is not expressed as a transformation string of the form + * "algorithm/mode/padding". Therefore, the argument to {@code Cipher.getInstance} + * must be the single algorithm name "HPKE". + *

+ * In HPKE, the sender's {@code Cipher} is always initialized with the + * recipient's public key in {@linkplain Cipher#ENCRYPT_MODE encrypt mode}, + * while the recipient's {@code Cipher} object is initialized with its own + * private key in {@linkplain Cipher#DECRYPT_MODE decrypt mode}. + *

+ * An {@code HPKEParameterSpec} object must be provided at HPKE + * {@linkplain Cipher#init(int, Key, AlgorithmParameterSpec) cipher initialization}. + *

+ * The {@link #of(int, int, int)} static method returns an {@code HPKEParameterSpec} + * object with the specified KEM, KDF, and AEAD algorithm identifiers. + * The terms "KEM algorithm identifiers", "KDF algorithm identifiers", and + * "AEAD algorithm identifiers" refer to their respective numeric values + * (specifically, {@code kem_id}, {@code kdf_id}, and {@code aead_id}) as + * defined in Section 7 + * of RFC 9180 and maintained on the + * IANA HPKE page. + *

+ * Once an {@code HPKEParameterSpec} object is created, additional methods + * are available to generate new {@code HPKEParameterSpec} objects with + * different features: + *

    + *
  • + * Application-supplied information can be provided using the + * {@link #withInfo(byte[])} method by both sides. + *
  • + * To authenticate using a pre-shared key ({@code mode_psk}), the + * pre-shared key and its identifier must be provided using the + * {@link #withPsk(SecretKey, byte[])} method by both sides. + *
  • + * To authenticate using an asymmetric key ({@code mode_auth}), + * the asymmetric keys must be provided using the {@link #withAuthKey(AsymmetricKey)} + * method. Precisely, the sender must call this method with its own private key + * and the recipient must call it with the sender's public key. + *
  • + * To authenticate using both a PSK and an asymmetric key + * ({@code mode_auth_psk}), both {@link #withAuthKey(AsymmetricKey)} and + * {@link #withPsk(SecretKey, byte[])} methods must be called as described above. + *
  • + * In HPKE, a shared secret is negotiated during the KEM step and a key + * encapsulation message must be transmitted from the sender to the recipient + * so that the recipient can recover the shared secret. On the sender side, + * after the cipher is initialized, the key encapsulation message can be + * retrieved using the {@link Cipher#getIV()} method. On the recipient side, + * this message must be supplied as part of an {@code HPKEParameterSpec} + * object obtained from the {@link #withEncapsulation(byte[])} method. + *
+ * For successful interoperability, both sides need to have identical algorithm + * identifiers, and supply identical + * {@code info}, {@code psk}, and {@code psk_id} or matching authentication + * keys if provided. For details about HPKE modes, refer to + * Section 5 + * of RFC 9180. + *

+ * If an HPKE cipher is {@linkplain Cipher#init(int, Key) initialized without + * parameters}, an {@code InvalidKeyException} is thrown. + *

+ * At HPKE cipher initialization, if no HPKE implementation supports the + * provided key type, an {@code InvalidKeyException} is thrown. If the provided + * {@code HPKEParameterSpec} is not accepted by any HPKE implementation, + * an {@code InvalidAlgorithmParameterException} is thrown. For example: + *

    + *
  • An algorithm identifier is unsupported or does not match the provided key type. + *
  • A key encapsulation message is provided on the sender side. + *
  • A key encapsulation message is not provided on the recipient side. + *
  • An attempt to use {@code withAuthKey(key)} is made with an incompatible key. + *
  • An attempt to use {@code withAuthKey(key)} is made but {@code mode_auth} + * or {@code mode_auth_psk} is not supported by the KEM algorithm used. + *
+ * After initialization, both the sender and recipient can process multiple + * messages in sequence with repeated {@code doFinal} calls, optionally preceded + * by one or more {@code updateAAD} and {@code update}. Each {@code doFinal} + * performs a complete HPKE encryption or decryption operation using a distinct + * IV derived from an internal sequence counter, as specified in + * Section 5.2 + * of RFC 9180. On the recipient side, each {@code doFinal} call must correspond + * to exactly one complete ciphertext, and the number and order of calls must + * match those on the sender side. This differs from the direct use of an AEAD + * cipher, where the caller must provide a fresh IV and reinitialize the cipher + * for each message. By managing IVs internally, HPKE allows a single + * initialization to support multiple messages while still ensuring IV + * uniqueness and preserving AEAD security guarantees. + *

+ * This example shows a sender and a recipient using HPKE to securely exchange + * messages with an X25519 key pair. + * {@snippet lang=java class="PackageSnippets" region="hpke-spec-example"} + * + * @implNote This class defines constants for some of the standard algorithm + * identifiers such as {@link #KEM_DHKEM_P_256_HKDF_SHA256}, + * {@link #KDF_HKDF_SHA256}, and {@link #AEAD_AES_128_GCM}. An HPKE {@code Cipher} + * implementation may support all, some, or none of the algorithm identifiers + * defined here. An implementation may also support additional identifiers not + * listed here, including private or experimental values. + * + * @spec https://www.rfc-editor.org/info/rfc9180 + * RFC 9180: Hybrid Public Key Encryption + * @spec security/standard-names.html + * Java Security Standard Algorithm Names + * @since 26 + */ +public final class HPKEParameterSpec implements AlgorithmParameterSpec { + + /** + * KEM algorithm identifier for DHKEM(P-256, HKDF-SHA256) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_256_HKDF_SHA256 = 0x10; + + /** + * KEM algorithm identifier for DHKEM(P-384, HKDF-SHA384) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_384_HKDF_SHA384 = 0x11; + + /** + * KEM algorithm identifier for DHKEM(P-521, HKDF-SHA512) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_P_521_HKDF_SHA512 = 0x12; + + /** + * KEM algorithm identifier for DHKEM(X25519, HKDF-SHA256) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_X25519_HKDF_SHA256 = 0x20; + + /** + * KEM algorithm identifier for DHKEM(X448, HKDF-SHA512) as defined in RFC 9180. + */ + public static final int KEM_DHKEM_X448_HKDF_SHA512 = 0x21; + + /** + * KDF algorithm identifier for HKDF-SHA256 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA256 = 0x1; + + /** + * KDF algorithm identifier for HKDF-SHA384 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA384 = 0x2; + + /** + * KDF algorithm identifier for HKDF-SHA512 as defined in RFC 9180. + */ + public static final int KDF_HKDF_SHA512 = 0x3; + + /** + * AEAD algorithm identifier for AES-128-GCM as defined in RFC 9180. + */ + public static final int AEAD_AES_128_GCM = 0x1; + + /** + * AEAD algorithm identifier for AES-256-GCM as defined in RFC 9180. + */ + public static final int AEAD_AES_256_GCM = 0x2; + + /** + * AEAD algorithm identifier for ChaCha20Poly1305 as defined in RFC 9180. + */ + public static final int AEAD_CHACHA20_POLY1305 = 0x3; + + /** + * AEAD algorithm identifier for Export-only as defined in RFC 9180. + */ + public static final int EXPORT_ONLY = 0xffff; + + private final int kem_id; + private final int kdf_id; + private final int aead_id; + private final byte[] info; // never null, can be empty + private final SecretKey psk; // null if not used + private final byte[] psk_id; // never null, can be empty + private final AsymmetricKey kS; // null if not used + private final byte[] encapsulation; // null if none + + // Note: this constructor does not clone array arguments. + private HPKEParameterSpec(int kem_id, int kdf_id, int aead_id, byte[] info, + SecretKey psk, byte[] psk_id, AsymmetricKey kS, byte[] encapsulation) { + this.kem_id = kem_id; + this.kdf_id = kdf_id; + this.aead_id = aead_id; + this.info = info; + this.psk = psk; + this.psk_id = psk_id; + this.kS = kS; + this.encapsulation = encapsulation; + } + + /** + * A factory method to create a new {@code HPKEParameterSpec} object with + * specified KEM, KDF, and AEAD algorithm identifiers in {@code mode_base} + * mode with an empty {@code info}. + * + * @param kem_id algorithm identifier for KEM, must be between 0 and 65535 (inclusive) + * @param kdf_id algorithm identifier for KDF, must be between 0 and 65535 (inclusive) + * @param aead_id algorithm identifier for AEAD, must be between 0 and 65535 (inclusive) + * @return a new {@code HPKEParameterSpec} object + * @throws IllegalArgumentException if any input value + * is out of range (must be between 0 and 65535, inclusive). + */ + public static HPKEParameterSpec of(int kem_id, int kdf_id, int aead_id) { + if (kem_id < 0 || kem_id > 65535) { + throw new IllegalArgumentException("Invalid kem_id: " + kem_id); + } + if (kdf_id < 0 || kdf_id > 65535) { + throw new IllegalArgumentException("Invalid kdf_id: " + kdf_id); + } + if (aead_id < 0 || aead_id > 65535) { + throw new IllegalArgumentException("Invalid aead_id: " + aead_id); + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + new byte[0], null, new byte[0], null, null); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * {@code info} value. + *

+ * For interoperability, RFC 9180 Section 7.2.1 recommends limiting + * this value to a maximum of 64 bytes. + * + * @param info application-supplied information. + * The contents of the array are copied to protect + * against subsequent modification. + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code info} is {@code null} + * @throws IllegalArgumentException if {@code info} is empty. + */ + public HPKEParameterSpec withInfo(byte[] info) { + Objects.requireNonNull(info); + if (info.length == 0) { + throw new IllegalArgumentException("info is empty"); + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info.clone(), psk, psk_id, kS, encapsulation); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * {@code psk} and {@code psk_id} values. + *

+ * RFC 9180 Section 5.1.2 requires the PSK MUST have at least 32 bytes + * of entropy. For interoperability, RFC 9180 Section 7.2.1 recommends + * limiting the key size and identifier length to a maximum of 64 bytes. + * + * @param psk pre-shared key + * @param psk_id identifier for PSK. The contents of the array are copied + * to protect against subsequent modification. + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code psk} or {@code psk_id} is {@code null} + * @throws IllegalArgumentException if {@code psk} is shorter than 32 bytes + * or {@code psk_id} is empty + */ + public HPKEParameterSpec withPsk(SecretKey psk, byte[] psk_id) { + Objects.requireNonNull(psk); + Objects.requireNonNull(psk_id); + if (psk_id.length == 0) { + throw new IllegalArgumentException("psk_id is empty"); + } + if ("RAW".equalsIgnoreCase(psk.getFormat())) { + // We can only check when psk is extractable. We can only + // check the length and not the real entropy size + var keyBytes = psk.getEncoded(); + assert keyBytes != null; + Arrays.fill(keyBytes, (byte)0); + if (keyBytes.length < 32) { + throw new IllegalArgumentException("psk is too short"); + } + } + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info, psk, psk_id.clone(), kS, encapsulation); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * key encapsulation message value that will be used by the recipient. + * + * @param encapsulation the key encapsulation message. + * The contents of the array are copied to protect against + * subsequent modification. + * + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code encapsulation} is {@code null} + */ + public HPKEParameterSpec withEncapsulation(byte[] encapsulation) { + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info, psk, psk_id, kS, + Objects.requireNonNull(encapsulation).clone()); + } + + /** + * Creates a new {@code HPKEParameterSpec} object with the specified + * authentication key value. + *

+ * Note: this method does not check whether the KEM algorithm supports + * {@code mode_auth} or {@code mode_auth_psk}. If the resulting object is + * used to initialize an HPKE cipher with an unsupported mode, an + * {@code InvalidAlgorithmParameterException} will be thrown at that time. + * + * @param kS the authentication key + * @return a new {@code HPKEParameterSpec} object + * @throws NullPointerException if {@code kS} is {@code null} + */ + public HPKEParameterSpec withAuthKey(AsymmetricKey kS) { + return new HPKEParameterSpec(kem_id, kdf_id, aead_id, + info, psk, psk_id, + Objects.requireNonNull(kS), + encapsulation); + } + + /** + * {@return the algorithm identifier for KEM } + */ + public int kem_id() { + return kem_id; + } + + /** + * {@return the algorithm identifier for KDF } + */ + public int kdf_id() { + return kdf_id; + } + + /** + * {@return the algorithm identifier for AEAD } + */ + public int aead_id() { + return aead_id; + } + + /** + * {@return a copy of the application-supplied information, empty if none} + */ + public byte[] info() { + return info.clone(); + } + + /** + * {@return pre-shared key, {@code null} if none} + */ + public SecretKey psk() { + return psk; + } + + /** + * {@return a copy of the identifier for PSK, empty if none} + */ + public byte[] psk_id() { + return psk_id.clone(); + } + + /** + * {@return the key for authentication, {@code null} if none} + */ + public AsymmetricKey authKey() { + return kS; + } + + /** + * {@return a copy of the key encapsulation message, {@code null} if none} + */ + public byte[] encapsulation() { + return encapsulation == null ? null : encapsulation.clone(); + } + + @Override + public String toString() { + return "HPKEParameterSpec{" + + "kem_id=" + kem_id + + ", kdf_id=" + kdf_id + + ", aead_id=" + aead_id + + ", info=" + bytesToString(info) + + ", " + (psk == null + ? (kS == null ? "mode_base" : "mode_auth") + : (kS == null ? "mode_psk" : "mode_auth_psk")) + "}"; + } + + // Returns a human-readable representation of a byte array. + private static String bytesToString(byte[] input) { + if (input.length == 0) { + return "(empty)"; + } else { + for (byte b : input) { + if (b < 0x20 || b > 0x7E || b == '"') { + // Non-ASCII or control characters are hard to read, and + // `"` requires character escaping. If any of these are + // present, return only the HEX representation. + return HexFormat.of().formatHex(input); + } + } + // Otherwise, all characters are printable and safe. + // Return both HEX and ASCII representations. + return HexFormat.of().formatHex(input) + + " (\"" + new String(input, StandardCharsets.US_ASCII) + "\")"; + } + } +} diff --git a/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java b/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java new file mode 100644 index 00000000000..e4074c1c4a9 --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/spec/snippet-files/PackageSnippets.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +import javax.crypto.Cipher; +import javax.crypto.spec.HPKEParameterSpec; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HexFormat; + +class PackageSnippets { + public static void main(String[] args) throws Exception { + + // @start region="hpke-spec-example" + // Recipient key pair generation + KeyPairGenerator g = KeyPairGenerator.getInstance("X25519"); + KeyPair kp = g.generateKeyPair(); + + // The HPKE sender cipher is initialized with the recipient's public + // key and an HPKEParameterSpec using specified algorithm identifiers + // and application-supplied info. + Cipher senderCipher = Cipher.getInstance("HPKE"); + HPKEParameterSpec ps = HPKEParameterSpec.of( + HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256, + HPKEParameterSpec.KDF_HKDF_SHA256, + HPKEParameterSpec.AEAD_AES_128_GCM) + .withInfo(HexFormat.of().parseHex("010203040506")); + senderCipher.init(Cipher.ENCRYPT_MODE, kp.getPublic(), ps); + + // Retrieve the key encapsulation message (from the KEM step) from + // the sender. + byte[] kemEncap = senderCipher.getIV(); + + // The HPKE recipient cipher is initialized with its own private key, + // an HPKEParameterSpec using the same algorithm identifiers as used by + // the sender, and the key encapsulation message from the sender. + Cipher recipientCipher = Cipher.getInstance("HPKE"); + HPKEParameterSpec pr = HPKEParameterSpec.of( + HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256, + HPKEParameterSpec.KDF_HKDF_SHA256, + HPKEParameterSpec.AEAD_AES_128_GCM) + .withInfo(HexFormat.of().parseHex("010203040506")) + .withEncapsulation(kemEncap); + recipientCipher.init(Cipher.DECRYPT_MODE, kp.getPrivate(), pr); + + // Encryption and decryption + byte[] msg = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] ct = senderCipher.doFinal(msg); + byte[] pt = recipientCipher.doFinal(ct); + + assert Arrays.equals(msg, pt); + // @end + } +} diff --git a/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java b/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java new file mode 100644 index 00000000000..4dc3fe0a3e8 --- /dev/null +++ b/src/java.base/share/classes/sun/security/util/SliceableSecretKey.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 sun.security.util; + +import javax.crypto.SecretKey; + +/** + * An interface for SecretKeys that support using its slice as a new + * SecretKey. + *

+ * This is mainly used by PKCS #11 implementations that support the + * EXTRACT_KEY_FROM_KEY mechanism even if the key itself is sensitive + * and non-extractable. + */ +public interface SliceableSecretKey { + + /** + * Returns a slice as a new SecretKey. + * + * @param alg the new algorithm name + * @param from the byte offset of the new key in the full key + * @param to the to offset (exclusive) of the new key in the full key + * @return the new key + * @throws ArrayIndexOutOfBoundsException for improper from + * and to values + * @throws UnsupportedOperationException if slicing is not supported + */ + SecretKey slice(String alg, int from, int to); +} diff --git a/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java new file mode 100644 index 00000000000..2e10bb23e82 --- /dev/null +++ b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Compliance.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2025, 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. + */ + +import jdk.test.lib.Asserts; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.HPKEParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.spec.NamedParameterSpec; + +import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_256_GCM; +import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA256; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256; + +/* + * @test + * @bug 8325448 + * @library /test/lib + * @summary HPKE compliance test + */ +public class Compliance { + public static void main(String[] args) throws Exception { + + var kp = KeyPairGenerator.getInstance("X25519").generateKeyPair(); + var info = "info".getBytes(StandardCharsets.UTF_8); + var psk = new SecretKeySpec(new byte[32], "ONE"); + var shortKey = new SecretKeySpec(new byte[31], "ONE"); + var psk_id = "psk_id".getBytes(StandardCharsets.UTF_8); + var emptyKey = new SecretKey() { + public String getAlgorithm() { return "GENERIC"; } + public String getFormat() { return "RAW"; } + public byte[] getEncoded() { return new byte[0]; } + }; + + // HPKEParameterSpec + + // A typical spec + var spec = HPKEParameterSpec.of( + KEM_DHKEM_X25519_HKDF_SHA256, + KDF_HKDF_SHA256, + AEAD_AES_256_GCM); + Asserts.assertEQ(spec.kem_id(), KEM_DHKEM_X25519_HKDF_SHA256); + Asserts.assertEQ(spec.kdf_id(), KDF_HKDF_SHA256); + Asserts.assertEQ(spec.aead_id(), AEAD_AES_256_GCM); + Asserts.assertEQ(spec.authKey(), null); + Asserts.assertEQ(spec.encapsulation(), null); + Asserts.assertEqualsByteArray(spec.info(), new byte[0]); + Asserts.assertEQ(spec.psk(), null); + Asserts.assertEqualsByteArray(spec.psk_id(), new byte[0]); + + // A fake spec but still valid + var specZero = HPKEParameterSpec.of(0, 0, 0); + Asserts.assertEQ(specZero.kem_id(), 0); + Asserts.assertEQ(specZero.kdf_id(), 0); + Asserts.assertEQ(specZero.aead_id(), 0); + Asserts.assertEQ(specZero.authKey(), null); + Asserts.assertEQ(specZero.encapsulation(), null); + Asserts.assertEqualsByteArray(specZero.info(), new byte[0]); + Asserts.assertEQ(specZero.psk(), null); + Asserts.assertEqualsByteArray(specZero.psk_id(), new byte[0]); + + // identifiers + HPKEParameterSpec.of(65535, 65535, 65535); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(-1, 0, 0)); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(0, -1, 0)); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(0, 0, -1)); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(65536, 0, 0)); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(0, 65536, 0)); + Asserts.assertThrows(IllegalArgumentException.class, + () -> HPKEParameterSpec.of(0, 0, 65536)); + + // auth key + Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).authKey() != null); + Asserts.assertTrue(spec.withAuthKey(kp.getPublic()).authKey() != null); + Asserts.assertThrows(NullPointerException.class, () -> spec.withAuthKey(null)); + + // info + Asserts.assertEqualsByteArray(spec.withInfo(info).info(), info); + Asserts.assertThrows(NullPointerException.class, () -> spec.withInfo(null)); + Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withInfo(new byte[0])); + + // encapsulation + Asserts.assertEqualsByteArray(spec.withEncapsulation(info).encapsulation(), info); + Asserts.assertThrows(NullPointerException.class, () -> spec.withEncapsulation(null)); + Asserts.assertTrue(spec.withEncapsulation(new byte[0]).encapsulation().length == 0); // not emptiness check (yet) + + // psk_id and psk + Asserts.assertEqualsByteArray(spec.withPsk(psk, psk_id).psk().getEncoded(), psk.getEncoded()); + Asserts.assertEqualsByteArray(spec.withPsk(psk, psk_id).psk_id(), psk_id); + Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(psk, null)); + Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(null, psk_id)); + Asserts.assertThrows(NullPointerException.class, () -> spec.withPsk(null, null)); + Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(psk, new byte[0])); + Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(emptyKey, psk_id)); + Asserts.assertThrows(IllegalArgumentException.class, () -> spec.withPsk(shortKey, psk_id)); + + // toString + Asserts.assertTrue(spec.toString().contains("kem_id=32, kdf_id=1, aead_id=2")); + Asserts.assertTrue(spec.toString().contains("info=(empty),")); + Asserts.assertTrue(spec.withInfo(new byte[3]).toString().contains("info=000000,")); + Asserts.assertTrue(spec.withInfo("info".getBytes(StandardCharsets.UTF_8)) + .toString().contains("info=696e666f (\"info\"),")); + Asserts.assertTrue(spec.withInfo("\"info\"".getBytes(StandardCharsets.UTF_8)) + .toString().contains("info=22696e666f22,")); + Asserts.assertTrue(spec.withInfo("'info'".getBytes(StandardCharsets.UTF_8)) + .toString().contains("info=27696e666f27 (\"'info'\"),")); + Asserts.assertTrue(spec.withInfo("i\\n\\f\\o".getBytes(StandardCharsets.UTF_8)) + .toString().contains("info=695c6e5c665c6f (\"i\\n\\f\\o\"),")); + Asserts.assertTrue(spec.toString().contains("mode_base}")); + Asserts.assertTrue(spec.withPsk(psk, psk_id).toString().contains("mode_psk}")); + Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).toString().contains("mode_auth}")); + Asserts.assertTrue(spec.withAuthKey(kp.getPrivate()).withPsk(psk, psk_id).toString().contains("mode_auth_psk}")); + + var c1 = Cipher.getInstance("HPKE"); + + Asserts.assertThrows(NoSuchAlgorithmException.class, () -> Cipher.getInstance("HPKE/None/NoPadding")); + + // Still at BEGIN, not initialized + Asserts.assertEQ(c1.getIV(), null); + Asserts.assertEQ(c1.getParameters(), null); + Asserts.assertEquals(0, c1.getBlockSize()); + Asserts.assertThrows(IllegalStateException.class, () -> c1.getOutputSize(100)); + Asserts.assertThrows(IllegalStateException.class, () -> c1.update(new byte[1])); + Asserts.assertThrows(IllegalStateException.class, () -> c1.update(new byte[1], 0, 1)); + Asserts.assertThrows(IllegalStateException.class, () -> c1.updateAAD(new byte[1])); + Asserts.assertThrows(IllegalStateException.class, () -> c1.updateAAD(new byte[1], 0, 1)); + Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal()); + Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1])); + Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1], 0, 1)); + Asserts.assertThrows(IllegalStateException.class, () -> c1.doFinal(new byte[1], 0, 1, new byte[1024], 0)); + + c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), spec); + var encap = c1.getIV(); + + // Does not support WRAP and UNWRAP mode + Asserts.assertThrows(UnsupportedOperationException.class, + () -> c1.init(Cipher.WRAP_MODE, kp.getPublic(), spec)); + Asserts.assertThrows(UnsupportedOperationException.class, + () -> c1.init(Cipher.UNWRAP_MODE, kp.getPublic(), spec)); + + // Nulls + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, null, spec)); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), (HPKEParameterSpec) null)); + + // Cannot init sender with private key + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPrivate(), spec)); + + // Cannot provide key encap msg to sender + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + spec.withEncapsulation(encap))); + + // Cannot init without HPKEParameterSpec + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic())); + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate())); + + // Cannot init with a spec not HPKEParameterSpec + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + NamedParameterSpec.X25519)); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(), + NamedParameterSpec.X25519)); + + // Cannot init recipient with public key + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.DECRYPT_MODE, kp.getPublic(), + spec.withEncapsulation(new byte[32]))); + // Cannot provide key encap msg to sender + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), spec.withEncapsulation(encap))); + // Must provide key encap msg to recipient + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(), spec)); + + // Unsupported identifiers + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + HPKEParameterSpec.of(0, KDF_HKDF_SHA256, AEAD_AES_256_GCM))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + HPKEParameterSpec.of(0x200, KDF_HKDF_SHA256, AEAD_AES_256_GCM))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + HPKEParameterSpec.of(KEM_DHKEM_X25519_HKDF_SHA256, 4, AEAD_AES_256_GCM))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + HPKEParameterSpec.of(KEM_DHKEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, 4))); + + // HPKE + checkEncryptDecrypt(kp, spec, spec); + + // extra features + var kp2 = KeyPairGenerator.getInstance("X25519").generateKeyPair(); + checkEncryptDecrypt(kp, + spec.withInfo(info), + spec.withInfo(info)); + checkEncryptDecrypt(kp, + spec.withPsk(psk, psk_id), + spec.withPsk(psk, psk_id)); + checkEncryptDecrypt(kp, + spec.withAuthKey(kp2.getPrivate()), + spec.withAuthKey(kp2.getPublic())); + checkEncryptDecrypt(kp, + spec.withInfo(info).withPsk(psk, psk_id).withAuthKey(kp2.getPrivate()), + spec.withInfo(info).withPsk(psk, psk_id).withAuthKey(kp2.getPublic())); + + // wrong keys + var kpRSA = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + var kpEC = KeyPairGenerator.getInstance("EC").generateKeyPair(); + + Asserts.assertThrows(InvalidKeyException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kpRSA.getPublic(), spec)); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kpEC.getPublic(), spec)); + + // mod_auth, wrong key type + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + spec.withAuthKey(kp2.getPublic()))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.DECRYPT_MODE, kp.getPrivate(), + spec.withAuthKey(kp2.getPrivate()))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + spec.withAuthKey(kpRSA.getPrivate()))); + Asserts.assertThrows(InvalidAlgorithmParameterException.class, + () -> c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), + spec.withAuthKey(kpEC.getPrivate()))); + } + + static void checkEncryptDecrypt(KeyPair kp, HPKEParameterSpec ps, + HPKEParameterSpec pr) throws Exception { + + var c1 = Cipher.getInstance("HPKE"); + var c2 = Cipher.getInstance("HPKE"); + var aad = "AAD".getBytes(StandardCharsets.UTF_8); + + c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), ps); + Asserts.assertEquals(16, c1.getBlockSize()); + Asserts.assertEquals(116, c1.getOutputSize(100)); + c1.updateAAD(aad); + var ct = c1.doFinal(new byte[2]); + + c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(), + pr.withEncapsulation(c1.getIV())); + Asserts.assertEquals(16, c2.getBlockSize()); + Asserts.assertEquals(84, c2.getOutputSize(100)); + c2.updateAAD(aad); + Asserts.assertEqualsByteArray(c2.doFinal(ct), new byte[2]); + } +} diff --git a/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java new file mode 100644 index 00000000000..9ebf4ce5c09 --- /dev/null +++ b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/Functions.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, 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. + */ + +import jdk.test.lib.Asserts; + +import javax.crypto.Cipher; +import javax.crypto.spec.HPKEParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; +import java.util.List; + +import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_128_GCM; +import static javax.crypto.spec.HPKEParameterSpec.AEAD_AES_256_GCM; +import static javax.crypto.spec.HPKEParameterSpec.AEAD_CHACHA20_POLY1305; +import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA256; +import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA384; +import static javax.crypto.spec.HPKEParameterSpec.KDF_HKDF_SHA512; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_256_HKDF_SHA256; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_384_HKDF_SHA384; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_P_521_HKDF_SHA512; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256; +import static javax.crypto.spec.HPKEParameterSpec.KEM_DHKEM_X448_HKDF_SHA512; + +/* + * @test + * @bug 8325448 + * @library /test/lib + * @summary HPKE running with different keys + */ +public class Functions { + + record Params(String name, int kem) {} + static List PARAMS = List.of( + new Params("secp256r1", KEM_DHKEM_P_256_HKDF_SHA256), + new Params("secp384r1", KEM_DHKEM_P_384_HKDF_SHA384), + new Params("secp521r1", KEM_DHKEM_P_521_HKDF_SHA512), + new Params("X25519", KEM_DHKEM_X25519_HKDF_SHA256), + new Params("X448", KEM_DHKEM_X448_HKDF_SHA512) + ); + + public static void main(String[] args) throws Exception { + + var msg = "hello".getBytes(StandardCharsets.UTF_8); + var msg2 = "goodbye".getBytes(StandardCharsets.UTF_8); + var info = "info".getBytes(StandardCharsets.UTF_8); + var psk = new SecretKeySpec("K".repeat(32).getBytes(StandardCharsets.UTF_8), "Generic"); + var psk_id = "psk1".getBytes(StandardCharsets.UTF_8); + + for (var param : PARAMS) { + var c1 = Cipher.getInstance("HPKE"); + var c2 = Cipher.getInstance("HPKE"); + var kp = genKeyPair(param.name()); + var kp2 = genKeyPair(param.name()); + for (var kdf : List.of(KDF_HKDF_SHA256, KDF_HKDF_SHA384, KDF_HKDF_SHA512)) { + for (var aead : List.of(AEAD_AES_256_GCM, AEAD_AES_128_GCM, AEAD_CHACHA20_POLY1305)) { + + var params = HPKEParameterSpec.of(param.kem, kdf, aead); + System.out.println(params); + + c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), params); + c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(), params.withEncapsulation(c1.getIV())); + Asserts.assertEqualsByteArray(msg, c2.doFinal(c1.doFinal(msg))); + Asserts.assertEqualsByteArray(msg2, c2.doFinal(c1.doFinal(msg2))); + + c1.init(Cipher.ENCRYPT_MODE, kp.getPublic(), params + .withAuthKey(kp2.getPrivate()) + .withInfo(info) + .withPsk(psk, psk_id)); + c2.init(Cipher.DECRYPT_MODE, kp.getPrivate(), params + .withAuthKey(kp2.getPublic()) + .withInfo(info) + .withPsk(psk, psk_id) + .withEncapsulation(c1.getIV())); + Asserts.assertEqualsByteArray(msg, c2.doFinal(c1.doFinal(msg))); + Asserts.assertEqualsByteArray(msg2, c2.doFinal(c1.doFinal(msg2))); + } + } + } + } + + static KeyPair genKeyPair(String name) throws Exception { + if (name.startsWith("secp")) { + var g = KeyPairGenerator.getInstance("EC"); + g.initialize(new ECGenParameterSpec(name)); + return g.generateKeyPair(); + } else { + return KeyPairGenerator.getInstance(name).generateKeyPair(); + } + } +} diff --git a/test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java new file mode 100644 index 00000000000..f4717f57883 --- /dev/null +++ b/test/jdk/com/sun/crypto/provider/Cipher/HPKE/KAT9180.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* + * @test + * @bug 8325448 + * @summary KAT inside RFC 9180 + * @library /test/lib + * @modules java.base/com.sun.crypto.provider + */ +import jdk.test.lib.Asserts; +import jdk.test.lib.artifacts.Artifact; +import jdk.test.lib.artifacts.ArtifactResolver; +import jdk.test.lib.json.JSONValue; + +import com.sun.crypto.provider.DHKEM; + +import javax.crypto.Cipher; +import javax.crypto.spec.HPKEParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HexFormat; + +/// This test is based on Appendix A (Test Vectors) of +/// [RFC 9180](https://datatracker.ietf.org/doc/html/rfc9180#name-test-vectors) +/// The test data is available as a JSON file at: +/// https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/5f503c564da00b0687b3de75f1dfbdfc4079ad31/test-vectors.json. +/// +/// The JSON file can either be hosted on an artifactory server or +/// provided via a local path with +/// ``` +/// jtreg -Djdk.test.lib.artifacts.rfc9180-test-vectors= KAT9180.java +/// ``` +public class KAT9180 { + + @Artifact( + organization = "jpg.tests.jdk.ietf", + name = "rfc9180-test-vectors", + revision = "5f503c5", + extension = "json", + unpack = false) + private static class RFC_9180_KAT { + } + + + public static void main(String[] args) throws Exception { + var h = HexFormat.of(); + Path archivePath = ArtifactResolver.fetchOne(RFC_9180_KAT.class); + System.out.println("Data path: " + archivePath); + var c1 = Cipher.getInstance("HPKE"); + var c2 = Cipher.getInstance("HPKE"); + var ts = JSONValue.parse(new String(Files.readAllBytes(archivePath), StandardCharsets.UTF_8)); + for (var tg : ts.asArray()) { + var mode = Integer.parseInt(tg.get("mode").asString()); + System.err.print('I'); + var kem_id = Integer.parseInt(tg.get("kem_id").asString()); + var kdf_id = Integer.parseInt(tg.get("kdf_id").asString()); + var aead_id = Integer.parseInt(tg.get("aead_id").asString()); + var ikmR = h.parseHex(tg.get("ikmR").asString()); + var ikmE = h.parseHex(tg.get("ikmE").asString()); + var info = h.parseHex(tg.get("info").asString()); + + var kpR = new DHKEM.RFC9180DeriveKeyPairSR(ikmR).derive(kem_id); + var spec = HPKEParameterSpec.of(kem_id, kdf_id, aead_id).withInfo(info); + var rand = new DHKEM.RFC9180DeriveKeyPairSR(ikmE); + + if (mode == 1 || mode == 3) { + spec = spec.withPsk( + new SecretKeySpec(h.parseHex(tg.get("psk").asString()), "Generic"), + h.parseHex(tg.get("psk_id").asString())); + } + if (mode == 0 || mode == 1) { + c1.init(Cipher.ENCRYPT_MODE, kpR.getPublic(), spec, rand); + c2.init(Cipher.DECRYPT_MODE, kpR.getPrivate(), + spec.withEncapsulation(c1.getIV())); + } else { + var ikmS = h.parseHex(tg.get("ikmS").asString()); + var kpS = new DHKEM.RFC9180DeriveKeyPairSR(ikmS).derive(kem_id); + c1.init(Cipher.ENCRYPT_MODE, kpR.getPublic(), + spec.withAuthKey(kpS.getPrivate()), rand); + c2.init(Cipher.DECRYPT_MODE, kpR.getPrivate(), + spec.withEncapsulation(c1.getIV()).withAuthKey(kpS.getPublic())); + } + var enc = tg.get("encryptions"); + if (enc != null) { + System.err.print('e'); + var count = 0; + for (var p : enc.asArray()) { + var aad = h.parseHex(p.get("aad").asString()); + var pt = h.parseHex(p.get("pt").asString()); + var ct = h.parseHex(p.get("ct").asString()); + c1.updateAAD(aad); + var ct1 = c1.doFinal(pt); + Asserts.assertEqualsByteArray(ct, ct1); + c2.updateAAD(aad); + var pt1 = c2.doFinal(ct); + Asserts.assertEqualsByteArray(pt, pt1); + count++; + } + System.err.print(count); + } + } + } +} diff --git a/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java b/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java index 22c5c89b57b..d8814513b12 100644 --- a/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java +++ b/test/jdk/com/sun/crypto/provider/DHKEM/Compliance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, 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 @@ -31,7 +31,6 @@ * @run main/othervm Compliance */ import jdk.test.lib.Asserts; -import jdk.test.lib.Utils; import javax.crypto.DecapsulateException; import javax.crypto.KEM; @@ -41,9 +40,7 @@ import java.security.*; import java.security.interfaces.ECPublicKey; import java.security.spec.*; import java.util.Arrays; -import java.util.Objects; import java.util.Random; -import java.util.function.Consumer; import com.sun.crypto.provider.DHKEM; @@ -66,12 +63,10 @@ public class Compliance { private static void conform() { new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), new byte[0], new byte[0]); new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), new byte[0], null); - Utils.runAndCheckException( - () -> new KEM.Encapsulated(null, new byte[0], null), - NullPointerException.class); - Utils.runAndCheckException( - () -> new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), null, null), - NullPointerException.class); + Asserts.assertThrows(NullPointerException.class, + () -> new KEM.Encapsulated(null, new byte[0], null)); + Asserts.assertThrows(NullPointerException.class, + () -> new KEM.Encapsulated(new SecretKeySpec(new byte[1], "X"), null, null)); } // basic should and shouldn't behaviors @@ -86,37 +81,33 @@ public class Compliance { KEM.getInstance("DHKEM", (String) null); KEM.getInstance("DHKEM", (Provider) null); KEM kem = KEM.getInstance("DHKEM"); - Utils.runAndCheckException( - () -> KEM.getInstance("OLALA"), - NoSuchAlgorithmException.class); - Utils.runAndCheckException( - () -> KEM.getInstance("DHKEM", "NoWhere"), - NoSuchProviderException.class); - Utils.runAndCheckException( - () -> KEM.getInstance("DHKEM", "SunRsaSign"), - NoSuchAlgorithmException.class); + Asserts.assertThrows(NoSuchAlgorithmException.class, + () -> KEM.getInstance("OLALA")); + Asserts.assertThrows(NoSuchProviderException.class, + () -> KEM.getInstance("DHKEM", "NoWhere")); + Asserts.assertThrows(NoSuchAlgorithmException.class, + () -> KEM.getInstance("DHKEM", "SunRsaSign")); - Utils.runAndCheckException( - () -> kem.newEncapsulator(null), - InvalidKeyException.class); - Utils.runAndCheckException( - () -> kem.newDecapsulator(null), - InvalidKeyException.class); + Asserts.assertThrows(InvalidKeyException.class, + () -> kem.newEncapsulator(null)); + Asserts.assertThrows(InvalidKeyException.class, + () -> kem.newDecapsulator(null)); // Still an EC key, rejected by implementation - Utils.runAndCheckException( - () -> kem.newEncapsulator(badECKey()), - ExChecker.of(InvalidKeyException.class).by(DHKEM.class)); + checkThrownBy(Asserts.assertThrows( + InvalidKeyException.class, + () -> kem.newEncapsulator(badECKey())), + DHKEM.class.getName()); // Not an EC key at all, rejected by framework coz it's not // listed in "SupportedKeyClasses" in SunJCE.java. - Utils.runAndCheckException( - () -> kem.newEncapsulator(kpRSA.getPublic()), - ExChecker.of(InvalidKeyException.class).by(KEM.class.getName() + "$DelayedKEM")); + checkThrownBy(Asserts.assertThrows( + InvalidKeyException.class, + () -> kem.newEncapsulator(kpRSA.getPublic())), + KEM.class.getName() + "$DelayedKEM"); - Utils.runAndCheckException( - () -> kem.newDecapsulator(kpRSA.getPrivate()), - InvalidKeyException.class); + Asserts.assertThrows(InvalidKeyException.class, + () -> kem.newDecapsulator(kpRSA.getPrivate())); kem.newEncapsulator(kpX.getPublic(), null); kem.newEncapsulator(kpX.getPublic(), null, null); @@ -125,15 +116,12 @@ public class Compliance { Asserts.assertEQ(enc1.key().getEncoded().length, e2.secretSize()); Asserts.assertEQ(enc1.key().getAlgorithm(), "AES"); - Utils.runAndCheckException( - () -> e2.encapsulate(-1, 12, "AES"), - IndexOutOfBoundsException.class); - Utils.runAndCheckException( - () -> e2.encapsulate(0, e2.secretSize() + 1, "AES"), - IndexOutOfBoundsException.class); - Utils.runAndCheckException( - () -> e2.encapsulate(0, e2.secretSize(), null), - NullPointerException.class); + Asserts.assertThrows(IndexOutOfBoundsException.class, + () -> e2.encapsulate(-1, 12, "AES")); + Asserts.assertThrows(IndexOutOfBoundsException.class, + () -> e2.encapsulate(0, e2.secretSize() + 1, "AES")); + Asserts.assertThrows(NullPointerException.class, + () -> e2.encapsulate(0, e2.secretSize(), null)); KEM.Encapsulated enc = e2.encapsulate(); Asserts.assertEQ(enc.key().getEncoded().length, e2.secretSize()); @@ -162,29 +150,23 @@ public class Compliance { d.secretSize() - 16, d.secretSize(), "AES"); Asserts.assertEQ(encTail.key(), decTail); - Utils.runAndCheckException( - () -> d.decapsulate(null), - NullPointerException.class); - Utils.runAndCheckException( - () -> d.decapsulate(enc.encapsulation(), -1, 12, "AES"), - IndexOutOfBoundsException.class); - Utils.runAndCheckException( - () -> d.decapsulate(enc.encapsulation(), 0, d.secretSize() + 1, "AES"), - IndexOutOfBoundsException.class); - Utils.runAndCheckException( - () -> d.decapsulate(enc.encapsulation(), 0, d.secretSize(), null), - NullPointerException.class); + Asserts.assertThrows(NullPointerException.class, + () -> d.decapsulate(null)); + Asserts.assertThrows(IndexOutOfBoundsException.class, + () -> d.decapsulate(enc.encapsulation(), -1, 12, "AES")); + Asserts.assertThrows(IndexOutOfBoundsException.class, + () -> d.decapsulate(enc.encapsulation(), 0, d.secretSize() + 1, "AES")); + Asserts.assertThrows(NullPointerException.class, + () -> d.decapsulate(enc.encapsulation(), 0, d.secretSize(), null)); KEM.Encapsulator e3 = kem.newEncapsulator(kpEC.getPublic()); KEM.Encapsulated enc2 = e3.encapsulate(); KEM.Decapsulator d3 = kem.newDecapsulator(kpX.getPrivate()); - Utils.runAndCheckException( - () -> d3.decapsulate(enc2.encapsulation()), - DecapsulateException.class); + Asserts.assertThrows(DecapsulateException.class, + () -> d3.decapsulate(enc2.encapsulation())); - Utils.runAndCheckException( - () -> d3.decapsulate(new byte[100]), - DecapsulateException.class); + Asserts.assertThrows(DecapsulateException.class, + () -> d3.decapsulate(new byte[100])); } static class MySecureRandom extends SecureRandom { @@ -273,34 +255,8 @@ public class Compliance { }; } - // Used by Utils.runAndCheckException. Checks for type and final thrower. - record ExChecker(Class ex, String caller) - implements Consumer { - ExChecker { - Objects.requireNonNull(ex); - } - static ExChecker of(Class ex) { - return new ExChecker(ex, null); - } - ExChecker by(String caller) { - return new ExChecker(ex(), caller); - } - ExChecker by(Class caller) { - return new ExChecker(ex(), caller.getName()); - } - @Override - public void accept(Throwable t) { - if (t == null) { - throw new AssertionError("no exception thrown"); - } else if (!ex.isAssignableFrom(t.getClass())) { - throw new AssertionError("exception thrown is " + t.getClass()); - } else if (caller == null) { - return; - } else if (t.getStackTrace()[0].getClassName().equals(caller)) { - return; - } else { - throw new AssertionError("thrown by " + t.getStackTrace()[0].getClassName()); - } - } + // Ensures `t` is thrown by `caller` + static void checkThrownBy(T t, String caller) { + Asserts.assertEquals(caller, t.getStackTrace()[0].getClassName()); } } diff --git a/test/jdk/sun/security/provider/all/Deterministic.java b/test/jdk/sun/security/provider/all/Deterministic.java index 8fb0e943768..60c56cd1b93 100644 --- a/test/jdk/sun/security/provider/all/Deterministic.java +++ b/test/jdk/sun/security/provider/all/Deterministic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, 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 @@ -42,6 +42,7 @@ import javax.crypto.KeyGenerator; import javax.crypto.spec.ChaCha20ParameterSpec; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HPKEParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEParameterSpec; import javax.crypto.spec.SecretKeySpec; @@ -96,6 +97,11 @@ public class Deterministic { key = new SecretKeySpec("isthisakey".getBytes(StandardCharsets.UTF_8), "PBE"); // Some cipher requires salt to be 8 byte long spec = new PBEParameterSpec("saltsalt".getBytes(StandardCharsets.UTF_8), 100); + } else if (alg.equals("HPKE")) { + key = KeyPairGenerator.getInstance("x25519").generateKeyPair().getPublic(); + spec = HPKEParameterSpec.of(HPKEParameterSpec.KEM_DHKEM_X25519_HKDF_SHA256, + HPKEParameterSpec.KDF_HKDF_SHA256, + HPKEParameterSpec.AEAD_AES_256_GCM); } else { key = generateKey(alg.split("/")[0], s.getProvider()); if (!alg.contains("/") || alg.contains("/ECB/")) { @@ -239,6 +245,8 @@ public class Deterministic { return g.generateKey(); } if (s.equals("RSA")) { return generateKeyPair("RSA", 3).getPublic(); + } if (s.equals("HPKE")) { + return generateKeyPair("EC", 3).getPublic(); } else { var g = KeyGenerator.getInstance(s, p); g.init(new SeededSecureRandom(SEED + 4)); diff --git a/test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java b/test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java new file mode 100644 index 00000000000..6340b520a55 --- /dev/null +++ b/test/jdk/sun/security/util/SliceableSecretKey/SoftSliceable.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2025, 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. + */ + + +import jdk.test.lib.Asserts; +import sun.security.util.SliceableSecretKey; + +import javax.crypto.KDF; +import javax.crypto.KDFParameters; +import javax.crypto.KDFSpi; +import javax.crypto.KEM; +import javax.crypto.SecretKey; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/* + * @test + * @bug 8325448 + * @library /test/lib /test/jdk/security/unsignedjce + * @build java.base/javax.crypto.ProviderVerifier + * @modules java.base/sun.security.util + * @run main/othervm SoftSliceable + * @summary Showcase how Sliceable can be used in DHKEM + */ +public class SoftSliceable { + + public static void main(String[] args) throws Exception { + + // Put an HKDF-SHA256 impl that is preferred to the SunJCE one + Security.insertProviderAt(new ProviderImpl(), 1); + + // Just plain KEM calls + var kp = KeyPairGenerator.getInstance("X25519").generateKeyPair(); + var k = KEM.getInstance("DHKEM"); + var e = k.newEncapsulator(kp.getPublic()); + var d = k.newDecapsulator(kp.getPrivate()); + var enc = e.encapsulate(3, 9, "Generic"); + var k2 = d.decapsulate(enc.encapsulation(), 3, 9, "Generic"); + var k2full = d.decapsulate(enc.encapsulation()); + + if (enc.key() instanceof KeyImpl ki1 + && k2 instanceof KeyImpl ki2 + && k2full instanceof KeyImpl ki2full) { + // So the keys do come from the new provider, and + // 1. It has the correct length + Asserts.assertEquals(6, ki1.bytes.length); + // 2. encaps and decaps result in same keys + Asserts.assertEqualsByteArray(ki1.bytes, ki2.bytes); + // 3. The key is the correct slice from the full shared secret + Asserts.assertEqualsByteArray( + Arrays.copyOfRange(ki2full.bytes, 3, 9), ki2.bytes); + } else { + throw new Exception("Unexpected key types"); + } + } + + // A trivial SliceableSecretKey that is non-extractable with getBytes() + public static class KeyImpl implements SecretKey, SliceableSecretKey { + + private final byte[] bytes; + private final String algorithm; + + public KeyImpl(byte[] bytes, String algorithm) { + this.bytes = bytes.clone(); + this.algorithm = algorithm; + } + + @Override + public String getAlgorithm() { + return algorithm; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return null; + } + + @Override + public SecretKey slice(String alg, int from, int to) { + return new KeyImpl(Arrays.copyOfRange(bytes, from, to), algorithm); + } + } + + // Our new provider + public static class ProviderImpl extends Provider { + public ProviderImpl() { + super("A", "A", "A"); + put("KDF.HKDF-SHA256", KDFImpl.class.getName()); + } + } + + // Our new HKDF-SHA256 impl that always returns a KeyImpl object + public static class KDFImpl extends KDFSpi { + + public KDFImpl(KDFParameters p) + throws InvalidAlgorithmParameterException { + super(p); + } + + @Override + protected KDFParameters engineGetParameters() { + return null; + } + + @Override + protected SecretKey engineDeriveKey(String alg, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException { + try { + var kdf = KDF.getInstance("HKDF-SHA256", "SunJCE"); + var bytes = kdf.deriveData(spec); + return new KeyImpl(bytes, alg); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new AssertionError("Cannot happen", e); + } + } + + @Override + protected byte[] engineDeriveData(AlgorithmParameterSpec spec) { + throw new UnsupportedOperationException("Cannot derive data"); + } + } +} From a89018582160a9d876f66925618c8b8f93190e67 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 20 Nov 2025 15:17:44 +0000 Subject: [PATCH 009/616] 8333727: Use JOpt in jpackage to parse command line 8371384: libapplauncher.so is copied to a wrong location in two step packaging when --install-dir=/usr Reviewed-by: almatvee --- .../share/classes/module-info.java | 3 +- .../jpackage/internal/LinuxAppBundler.java | 41 - .../internal/LinuxBundlingEnvironment.java | 114 ++ .../jpackage/internal/LinuxDebBundler.java | 79 - .../jpackage/internal/LinuxFromOptions.java | 119 ++ .../jpackage/internal/LinuxFromParams.java | 146 -- .../internal/LinuxPackageBundler.java | 88 - .../jdk/jpackage/internal/LinuxPackager.java | 3 +- .../internal/LinuxPackagingPipeline.java | 27 +- .../jpackage/internal/LinuxRpmBundler.java | 80 - .../internal/model/LinuxLauncher.java | 6 +- .../resources/LinuxResources.properties | 4 - .../linux/classes/module-info.java.extra | 9 +- .../jdk/jpackage/internal/MacAppBundler.java | 81 - .../internal/MacBundlingEnvironment.java | 110 ++ .../jdk/jpackage/internal/MacDmgBundler.java | 98 -- .../jdk/jpackage/internal/MacFromOptions.java | 331 ++++ .../jdk/jpackage/internal/MacFromParams.java | 384 ----- .../internal/MacPackagingPipeline.java | 16 +- .../jdk/jpackage/internal/MacPkgBundler.java | 99 -- .../internal/model/MacApplication.java | 38 +- .../resources/MacResources.properties | 8 - .../macosx/classes/module-info.java.extra | 9 +- .../internal/AddLauncherArguments.java | 212 --- .../jpackage/internal/AppImageBundler.java | 168 -- .../jdk/jpackage/internal/AppImageFile.java | 341 ++-- .../jpackage/internal/ApplicationBuilder.java | 41 +- .../internal/ApplicationLayoutUtils.java | 71 - .../jdk/jpackage/internal/Arguments.java | 867 ---------- .../jdk/jpackage/internal/BasicBundlers.java | 86 - .../internal/BuildEnvFromOptions.java | 104 ++ .../jpackage/internal/BuildEnvFromParams.java | 74 - .../jdk/jpackage/internal/BundleParams.java | 72 - .../jdk/jpackage/internal/Bundler.java | 126 -- .../jpackage/internal/BundlerParamInfo.java | 179 --- .../jdk/jpackage/internal/Bundlers.java | 112 -- .../jdk/jpackage/internal/CLIHelp.java | 116 -- .../jdk/jpackage/internal/CfgFile.java | 19 +- .../internal/DefaultBundlingEnvironment.java | 283 ++++ .../jdk/jpackage/internal/DeployParams.java | 362 ----- .../internal/FileAssociationGroup.java | 4 + .../jdk/jpackage/internal/FromOptions.java | 237 +++ .../jdk/jpackage/internal/FromParams.java | 251 --- .../jdk/jpackage/internal/IOUtils.java | 10 +- .../internal/JLinkRuntimeBuilder.java | 10 +- .../internal/JPackageToolProvider.java | 58 - .../jdk/jpackage/internal/LauncherData.java | 307 ---- .../internal/LauncherFromOptions.java | 189 +++ .../jpackage/internal/LauncherFromParams.java | 156 -- .../internal/LauncherStartupInfoBuilder.java | 207 ++- .../jdk/jpackage/internal/OptionUtils.java | 54 + .../jpackage/internal/OptionsTransformer.java | 86 + .../jdk/jpackage/internal/Packager.java | 3 +- .../jpackage/internal/PackagingPipeline.java | 33 +- .../internal/StandardBundlerParam.java | 513 ------ ...bstractBundler.java => TempDirectory.java} | 55 +- .../jdk/jpackage/internal/ValidOptions.java | 187 --- .../internal/cli/AdditionalLauncher.java} | 10 +- .../cli/BundlingOperationModifier.java | 42 + .../cli/BundlingOperationOptionScope.java | 38 + .../internal/cli/CliBundlingEnvironment.java | 47 + .../jpackage/internal/cli/DefaultOptions.java | 191 +++ .../jpackage/internal/cli/HelpFormatter.java | 178 ++ .../jdk/jpackage/internal/cli/I18N.java} | 41 +- .../cli/JOptSimpleOptionsBuilder.java | 817 ++++++++++ .../jdk/jpackage/internal/cli/Main.java | 224 +++ .../internal/cli/MessageFormatUtils.java | 79 + .../jdk/jpackage/internal/cli/Option.java | 54 + .../OptionArrayValueConverter.java} | 24 +- .../internal/cli/OptionIdentifier.java | 53 + .../jdk/jpackage/internal/cli/OptionName.java | 68 + .../OptionScope.java} | 11 +- .../jpackage/internal/cli/OptionSource.java | 68 + .../jdk/jpackage/internal/cli/OptionSpec.java | 180 +++ .../internal/cli/OptionSpecBuilder.java | 475 ++++++ .../cli/OptionSpecMapperOptionScope.java | 157 ++ .../jpackage/internal/cli/OptionValue.java | 190 +++ .../internal/cli/OptionValueConverter.java | 273 ++++ .../cli/OptionValueExceptionFactory.java | 183 +++ .../jdk/jpackage/internal/cli/Options.java | 202 +++ .../internal/cli/OptionsAnalyzer.java | 430 +++++ .../internal/cli/OptionsProcessor.java | 428 +++++ .../cli/StandardAppImageFileOption.java | 246 +++ .../cli/StandardBundlingOperation.java | 173 ++ .../internal/cli/StandardFaOption.java | 111 ++ .../internal/cli/StandardHelpFormatter.java | 398 +++++ .../jpackage/internal/cli/StandardOption.java | 802 +++++++++ .../internal/cli/StandardOptionContext.java | 68 + .../StandardOptionValueExceptionFactory.java | 79 + .../internal/cli/StandardValidator.java | 162 ++ .../internal/cli/StandardValueConverter.java | 83 + .../jpackage/internal/cli/StringToken.java | 57 + .../jdk/jpackage/internal/cli/Utils.java | 109 ++ .../jdk/jpackage/internal/cli/Validator.java | 265 +++ .../jpackage/internal/cli/ValueConverter.java | 68 + .../internal/cli/WithOptionIdentifier.java | 38 + .../cli/WithOptionIdentifierStub.java} | 14 +- .../internal/model/BundlingEnvironment.java | 47 +- .../model/BundlingOperationDescriptor.java | 53 + .../internal/model/ConfigException.java | 7 +- .../internal/model/ExternalApplication.java | 145 +- .../internal/model/JPackageException.java} | 27 +- .../LauncherModularStartupInfoMixin.java | 13 +- .../internal/model/PackagerException.java | 84 - .../internal/model/RuntimeBuilder.java | 2 +- .../resources/HelpResources.properties | 629 ++++---- .../resources/MainResources.properties | 55 +- .../jdk/jpackage/internal/util/FileUtils.java | 14 + .../jpackage/internal/util/SetBuilder.java | 87 + .../share/classes/jdk/jpackage/main/Main.java | 86 +- .../share/classes/module-info.java | 14 +- .../internal/WinBundlingEnvironment.java | 109 ++ .../jdk/jpackage/internal/WinExeBundler.java | 62 +- .../jdk/jpackage/internal/WinFromOpions.java | 115 ++ .../jdk/jpackage/internal/WinFromParams.java | 164 -- .../jdk/jpackage/internal/WinMsiBundler.java | 122 -- .../jdk/jpackage/internal/WinMsiPackager.java | 5 +- .../internal/WinPackagingPipeline.java | 45 +- .../jpackage/internal/model/WinLauncher.java | 10 +- .../resources/WinResources.properties | 5 - .../windows/classes/module-info.java.extra | 9 +- test/jdk/tools/jpackage/TEST.properties | 2 + .../jdk/jpackage/test/AppImageFile.java | 98 +- .../helpers/jdk/jpackage/test/MacHelper.java | 2 +- .../jdk/jpackage/test/PackageType.java | 90 +- test/jdk/tools/jpackage/junit/TEST.properties | 8 +- .../jpackage/internal/AppImageFileTest.java | 678 ++++++-- .../DefaultBundlingEnvironmentTest.java | 58 + .../jpackage/internal/DeployParamsTest.java | 74 - .../LauncherStartupInfoBuilderTest.java | 155 ++ .../internal/PackagingPipelineTest.java | 43 +- .../internal/cli/DefaultOptionsTest.java | 57 + .../internal/cli/ExpectedOptions.java | 94 ++ .../jdk/jpackage/internal/cli/HelpTest.java | 181 +++ .../cli/JOptSimpleOptionsBuilderTest.java | 1428 +++++++++++++++++ .../jdk/jpackage/internal/cli/MainTest.java | 203 +++ .../cli/MockupCliBundlingEnvironment.java | 160 ++ .../internal/cli/OptionIdentifierTest.java | 63 + .../jpackage/internal/cli/OptionNameTest.java | 152 ++ .../cli/OptionSpecMutatorOptionScopeTest.java | 144 ++ .../jpackage/internal/cli/OptionSpecTest.java | 365 +++++ .../cli/OptionValueConverterTest.java | 216 +++ .../cli/OptionValueExceptionFactoryTest.java | 152 ++ .../internal/cli/OptionValueTest.java | 212 +++ .../internal/cli/OptionsProcessorTest.java | 786 +++++++++ .../jpackage/internal/cli/OptionsTest.java | 272 ++++ .../cli/OptionsValidationFailTest.excludes | 46 + .../cli/OptionsValidationFailTest.java | 245 +++ .../cli/StandardBundlingOperationTest.java | 103 ++ .../internal/cli/StandardOptionTest.java | 639 ++++++++ .../internal/cli/StandardValidatorTest.java | 259 +++ .../cli/StandardValueConverterTest.java | 161 ++ .../internal/cli/StringTokenTest.java | 49 + .../jdk/jpackage/internal/cli/TestUtils.java | 179 +++ .../jdk/jpackage/internal/cli/UtilsTest.java | 91 ++ .../jpackage/internal/cli/ValidatorTest.java | 284 ++++ .../jdk/jpackage/internal/cli/help-linux.txt | 197 +++ .../jdk/jpackage/internal/cli/help-macos.txt | 238 +++ .../jpackage/internal/cli/help-windows.txt | 205 +++ .../jpackage/internal/cli/jpackage-options.md | 63 + .../jpackage/share/AppImagePackageTest.java | 6 +- test/jdk/tools/jpackage/share/AsyncTest.java | 222 +++ test/jdk/tools/jpackage/share/ErrorTest.java | 8 +- 163 files changed, 18527 insertions(+), 6692 deletions(-) delete mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java delete mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java delete mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java delete mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java delete mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java delete mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java create mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java delete mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java create mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java delete mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java delete mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayoutUtils.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/BasicBundlers.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromParams.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundleParams.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundler.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundlerParamInfo.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundlers.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/CLIHelp.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/DeployParams.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromOptions.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/JPackageToolProvider.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromOptions.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromParams.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionsTransformer.java delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java rename src/jdk.jpackage/share/classes/jdk/jpackage/internal/{AbstractBundler.java => TempDirectory.java} (53%) delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ValidOptions.java rename src/jdk.jpackage/{macosx/classes/jdk/jpackage/internal/MacBuildEnvFromParams.java => share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java} (76%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java rename src/jdk.jpackage/{macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java => share/classes/jdk/jpackage/internal/cli/I18N.java} (56%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/MessageFormatUtils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Option.java rename src/jdk.jpackage/share/classes/jdk/jpackage/internal/{model/BundleCreator.java => cli/OptionArrayValueConverter.java} (68%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionIdentifier.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionName.java rename src/jdk.jpackage/share/classes/jdk/jpackage/internal/{model/BundlingOperation.java => cli/OptionScope.java} (81%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSource.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpec.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecMapperOptionScope.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValueExceptionFactory.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Options.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsProcessor.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardFaOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardHelpFormatter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOptionContext.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOptionValueExceptionFactory.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StringToken.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Utils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/ValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/WithOptionIdentifier.java rename src/jdk.jpackage/{macosx/classes/jdk/jpackage/internal/MacAppImageFileExtras.java => share/classes/jdk/jpackage/internal/cli/WithOptionIdentifierStub.java} (69%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/BundlingOperationDescriptor.java rename src/jdk.jpackage/{windows/classes/jdk/jpackage/internal/WinAppBundler.java => share/classes/jdk/jpackage/internal/model/JPackageException.java} (69%) delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackagerException.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOpions.java delete mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java delete mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java delete mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DeployParamsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/LauncherStartupInfoBuilderTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/DefaultOptionsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ExpectedOptions.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/HelpTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilderTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MockupCliBundlingEnvironment.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionIdentifierTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionNameTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionSpecMutatorOptionScopeTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionSpecTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueConverterTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueExceptionFactoryTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsProcessorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardBundlingOperationTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardValidatorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardValueConverterTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StringTokenTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/UtilsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md create mode 100644 test/jdk/tools/jpackage/share/AsyncTest.java diff --git a/src/jdk.internal.opt/share/classes/module-info.java b/src/jdk.internal.opt/share/classes/module-info.java index ba6987f1ea9..728c2de500d 100644 --- a/src/jdk.internal.opt/share/classes/module-info.java +++ b/src/jdk.internal.opt/share/classes/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -32,6 +32,7 @@ module jdk.internal.opt { exports jdk.internal.joptsimple to jdk.jlink, jdk.jshell, + jdk.jpackage, jdk.jdeps; exports jdk.internal.opt to jdk.compiler, diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java deleted file mode 100644 index fe8d6bcf34f..00000000000 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.Optional; - -public class LinuxAppBundler extends AppImageBundler { - public LinuxAppBundler() { - setAppImageSupplier((params, output) -> { - // Order is important! - var app = LinuxFromParams.APPLICATION.fetchFrom(params); - var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - LinuxPackagingPipeline.build(Optional.empty()) - .excludeDirFromCopying(output.getParent()) - .create().execute(BuildEnv.withAppImageDir(env, output), app); - }); - } -} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java new file mode 100644 index 00000000000..cfd8ab391bb --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 java.util.stream.Collectors.toMap; +import static jdk.jpackage.internal.LinuxFromOptions.createLinuxApplication; +import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_LINUX_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_LINUX_DEB; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_LINUX_RPM; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardBundlingOperation; +import jdk.jpackage.internal.model.BundlingOperationDescriptor; +import jdk.jpackage.internal.model.LinuxPackage; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.util.Result; + +public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment { + + public LinuxBundlingEnvironment() { + super(build() + .defaultOperation(() -> { + return LazyLoad.SYS_ENV.value().map(LinuxSystemEnvironment::nativePackageType).map(DESCRIPTORS::get); + }) + .bundler(CREATE_LINUX_APP_IMAGE, LinuxBundlingEnvironment::createAppImage) + .bundler(CREATE_LINUX_DEB, LazyLoad::debSysEnv, LinuxBundlingEnvironment::createDebPackage) + .bundler(CREATE_LINUX_RPM, LazyLoad::rpmSysEnv, LinuxBundlingEnvironment::createRpmPackage)); + } + + private static void createDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) { + + createNativePackage(options, + LinuxFromOptions::createLinuxDebPackage, + buildEnv()::create, + LinuxBundlingEnvironment::buildPipeline, + (env, pkg, outputDir) -> { + return new LinuxDebPackager(env, pkg, outputDir, sysEnv); + }); + } + + private static void createRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) { + + createNativePackage(options, + LinuxFromOptions::createLinuxRpmPackage, + buildEnv()::create, + LinuxBundlingEnvironment::buildPipeline, + (env, pkg, outputDir) -> { + return new LinuxRpmPackager(env, pkg, outputDir, sysEnv); + }); + } + + private static void createAppImage(Options options) { + + final var app = createLinuxApplication(options); + + createApplicationImage(options, app, LinuxPackagingPipeline.build(Optional.empty())); + } + + private static PackagingPipeline.Builder buildPipeline(LinuxPackage pkg) { + return LinuxPackagingPipeline.build(Optional.of(pkg)); + } + + private static BuildEnvFromOptions buildEnv() { + return new BuildEnvFromOptions().predefinedAppImageLayout(APPLICATION_LAYOUT); + } + + private static final class LazyLoad { + + static Result debSysEnv() { + return DEB_SYS_ENV; + } + + static Result rpmSysEnv() { + return RPM_SYS_ENV; + } + + private static final Result SYS_ENV = LinuxSystemEnvironment.create(); + + private static final Result DEB_SYS_ENV = LinuxDebSystemEnvironment.create(SYS_ENV); + + private static final Result RPM_SYS_ENV = LinuxRpmSystemEnvironment.create(SYS_ENV); + } + + private static final Map DESCRIPTORS = Stream.of( + CREATE_LINUX_DEB, + CREATE_LINUX_RPM + ).collect(toMap(StandardBundlingOperation::packageType, StandardBundlingOperation::descriptor)); +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java deleted file mode 100644 index 76a08519b48..00000000000 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import java.util.Optional; -import jdk.jpackage.internal.model.LinuxDebPackage; -import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.model.StandardPackageType; -import jdk.jpackage.internal.util.Result; - -public class LinuxDebBundler extends LinuxPackageBundler { - - public LinuxDebBundler() { - super(LinuxFromParams.DEB_PACKAGE); - } - - @Override - public String getName() { - return I18N.getString("deb.bundler.name"); - } - - @Override - public String getID() { - return "deb"; - } - - @Override - public Path execute(Map params, Path outputParentDir) throws PackagerException { - - var pkg = LinuxFromParams.DEB_PACKAGE.fetchFrom(params); - - return Packager.build().outputDir(outputParentDir) - .pkg(pkg) - .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, _, outputDir) -> { - return new LinuxDebPackager(env, pkg, outputDir, sysEnv.orElseThrow()); - }).execute(LinuxPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - protected Result sysEnv() { - return sysEnv; - } - - @Override - public boolean isDefault() { - return sysEnv.value() - .map(LinuxSystemEnvironment::nativePackageType) - .map(StandardPackageType.LINUX_DEB::equals) - .orElse(false); - } - - private final Result sysEnv = LinuxDebSystemEnvironment.create(SYS_ENV); -} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java new file mode 100644 index 00000000000..799c92ce2e1 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.FromOptions.buildApplicationBuilder; +import static jdk.jpackage.internal.FromOptions.createPackageBuilder; +import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_APP_CATEGORY; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_DEB_MAINTAINER_EMAIL; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_MENU_GROUP; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_PACKAGE_DEPENDENCIES; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_PACKAGE_NAME; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_RELEASE; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_RPM_LICENSE_TYPE; +import static jdk.jpackage.internal.cli.StandardOption.LINUX_SHORTCUT_HINT; +import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; +import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM; + +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LinuxApplication; +import jdk.jpackage.internal.model.LinuxDebPackage; +import jdk.jpackage.internal.model.LinuxLauncher; +import jdk.jpackage.internal.model.LinuxLauncherMixin; +import jdk.jpackage.internal.model.LinuxRpmPackage; +import jdk.jpackage.internal.model.StandardPackageType; + +final class LinuxFromOptions { + + static LinuxApplication createLinuxApplication(Options options) { + + final var launcherFromOptions = new LauncherFromOptions().faWithDefaultDescription(); + + final var appBuilder = buildApplicationBuilder().create(options, launcherOptions -> { + + final var launcher = launcherFromOptions.create(launcherOptions); + + final var shortcut = LINUX_SHORTCUT_HINT.findIn(launcherOptions); + + return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(shortcut)); + + }, (LinuxLauncher linuxLauncher, Launcher launcher) -> { + return LinuxLauncher.create(launcher, linuxLauncher); + }, APPLICATION_LAYOUT); + + appBuilder.launchers().map(LinuxPackagingPipeline::normalizeShortcuts).ifPresent(appBuilder::launchers); + + return LinuxApplication.create(appBuilder.create()); + } + + static LinuxRpmPackage createLinuxRpmPackage(Options options) { + + final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_RPM); + + final var pkgBuilder = new LinuxRpmPackageBuilder(superPkgBuilder); + + LINUX_RPM_LICENSE_TYPE.ifPresentIn(options, pkgBuilder::licenseType); + + return pkgBuilder.create(); + } + + static LinuxDebPackage createLinuxDebPackage(Options options) { + + final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_DEB); + + final var pkgBuilder = new LinuxDebPackageBuilder(superPkgBuilder); + + LINUX_DEB_MAINTAINER_EMAIL.ifPresentIn(options, pkgBuilder::maintainerEmail); + + final var pkg = pkgBuilder.create(); + + // Show warning if license file is missing + if (pkg.licenseFile().isEmpty()) { + Log.verbose(I18N.getString("message.debs-like-licenses")); + } + + return pkg; + } + + private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, StandardPackageType type) { + + final var app = createLinuxApplication(options); + + final var superPkgBuilder = createPackageBuilder(options, app, type); + + final var pkgBuilder = new LinuxPackageBuilder(superPkgBuilder); + + LINUX_PACKAGE_DEPENDENCIES.ifPresentIn(options, pkgBuilder::additionalDependencies); + LINUX_APP_CATEGORY.ifPresentIn(options, pkgBuilder::category); + LINUX_MENU_GROUP.ifPresentIn(options, pkgBuilder::menuGroupName); + LINUX_RELEASE.ifPresentIn(options, pkgBuilder::release); + LINUX_PACKAGE_NAME.ifPresentIn(options, pkgBuilder::literalName); + + return pkgBuilder; + } + +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java deleted file mode 100644 index e9d1416b5c3..00000000000 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.BundlerParamInfo.createStringBundlerParam; -import static jdk.jpackage.internal.FromParams.createApplicationBuilder; -import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam; -import static jdk.jpackage.internal.FromParams.createPackageBuilder; -import static jdk.jpackage.internal.FromParams.createPackageBundlerParam; -import static jdk.jpackage.internal.FromParams.findLauncherShortcut; -import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT; -import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; -import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; - -import java.io.IOException; -import java.util.Map; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.LinuxApplication; -import jdk.jpackage.internal.model.LinuxDebPackage; -import jdk.jpackage.internal.model.LinuxLauncher; -import jdk.jpackage.internal.model.LinuxLauncherMixin; -import jdk.jpackage.internal.model.LinuxRpmPackage; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.StandardPackageType; - -final class LinuxFromParams { - - private static LinuxApplication createLinuxApplication( - Map params) throws ConfigException, IOException { - final var launcherFromParams = new LauncherFromParams(); - - final var app = createApplicationBuilder(params, toFunction(launcherParams -> { - final var launcher = launcherFromParams.create(launcherParams); - final var shortcut = findLauncherShortcut(LINUX_SHORTCUT_HINT, params, launcherParams); - return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(shortcut)); - }), (LinuxLauncher linuxLauncher, Launcher launcher) -> { - return LinuxLauncher.create(launcher, linuxLauncher); - }, APPLICATION_LAYOUT).create(); - return LinuxApplication.create(app); - } - - private static LinuxPackageBuilder createLinuxPackageBuilder( - Map params, StandardPackageType type) throws ConfigException, IOException { - - final var app = APPLICATION.fetchFrom(params); - - final var superPkgBuilder = createPackageBuilder(params, app, type); - - final var pkgBuilder = new LinuxPackageBuilder(superPkgBuilder); - - LINUX_PACKAGE_DEPENDENCIES.copyInto(params, pkgBuilder::additionalDependencies); - LINUX_CATEGORY.copyInto(params, pkgBuilder::category); - LINUX_MENU_GROUP.copyInto(params, pkgBuilder::menuGroupName); - RELEASE.copyInto(params, pkgBuilder::release); - LINUX_PACKAGE_NAME.copyInto(params, pkgBuilder::literalName); - - return pkgBuilder; - } - - private static LinuxRpmPackage createLinuxRpmPackage( - Map params) throws ConfigException, IOException { - - final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_RPM); - - final var pkgBuilder = new LinuxRpmPackageBuilder(superPkgBuilder); - - LICENSE_TYPE.copyInto(params, pkgBuilder::licenseType); - - return pkgBuilder.create(); - } - - private static LinuxDebPackage createLinuxDebPackage( - Map params) throws ConfigException, IOException { - - final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_DEB); - - final var pkgBuilder = new LinuxDebPackageBuilder(superPkgBuilder); - - MAINTAINER_EMAIL.copyInto(params, pkgBuilder::maintainerEmail); - - final var pkg = pkgBuilder.create(); - - // Show warning if license file is missing - if (pkg.licenseFile().isEmpty()) { - Log.verbose(I18N.getString("message.debs-like-licenses")); - } - - return pkg; - } - - static final BundlerParamInfo APPLICATION = createApplicationBundlerParam( - LinuxFromParams::createLinuxApplication); - - static final BundlerParamInfo RPM_PACKAGE = createPackageBundlerParam( - LinuxFromParams::createLinuxRpmPackage); - - static final BundlerParamInfo DEB_PACKAGE = createPackageBundlerParam( - LinuxFromParams::createLinuxDebPackage); - - private static final BundlerParamInfo LINUX_SHORTCUT_HINT = createStringBundlerParam( - Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId()); - - private static final BundlerParamInfo LINUX_CATEGORY = createStringBundlerParam( - Arguments.CLIOptions.LINUX_CATEGORY.getId()); - - private static final BundlerParamInfo LINUX_PACKAGE_DEPENDENCIES = createStringBundlerParam( - Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId()); - - private static final BundlerParamInfo LINUX_MENU_GROUP = createStringBundlerParam( - Arguments.CLIOptions.LINUX_MENU_GROUP.getId()); - - private static final BundlerParamInfo RELEASE = createStringBundlerParam( - Arguments.CLIOptions.RELEASE.getId()); - - private static final BundlerParamInfo LINUX_PACKAGE_NAME = createStringBundlerParam( - Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId()); - - private static final BundlerParamInfo LICENSE_TYPE = createStringBundlerParam( - Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId()); - - private static final BundlerParamInfo MAINTAINER_EMAIL = createStringBundlerParam( - Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId()); -} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java deleted file mode 100644 index 1f674c0be11..00000000000 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2019, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.Map; -import java.util.Objects; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.LinuxPackage; -import jdk.jpackage.internal.util.Result; - -abstract class LinuxPackageBundler extends AbstractBundler { - - LinuxPackageBundler(BundlerParamInfo pkgParam) { - this.pkgParam = Objects.requireNonNull(pkgParam); - } - - @Override - public final boolean validate(Map params) - throws ConfigException { - - // Order is important! - pkgParam.fetchFrom(params); - BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - - LinuxSystemEnvironment sysEnv; - try { - sysEnv = sysEnv().orElseThrow(); - } catch (RuntimeException ex) { - throw ConfigException.rethrowConfigException(ex); - } - - if (!isDefault()) { - Log.verbose(I18N.format( - "message.not-default-bundler-no-dependencies-lookup", - getName())); - } else if (!sysEnv.soLookupAvailable()) { - final String advice; - if ("deb".equals(getID())) { - advice = "message.deb-ldd-not-available.advice"; - } else { - advice = "message.rpm-ldd-not-available.advice"; - } - // Let user know package dependencies will not be generated. - Log.error(String.format("%s\n%s", I18N.getString( - "message.ldd-not-available"), I18N.getString(advice))); - } - - return true; - } - - @Override - public final String getBundleType() { - return "INSTALLER"; - } - - @Override - public boolean supported(boolean runtimeInstaller) { - return sysEnv().hasValue(); - } - - protected abstract Result sysEnv(); - - private final BundlerParamInfo pkgParam; - - static final Result SYS_ENV = LinuxSystemEnvironment.create(); -} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java index 806592904d1..af7f5288cc5 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java @@ -40,7 +40,6 @@ import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.LinuxPackage; -import jdk.jpackage.internal.model.PackagerException; abstract class LinuxPackager implements Consumer { @@ -95,7 +94,7 @@ abstract class LinuxPackager implements Consumer { + // Return "true" if shortcut is not configured for the launcher. + return launcher.shortcut().isEmpty(); + }, (LinuxLauncher launcher) -> { + return launcher.shortcut().flatMap(LauncherShortcut::startupDirectory); + }, (launcher, shortcut) -> { + return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(Optional.of(new LauncherShortcut(shortcut)))); + }); + } + private static void writeLauncherLib( AppImageBuildEnv env) throws IOException { @@ -90,6 +106,15 @@ final class LinuxPackagingPipeline { }); } + private static final ApplicationLayout LINUX_APPLICATION_LAYOUT = ApplicationLayout.build() + .launchersDirectory("bin") + .appDirectory("lib/app") + .runtimeDirectory("lib/runtime") + .desktopIntegrationDirectory("lib") + .appModsDirectory("lib/app/mods") + .contentDirectory("lib") + .create(); + static final LinuxApplicationLayout APPLICATION_LAYOUT = LinuxApplicationLayout.create( - ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT, Path.of("lib/libapplauncher.so")); + LINUX_APPLICATION_LAYOUT, Path.of("lib/libapplauncher.so")); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java deleted file mode 100644 index c134aa91d6a..00000000000 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import java.util.Optional; -import jdk.jpackage.internal.model.LinuxRpmPackage; -import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.model.StandardPackageType; -import jdk.jpackage.internal.util.Result; - - -public class LinuxRpmBundler extends LinuxPackageBundler { - - public LinuxRpmBundler() { - super(LinuxFromParams.RPM_PACKAGE); - } - - @Override - public String getName() { - return I18N.getString("rpm.bundler.name"); - } - - @Override - public String getID() { - return "rpm"; - } - - @Override - public Path execute(Map params, Path outputParentDir) throws PackagerException { - - var pkg = LinuxFromParams.RPM_PACKAGE.fetchFrom(params); - - return Packager.build().outputDir(outputParentDir) - .pkg(pkg) - .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, _, outputDir) -> { - return new LinuxRpmPackager(env, pkg, outputDir, sysEnv.orElseThrow()); - }).execute(LinuxPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - protected Result sysEnv() { - return sysEnv; - } - - @Override - public boolean isDefault() { - return sysEnv.value() - .map(LinuxSystemEnvironment::nativePackageType) - .map(StandardPackageType.LINUX_RPM::equals) - .orElse(false); - } - - private final Result sysEnv = LinuxRpmSystemEnvironment.create(SYS_ENV); -} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java index c84b5e3bbf5..3c654f604c2 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java @@ -24,6 +24,8 @@ */ package jdk.jpackage.internal.model; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LINUX_LAUNCHER_SHORTCUT; + import java.util.HashMap; import java.util.Map; import jdk.jpackage.internal.util.CompositeProxy; @@ -39,7 +41,7 @@ public interface LinuxLauncher extends Launcher, LinuxLauncherMixin { default Map extraAppImageFileData() { Map map = new HashMap<>(); shortcut().ifPresent(shortcut -> { - shortcut.store(SHORTCUT_ID, map::put); + shortcut.store(LINUX_LAUNCHER_SHORTCUT.getName(), map::put); }); return map; } @@ -55,6 +57,4 @@ public interface LinuxLauncher extends Launcher, LinuxLauncherMixin { public static LinuxLauncher create(Launcher launcher, LinuxLauncherMixin mixin) { return CompositeProxy.create(LinuxLauncher.class, launcher, mixin); } - - public static final String SHORTCUT_ID = "linux-shortcut"; } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties index a732d02c7d1..6f05b623064 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties @@ -23,10 +23,6 @@ # questions. # # -app.bundler.name=Linux Application Image -deb.bundler.name=DEB Bundle -rpm.bundler.name=RPM Bundle - param.license-type.default=Unknown resource.deb-control-file=DEB control file diff --git a/src/jdk.jpackage/linux/classes/module-info.java.extra b/src/jdk.jpackage/linux/classes/module-info.java.extra index d32314b0429..7bef2286214 100644 --- a/src/jdk.jpackage/linux/classes/module-info.java.extra +++ b/src/jdk.jpackage/linux/classes/module-info.java.extra @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -23,8 +23,5 @@ * questions. */ -provides jdk.jpackage.internal.Bundler with - jdk.jpackage.internal.LinuxAppBundler, - jdk.jpackage.internal.LinuxDebBundler, - jdk.jpackage.internal.LinuxRpmBundler; - +provides jdk.jpackage.internal.cli.CliBundlingEnvironment with + jdk.jpackage.internal.LinuxBundlingEnvironment; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java deleted file mode 100644 index cce35ece117..00000000000 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.StandardBundlerParam.OUTPUT_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE; - -import java.util.Map; -import java.util.Optional; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.util.function.ExceptionBox; - -public class MacAppBundler extends AppImageBundler { - public MacAppBundler() { - setAppImageSupplier((params, output) -> { - - // Order is important! - final var app = MacFromParams.APPLICATION.fetchFrom(params); - final BuildEnv env; - - if (StandardBundlerParam.hasPredefinedAppImage(params)) { - env = MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params); - final var pkg = MacPackagingPipeline.createSignAppImagePackage(app, env); - MacPackagingPipeline.build(Optional.of(pkg)).create().execute(env, pkg, output); - } else { - env = BuildEnv.withAppImageDir(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params), output); - MacPackagingPipeline.build(Optional.empty()) - .excludeDirFromCopying(output.getParent()) - .excludeDirFromCopying(OUTPUT_DIR.fetchFrom(params)).create().execute(env, app); - } - - }); - setParamsValidator(MacAppBundler::doValidate); - } - - private static void doValidate(Map params) - throws ConfigException { - - try { - MacFromParams.APPLICATION.fetchFrom(params); - } catch (ExceptionBox ex) { - if (ex.getCause() instanceof ConfigException cfgEx) { - throw cfgEx; - } else { - throw ex; - } - } - - if (StandardBundlerParam.hasPredefinedAppImage(params)) { - if (!Optional.ofNullable( - SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { - throw new ConfigException( - I18N.getString("error.app-image.mac-sign.required"), - null); - } - } - } -} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java new file mode 100644 index 00000000000..371a3c7307a --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.MacFromOptions.createMacApplication; +import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; +import static jdk.jpackage.internal.MacPackagingPipeline.createSignAppImagePackage; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_DMG; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_PKG; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE; + +import java.util.Optional; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.model.MacPackage; +import jdk.jpackage.internal.model.Package; +import jdk.jpackage.internal.util.Result; + +public class MacBundlingEnvironment extends DefaultBundlingEnvironment { + + public MacBundlingEnvironment() { + super(build() + .defaultOperation(CREATE_MAC_DMG) + .bundler(SIGN_MAC_APP_IMAGE, MacBundlingEnvironment::signAppImage) + .bundler(CREATE_MAC_APP_IMAGE, MacBundlingEnvironment::createAppImage) + .bundler(CREATE_MAC_DMG, LazyLoad::dmgSysEnv, MacBundlingEnvironment::createDmdPackage) + .bundler(CREATE_MAC_PKG, MacBundlingEnvironment::createPkgPackage)); + } + + private static void createDmdPackage(Options options, MacDmgSystemEnvironment sysEnv) { + createNativePackage(options, + MacFromOptions::createMacDmgPackage, + buildEnv()::create, + MacBundlingEnvironment::buildPipeline, + (env, pkg, outputDir) -> { + Log.verbose(I18N.format("message.building-dmg", pkg.app().name())); + return new MacDmgPackager(env, pkg, outputDir, sysEnv); + }); + } + + private static void createPkgPackage(Options options) { + createNativePackage(options, + MacFromOptions::createMacPkgPackage, + buildEnv()::create, + MacBundlingEnvironment::buildPipeline, + (env, pkg, outputDir) -> { + Log.verbose(I18N.format("message.building-pkg", pkg.app().name())); + return new MacPkgPackager(env, pkg, outputDir); + }); + } + + private static void signAppImage(Options options) { + + final var app = createMacApplication(options); + + final var env = buildEnv().create(options, app); + + final var pkg = createSignAppImagePackage(app, env); + + buildPipeline(pkg).create().execute(env, pkg, env.appImageDir()); + } + + private static void createAppImage(Options options) { + + final var app = createMacApplication(options); + + createApplicationImage(options, app, MacPackagingPipeline.build(Optional.empty())); + } + + private static PackagingPipeline.Builder buildPipeline(Package pkg) { + return MacPackagingPipeline.build(Optional.of(pkg)); + } + + private static BuildEnvFromOptions buildEnv() { + return new BuildEnvFromOptions() + .predefinedAppImageLayout(APPLICATION_LAYOUT) + .predefinedRuntimeImageLayout(MacPackage::guessRuntimeLayout); + } + + private static final class LazyLoad { + + static Result dmgSysEnv() { + return DMG_SYS_ENV; + } + + private static final Result DMG_SYS_ENV = MacDmgSystemEnvironment.create(); + } +} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java deleted file mode 100644 index 0ddb987dbee..00000000000 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.MacDmgPackage; -import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.util.Result; - -public class MacDmgBundler extends MacBaseInstallerBundler { - - @Override - public String getName() { - return I18N.getString("dmg.bundler.name"); - } - - @Override - public String getID() { - return "dmg"; - } - - @Override - public boolean validate(Map params) - throws ConfigException { - try { - Objects.requireNonNull(params); - - MacFromParams.DMG_PACKAGE.fetchFrom(params); - - //run basic validation to ensure requirements are met - //we are not interested in return code, only possible exception - validateAppImageAndBundeler(params); - - return true; - } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } - } - } - - @Override - public Path execute(Map params, - Path outputParentDir) throws PackagerException { - - var pkg = MacFromParams.DMG_PACKAGE.fetchFrom(params); - - Log.verbose(I18N.format("message.building-dmg", pkg.app().name())); - - return Packager.build().outputDir(outputParentDir) - .pkg(pkg) - .env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, _, outputDir) -> { - return new MacDmgPackager(env, pkg, outputDir, sysEnv.orElseThrow()); - }).execute(MacPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - public boolean supported(boolean runtimeInstaller) { - return sysEnv.hasValue(); - } - - @Override - public boolean isDefault() { - return true; - } - - private final Result sysEnv = MacDmgSystemEnvironment.create(); -} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java new file mode 100644 index 00000000000..074014dede0 --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.FromOptions.buildApplicationBuilder; +import static jdk.jpackage.internal.FromOptions.createPackageBuilder; +import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; +import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib; +import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir; +import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.ICON; +import static jdk.jpackage.internal.cli.StandardOption.APPCLASS; +import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_CATEGORY; +import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_IMAGE_SIGN_IDENTITY; +import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_STORE; +import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_IDENTIFIER; +import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_NAME; +import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_SIGNING_PREFIX; +import static jdk.jpackage.internal.cli.StandardOption.MAC_DMG_CONTENT; +import static jdk.jpackage.internal.cli.StandardOption.MAC_ENTITLEMENTS; +import static jdk.jpackage.internal.cli.StandardOption.MAC_INSTALLER_SIGN_IDENTITY; +import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGN; +import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGNING_KEYCHAIN; +import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGNING_KEY_NAME; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; +import static jdk.jpackage.internal.model.MacPackage.RUNTIME_BUNDLE_LAYOUT; +import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG; +import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG; +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo; +import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException; +import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardFaOption; +import jdk.jpackage.internal.model.ApplicationLaunchers; +import jdk.jpackage.internal.model.ExternalApplication; +import jdk.jpackage.internal.model.FileAssociation; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.MacApplication; +import jdk.jpackage.internal.model.MacDmgPackage; +import jdk.jpackage.internal.model.MacFileAssociation; +import jdk.jpackage.internal.model.MacLauncher; +import jdk.jpackage.internal.model.MacPackage; +import jdk.jpackage.internal.model.MacPkgPackage; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.RuntimeLayout; +import jdk.jpackage.internal.util.Result; +import jdk.jpackage.internal.util.function.ExceptionBox; + + +final class MacFromOptions { + + static MacApplication createMacApplication(Options options) { + return createMacApplicationInternal(options).app(); + } + + static MacDmgPackage createMacDmgPackage(Options options) { + + final var app = createMacApplicationInternal(options); + + final var superPkgBuilder = createMacPackageBuilder(options, app, MAC_DMG); + + final var pkgBuilder = new MacDmgPackageBuilder(superPkgBuilder); + + MAC_DMG_CONTENT.ifPresentIn(options, pkgBuilder::dmgContent); + + return pkgBuilder.create(); + } + + static MacPkgPackage createMacPkgPackage(Options options) { + + // + // One of "MacSignTest.testExpiredCertificate" test cases expects + // two error messages about expired certificates in the output: one for + // certificate for signing an app image, another certificate for signing a PKG. + // So creation of a PKG package is a bit messy. + // + + final boolean sign = MAC_SIGN.findIn(options).orElse(false); + final boolean appStore = MAC_APP_STORE.findIn(options).orElse(false); + + final var appResult = Result.create(() -> createMacApplicationInternal(options)); + + final Optional pkgBuilder; + if (appResult.hasValue()) { + final var superPkgBuilder = createMacPackageBuilder(options, appResult.orElseThrow(), MAC_PKG); + pkgBuilder = Optional.of(new MacPkgPackageBuilder(superPkgBuilder)); + } else { + // Failed to create an app. Is it because of the expired certificate? + rethrowIfNotExpiredCertificateException(appResult); + // Yes, the certificate for signing the app image has expired. + // Keep going, try to create a signing config for the package. + pkgBuilder = Optional.empty(); + } + + if (sign) { + final var signingIdentityBuilder = createSigningIdentityBuilder(options); + MAC_INSTALLER_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity); + MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> { + final StandardCertificateSelector domain; + if (appStore) { + domain = StandardCertificateSelector.APP_STORE_PKG_INSTALLER; + } else { + domain = StandardCertificateSelector.PKG_INSTALLER; + } + + signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); + }); + + if (pkgBuilder.isPresent()) { + pkgBuilder.orElseThrow().signingBuilder(signingIdentityBuilder); + } else { + // + // The certificate for signing the app image has expired. Can not create a + // package because there is no app. + // Try to create a signing config for the package and see if the certificate for + // signing the package is also expired. + // + + final var expiredAppCertException = appResult.firstError().orElseThrow(); + + final var pkgSignConfigResult = Result.create(signingIdentityBuilder::create); + try { + rethrowIfNotExpiredCertificateException(pkgSignConfigResult); + // The certificate for the package signing config is also expired! + } catch (RuntimeException ex) { + // Some error occurred trying to configure the signing config for the package. + // Ignore it, bail out with the first error. + rethrowUnchecked(expiredAppCertException); + } + + Log.error(pkgSignConfigResult.firstError().orElseThrow().getMessage()); + rethrowUnchecked(expiredAppCertException); + } + } + + return pkgBuilder.orElseThrow().create(); + } + + private record ApplicationWithDetails(MacApplication app, Optional externalApp) { + ApplicationWithDetails { + Objects.requireNonNull(app); + Objects.requireNonNull(externalApp); + } + } + + private static ApplicationWithDetails createMacApplicationInternal(Options options) { + + final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(options) + .map(MacPackage::guessRuntimeLayout); + + predefinedRuntimeLayout.ifPresent(layout -> { + validateRuntimeHasJliLib(layout); + if (MAC_APP_STORE.containsIn(options)) { + validateRuntimeHasNoBinDir(layout); + } + }); + + final var launcherFromOptions = new LauncherFromOptions().faMapper(MacFromOptions::createMacFa); + + final var superAppBuilder = buildApplicationBuilder() + .runtimeLayout(RUNTIME_BUNDLE_LAYOUT) + .predefinedRuntimeLayout(predefinedRuntimeLayout.map(RuntimeLayout::unresolve).orElse(null)) + .create(options, launcherOptions -> { + var launcher = launcherFromOptions.create(launcherOptions); + return MacLauncher.create(launcher); + }, (MacLauncher _, Launcher launcher) -> { + return MacLauncher.create(launcher); + }, APPLICATION_LAYOUT); + + if (PREDEFINED_APP_IMAGE.containsIn(options)) { + // Set the main launcher start up info. + // AppImageFile assumes the main launcher start up info is available when + // it is constructed from Application instance. + // This happens when jpackage signs predefined app image. + final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra(); + final var mainLauncherStartupInfo = new MainLauncherStartupInfo(APPCLASS.getFrom(appImageFileOptions)); + final var launchers = superAppBuilder.launchers().orElseThrow(); + final var mainLauncher = ApplicationBuilder.overrideLauncherStartupInfo(launchers.mainLauncher(), mainLauncherStartupInfo); + superAppBuilder.launchers(new ApplicationLaunchers(MacLauncher.create(mainLauncher), launchers.additionalLaunchers())); + } + + final var app = superAppBuilder.create(); + + final var appBuilder = new MacApplicationBuilder(app); + + PREDEFINED_APP_IMAGE.findIn(options) + .map(MacBundle::new) + .map(MacBundle::infoPlistFile) + .ifPresent(appBuilder::externalInfoPlistFile); + + ICON.ifPresentIn(options, appBuilder::icon); + MAC_BUNDLE_NAME.ifPresentIn(options, appBuilder::bundleName); + MAC_BUNDLE_IDENTIFIER.ifPresentIn(options, appBuilder::bundleIdentifier); + MAC_APP_CATEGORY.ifPresentIn(options, appBuilder::category); + + final boolean sign; + final boolean appStore; + + if (PREDEFINED_APP_IMAGE.containsIn(options) && OptionUtils.bundlingOperation(options) != SIGN_MAC_APP_IMAGE) { + final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra(); + sign = MAC_SIGN.getFrom(appImageFileOptions); + appStore = MAC_APP_STORE.getFrom(appImageFileOptions); + } else { + sign = MAC_SIGN.getFrom(options); + appStore = MAC_APP_STORE.getFrom(options); + } + + appBuilder.appStore(appStore); + + if (sign) { + final var signingIdentityBuilder = createSigningIdentityBuilder(options); + MAC_APP_IMAGE_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity); + MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> { + final StandardCertificateSelector domain; + if (appStore) { + domain = StandardCertificateSelector.APP_STORE_APP_IMAGE; + } else { + domain = StandardCertificateSelector.APP_IMAGE; + } + + signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); + }); + + final var signingBuilder = new AppImageSigningConfigBuilder(signingIdentityBuilder); + if (appStore) { + signingBuilder.entitlementsResourceName("sandbox.plist"); + } + + app.mainLauncher().flatMap(Launcher::startupInfo).ifPresentOrElse( + signingBuilder::signingIdentifierPrefix, + () -> { + // Runtime installer does not have the main launcher, use + // 'bundleIdentifier' as the prefix by default. + var bundleIdentifier = appBuilder.create().bundleIdentifier(); + signingBuilder.signingIdentifierPrefix(bundleIdentifier + "."); + }); + MAC_BUNDLE_SIGNING_PREFIX.ifPresentIn(options, signingBuilder::signingIdentifierPrefix); + + MAC_ENTITLEMENTS.ifPresentIn(options, signingBuilder::entitlements); + + appBuilder.signingBuilder(signingBuilder); + } + + return new ApplicationWithDetails(appBuilder.create(), superAppBuilder.externalApplication()); + } + + private static MacPackageBuilder createMacPackageBuilder(Options options, ApplicationWithDetails app, PackageType type) { + + final var builder = new MacPackageBuilder(createPackageBuilder(options, app.app(), type)); + + app.externalApp() + .map(ExternalApplication::extra) + .flatMap(MAC_SIGN::findIn) + .ifPresent(builder::predefinedAppImageSigned); + + PREDEFINED_RUNTIME_IMAGE.findIn(options) + .map(MacBundle::new) + .filter(MacBundle::isValid) + .map(MacBundle::isSigned) + .ifPresent(builder::predefinedAppImageSigned); + + return builder; + } + + private static void rethrowIfNotExpiredCertificateException(Result result) { + final var ex = result.firstError().orElseThrow(); + + if (ex instanceof ExpiredCertificateException) { + return; + } + + if (ex instanceof ExceptionBox box) { + if (box.getCause() instanceof Exception cause) { + rethrowIfNotExpiredCertificateException(Result.ofError(cause)); + } + } + + rethrowUnchecked(ex); + } + + private static SigningIdentityBuilder createSigningIdentityBuilder(Options options) { + final var builder = new SigningIdentityBuilder(); + MAC_SIGNING_KEYCHAIN.findIn(options).map(Path::toString).ifPresent(builder::keychain); + return builder; + } + + private static MacFileAssociation createMacFa(Options options, FileAssociation fa) { + + final var builder = new MacFileAssociationBuilder(); + + StandardFaOption.MAC_CFBUNDLETYPEROLE.ifPresentIn(options, builder::cfBundleTypeRole); + StandardFaOption.MAC_LSHANDLERRANK.ifPresentIn(options, builder::lsHandlerRank); + StandardFaOption.MAC_NSSTORETYPEKEY.ifPresentIn(options, builder::nsPersistentStoreTypeKey); + StandardFaOption.MAC_NSDOCUMENTCLASS.ifPresentIn(options, builder::nsDocumentClass); + StandardFaOption.MAC_LSTYPEISPACKAGE.ifPresentIn(options, builder::lsTypeIsPackage); + StandardFaOption.MAC_LSDOCINPLACE.ifPresentIn(options, builder::lsSupportsOpeningDocumentsInPlace); + StandardFaOption.MAC_UIDOCBROWSER.ifPresentIn(options, builder::uiSupportsDocumentBrowser); + StandardFaOption.MAC_NSEXPORTABLETYPES.ifPresentIn(options, builder::nsExportableTypes); + StandardFaOption.MAC_UTTYPECONFORMSTO.ifPresentIn(options, builder::utTypeConformsTo); + + return builder.create(fa); + } +} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java deleted file mode 100644 index 72c33ef6475..00000000000 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.BundlerParamInfo.createBooleanBundlerParam; -import static jdk.jpackage.internal.BundlerParamInfo.createPathBundlerParam; -import static jdk.jpackage.internal.BundlerParamInfo.createStringBundlerParam; -import static jdk.jpackage.internal.FromParams.createApplicationBuilder; -import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam; -import static jdk.jpackage.internal.FromParams.createPackageBuilder; -import static jdk.jpackage.internal.FromParams.createPackageBundlerParam; -import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; -import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib; -import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir; -import static jdk.jpackage.internal.StandardBundlerParam.DMG_CONTENT; -import static jdk.jpackage.internal.StandardBundlerParam.ICON; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE_FILE; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE; -import static jdk.jpackage.internal.StandardBundlerParam.hasPredefinedAppImage; -import static jdk.jpackage.internal.model.MacPackage.RUNTIME_BUNDLE_LAYOUT; -import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG; -import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.Callable; -import java.util.function.Predicate; -import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo; -import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException; -import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector; -import jdk.jpackage.internal.model.ApplicationLaunchers; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.FileAssociation; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.MacApplication; -import jdk.jpackage.internal.model.MacDmgPackage; -import jdk.jpackage.internal.model.MacFileAssociation; -import jdk.jpackage.internal.model.MacLauncher; -import jdk.jpackage.internal.model.MacPackage; -import jdk.jpackage.internal.model.MacPkgPackage; -import jdk.jpackage.internal.model.PackageType; -import jdk.jpackage.internal.model.RuntimeLayout; -import jdk.jpackage.internal.util.function.ExceptionBox; - - -final class MacFromParams { - - private static MacApplication createMacApplication( - Map params) throws ConfigException, IOException { - - final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(params) - .map(MacPackage::guessRuntimeLayout); - - if (predefinedRuntimeLayout.isPresent()) { - validateRuntimeHasJliLib(predefinedRuntimeLayout.orElseThrow()); - if (APP_STORE.findIn(params).orElse(false)) { - validateRuntimeHasNoBinDir(predefinedRuntimeLayout.orElseThrow()); - } - } - - final var launcherFromParams = new LauncherFromParams(Optional.of(MacFromParams::createMacFa)); - - final var superAppBuilder = createApplicationBuilder(params, toFunction(launcherParams -> { - var launcher = launcherFromParams.create(launcherParams); - return MacLauncher.create(launcher); - }), (MacLauncher _, Launcher launcher) -> { - return MacLauncher.create(launcher); - }, APPLICATION_LAYOUT, RUNTIME_BUNDLE_LAYOUT, predefinedRuntimeLayout.map(RuntimeLayout::unresolve)); - - if (hasPredefinedAppImage(params)) { - // Set the main launcher start up info. - // AppImageFile assumes the main launcher start up info is available when - // it is constructed from Application instance. - // This happens when jpackage signs predefined app image. - final var mainLauncherStartupInfo = new MainLauncherStartupInfo(superAppBuilder.mainLauncherClassName().orElseThrow()); - final var launchers = superAppBuilder.launchers().orElseThrow(); - final var mainLauncher = ApplicationBuilder.overrideLauncherStartupInfo(launchers.mainLauncher(), mainLauncherStartupInfo); - superAppBuilder.launchers(new ApplicationLaunchers(MacLauncher.create(mainLauncher), launchers.additionalLaunchers())); - } - - final var app = superAppBuilder.create(); - - final var appBuilder = new MacApplicationBuilder(app); - - if (hasPredefinedAppImage(params)) { - appBuilder.externalInfoPlistFile(PREDEFINED_APP_IMAGE.findIn(params).map(MacBundle::new).orElseThrow().infoPlistFile()); - } - - ICON.copyInto(params, appBuilder::icon); - MAC_CF_BUNDLE_NAME.copyInto(params, appBuilder::bundleName); - MAC_CF_BUNDLE_IDENTIFIER.copyInto(params, appBuilder::bundleIdentifier); - APP_CATEGORY.copyInto(params, appBuilder::category); - - final boolean sign; - final boolean appStore; - - if (hasPredefinedAppImage(params) && PACKAGE_TYPE.findIn(params).filter(Predicate.isEqual("app-image")).isEmpty()) { - final var appImageFileExtras = new MacAppImageFileExtras(superAppBuilder.externalApplication().orElseThrow()); - sign = appImageFileExtras.signed(); - appStore = appImageFileExtras.appStore(); - } else { - sign = SIGN_BUNDLE.findIn(params).orElse(false); - appStore = APP_STORE.findIn(params).orElse(false); - } - - appBuilder.appStore(appStore); - - if (sign) { - final var signingIdentityBuilder = createSigningIdentityBuilder(params); - APP_IMAGE_SIGN_IDENTITY.copyInto(params, signingIdentityBuilder::signingIdentity); - SIGNING_KEY_USER.findIn(params).ifPresent(userName -> { - final StandardCertificateSelector domain; - if (appStore) { - domain = StandardCertificateSelector.APP_STORE_APP_IMAGE; - } else { - domain = StandardCertificateSelector.APP_IMAGE; - } - - signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); - }); - - final var signingBuilder = new AppImageSigningConfigBuilder(signingIdentityBuilder); - if (appStore) { - signingBuilder.entitlementsResourceName("sandbox.plist"); - } - - final var bundleIdentifier = appBuilder.create().bundleIdentifier(); - app.mainLauncher().flatMap(Launcher::startupInfo).ifPresentOrElse( - signingBuilder::signingIdentifierPrefix, - () -> { - // Runtime installer does not have main launcher, so use - // 'bundleIdentifier' as prefix by default. - signingBuilder.signingIdentifierPrefix( - bundleIdentifier + "."); - }); - SIGN_IDENTIFIER_PREFIX.copyInto(params, signingBuilder::signingIdentifierPrefix); - - ENTITLEMENTS.copyInto(params, signingBuilder::entitlements); - - appBuilder.signingBuilder(signingBuilder); - } - - return appBuilder.create(); - } - - private static MacPackageBuilder createMacPackageBuilder( - Map params, MacApplication app, - PackageType type) throws ConfigException { - final var builder = new MacPackageBuilder(createPackageBuilder(params, app, type)); - - PREDEFINED_APP_IMAGE_FILE.findIn(params) - .map(MacAppImageFileExtras::new) - .map(MacAppImageFileExtras::signed) - .ifPresent(builder::predefinedAppImageSigned); - - PREDEFINED_RUNTIME_IMAGE.findIn(params) - .map(MacBundle::new) - .filter(MacBundle::isValid) - .map(MacBundle::isSigned) - .ifPresent(builder::predefinedAppImageSigned); - - return builder; - } - - private static MacDmgPackage createMacDmgPackage( - Map params) throws ConfigException, IOException { - - final var app = APPLICATION.fetchFrom(params); - - final var superPkgBuilder = createMacPackageBuilder(params, app, MAC_DMG); - - final var pkgBuilder = new MacDmgPackageBuilder(superPkgBuilder); - - DMG_CONTENT.copyInto(params, pkgBuilder::dmgContent); - - return pkgBuilder.create(); - } - - private record WithExpiredCertificateException(Optional obj, Optional certEx) { - WithExpiredCertificateException { - if (obj.isEmpty() == certEx.isEmpty()) { - throw new IllegalArgumentException(); - } - } - - static WithExpiredCertificateException of(Callable callable) { - try { - return new WithExpiredCertificateException<>(Optional.of(callable.call()), Optional.empty()); - } catch (ExpiredCertificateException ex) { - return new WithExpiredCertificateException<>(Optional.empty(), Optional.of(ex)); - } catch (ExceptionBox ex) { - if (ex.getCause() instanceof ExpiredCertificateException certEx) { - return new WithExpiredCertificateException<>(Optional.empty(), Optional.of(certEx)); - } - throw ex; - } catch (RuntimeException ex) { - throw ex; - } catch (Throwable t) { - throw ExceptionBox.rethrowUnchecked(t); - } - } - } - - private static MacPkgPackage createMacPkgPackage( - Map params) throws ConfigException, IOException { - - // This is over complicated to make "MacSignTest.testExpiredCertificate" test pass. - - final boolean sign = SIGN_BUNDLE.findIn(params).orElse(false); - final boolean appStore = APP_STORE.findIn(params).orElse(false); - - final var appOrExpiredCertEx = WithExpiredCertificateException.of(() -> { - return APPLICATION.fetchFrom(params); - }); - - final Optional pkgBuilder; - if (appOrExpiredCertEx.obj().isPresent()) { - final var superPkgBuilder = createMacPackageBuilder(params, appOrExpiredCertEx.obj().orElseThrow(), MAC_PKG); - pkgBuilder = Optional.of(new MacPkgPackageBuilder(superPkgBuilder)); - } else { - pkgBuilder = Optional.empty(); - } - - if (sign) { - final var signingIdentityBuilder = createSigningIdentityBuilder(params); - INSTALLER_SIGN_IDENTITY.copyInto(params, signingIdentityBuilder::signingIdentity); - SIGNING_KEY_USER.findIn(params).ifPresent(userName -> { - final StandardCertificateSelector domain; - if (appStore) { - domain = StandardCertificateSelector.APP_STORE_PKG_INSTALLER; - } else { - domain = StandardCertificateSelector.PKG_INSTALLER; - } - - signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); - }); - - if (pkgBuilder.isPresent()) { - pkgBuilder.orElseThrow().signingBuilder(signingIdentityBuilder); - } else { - final var expiredPkgCert = WithExpiredCertificateException.of(() -> { - return signingIdentityBuilder.create(); - }).certEx(); - expiredPkgCert.map(ConfigException::getMessage).ifPresent(Log::error); - throw appOrExpiredCertEx.certEx().orElseThrow(); - } - } - - return pkgBuilder.orElseThrow().create(); - } - - private static SigningIdentityBuilder createSigningIdentityBuilder(Map params) { - final var builder = new SigningIdentityBuilder(); - SIGNING_KEYCHAIN.copyInto(params, builder::keychain); - return builder; - } - - private static MacFileAssociation createMacFa(FileAssociation fa, Map params) { - - final var builder = new MacFileAssociationBuilder(); - - FA_MAC_CFBUNDLETYPEROLE.copyInto(params, builder::cfBundleTypeRole); - FA_MAC_LSHANDLERRANK.copyInto(params, builder::lsHandlerRank); - FA_MAC_NSSTORETYPEKEY.copyInto(params, builder::nsPersistentStoreTypeKey); - FA_MAC_NSDOCUMENTCLASS.copyInto(params, builder::nsDocumentClass); - FA_MAC_LSTYPEISPACKAGE.copyInto(params, builder::lsTypeIsPackage); - FA_MAC_LSDOCINPLACE.copyInto(params, builder::lsSupportsOpeningDocumentsInPlace); - FA_MAC_UIDOCBROWSER.copyInto(params, builder::uiSupportsDocumentBrowser); - FA_MAC_NSEXPORTABLETYPES.copyInto(params, builder::nsExportableTypes); - FA_MAC_UTTYPECONFORMSTO.copyInto(params, builder::utTypeConformsTo); - - return toFunction(builder::create).apply(fa); - } - - static final BundlerParamInfo APPLICATION = createApplicationBundlerParam( - MacFromParams::createMacApplication); - - static final BundlerParamInfo DMG_PACKAGE = createPackageBundlerParam( - MacFromParams::createMacDmgPackage); - - static final BundlerParamInfo PKG_PACKAGE = createPackageBundlerParam( - MacFromParams::createMacPkgPackage); - - private static final BundlerParamInfo MAC_CF_BUNDLE_NAME = createStringBundlerParam( - Arguments.CLIOptions.MAC_BUNDLE_NAME.getId()); - - private static final BundlerParamInfo APP_CATEGORY = createStringBundlerParam( - Arguments.CLIOptions.MAC_CATEGORY.getId()); - - private static final BundlerParamInfo ENTITLEMENTS = createPathBundlerParam( - Arguments.CLIOptions.MAC_ENTITLEMENTS.getId()); - - private static final BundlerParamInfo MAC_CF_BUNDLE_IDENTIFIER = createStringBundlerParam( - Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId()); - - private static final BundlerParamInfo SIGN_IDENTIFIER_PREFIX = createStringBundlerParam( - Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId()); - - private static final BundlerParamInfo APP_IMAGE_SIGN_IDENTITY = createStringBundlerParam( - Arguments.CLIOptions.MAC_APP_IMAGE_SIGN_IDENTITY.getId()); - - private static final BundlerParamInfo INSTALLER_SIGN_IDENTITY = createStringBundlerParam( - Arguments.CLIOptions.MAC_INSTALLER_SIGN_IDENTITY.getId()); - - private static final BundlerParamInfo SIGNING_KEY_USER = createStringBundlerParam( - Arguments.CLIOptions.MAC_SIGNING_KEY_NAME.getId()); - - private static final BundlerParamInfo SIGNING_KEYCHAIN = createStringBundlerParam( - Arguments.CLIOptions.MAC_SIGNING_KEYCHAIN.getId()); - - private static final BundlerParamInfo PACKAGE_TYPE = createStringBundlerParam( - Arguments.CLIOptions.PACKAGE_TYPE.getId()); - - private static final BundlerParamInfo APP_STORE = createBooleanBundlerParam( - Arguments.CLIOptions.MAC_APP_STORE.getId()); - - private static final BundlerParamInfo FA_MAC_CFBUNDLETYPEROLE = createStringBundlerParam( - Arguments.MAC_CFBUNDLETYPEROLE); - - private static final BundlerParamInfo FA_MAC_LSHANDLERRANK = createStringBundlerParam( - Arguments.MAC_LSHANDLERRANK); - - private static final BundlerParamInfo FA_MAC_NSSTORETYPEKEY = createStringBundlerParam( - Arguments.MAC_NSSTORETYPEKEY); - - private static final BundlerParamInfo FA_MAC_NSDOCUMENTCLASS = createStringBundlerParam( - Arguments.MAC_NSDOCUMENTCLASS); - - private static final BundlerParamInfo FA_MAC_LSTYPEISPACKAGE = createBooleanBundlerParam( - Arguments.MAC_LSTYPEISPACKAGE); - - private static final BundlerParamInfo FA_MAC_LSDOCINPLACE = createBooleanBundlerParam( - Arguments.MAC_LSDOCINPLACE); - - private static final BundlerParamInfo FA_MAC_UIDOCBROWSER = createBooleanBundlerParam( - Arguments.MAC_UIDOCBROWSER); - - @SuppressWarnings("unchecked") - private static final BundlerParamInfo> FA_MAC_NSEXPORTABLETYPES = - new BundlerParamInfo<>( - Arguments.MAC_NSEXPORTABLETYPES, - (Class>) (Object) List.class, - null, - (s, p) -> Arrays.asList(s.split("(,|\\s)+")) - ); - - @SuppressWarnings("unchecked") - private static final BundlerParamInfo> FA_MAC_UTTYPECONFORMSTO = - new BundlerParamInfo<>( - Arguments.MAC_UTTYPECONFORMSTO, - (Class>) (Object) List.class, - null, - (s, p) -> Arrays.asList(s.split("(,|\\s)+")) - ); -} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java index b82b20c0c36..51fd15afabb 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -75,7 +75,6 @@ import jdk.jpackage.internal.model.MacFileAssociation; import jdk.jpackage.internal.model.MacPackage; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.PackageType; -import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; @@ -268,7 +267,7 @@ final class MacPackagingPipeline { static AppImageTaskAction withBundleLayout(AppImageTaskAction action) { return new AppImageTaskAction<>() { @Override - public void execute(AppImageBuildEnv env) throws IOException, PackagerException { + public void execute(AppImageBuildEnv env) throws IOException { if (!env.envLayout().runtimeDirectory().getName(0).equals(Path.of("Contents"))) { env = LayoutUtils.fromPackagerLayout(env); } @@ -610,11 +609,20 @@ final class MacPackagingPipeline { } @Override - public void execute(TaskAction taskAction) throws IOException, PackagerException { + public void execute(TaskAction taskAction) throws IOException { delegate.execute(taskAction); } } + private static final ApplicationLayout MAC_APPLICATION_LAYOUT = ApplicationLayout.build() + .launchersDirectory("Contents/MacOS") + .appDirectory("Contents/app") + .runtimeDirectory("Contents/runtime/Contents/Home") + .desktopIntegrationDirectory("Contents/Resources") + .appModsDirectory("Contents/app/mods") + .contentDirectory("Contents") + .create(); + static final MacApplicationLayout APPLICATION_LAYOUT = MacApplicationLayout.create( - ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT, Path.of("Contents/runtime")); + MAC_APPLICATION_LAYOUT, Path.of("Contents/runtime")); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java deleted file mode 100644 index e827f238db3..00000000000 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2014, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.MacPkgPackage; -import jdk.jpackage.internal.model.PackagerException; - -public class MacPkgBundler extends MacBaseInstallerBundler { - - @Override - public String getName() { - return I18N.getString("pkg.bundler.name"); - } - - @Override - public String getID() { - return "pkg"; - } - - @Override - public boolean validate(Map params) - throws ConfigException { - try { - Objects.requireNonNull(params); - - final var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); - - // run basic validation to ensure requirements are met - // we are not interested in return code, only possible exception - validateAppImageAndBundeler(params); - - // hdiutil is always available so there's no need - // to test for availability. - - return true; - } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } - } - } - - @Override - public Path execute(Map params, - Path outputParentDir) throws PackagerException { - - var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); - - Log.verbose(I18N.format("message.building-pkg", pkg.app().name())); - - return Packager.build().outputDir(outputParentDir) - .pkg(pkg) - .env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, _, outputDir) -> { - return new MacPkgPackager(env, pkg, outputDir); - }).execute(MacPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - public boolean supported(boolean runtimeInstaller) { - return true; - } - - @Override - public boolean isDefault() { - return false; - } - -} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java index 04ab7042ac5..cfe10e8a012 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java @@ -25,13 +25,18 @@ package jdk.jpackage.internal.model; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toUnmodifiableMap; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_APP_STORE; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_MAIN_CLASS; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_SIGNED; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; +import jdk.jpackage.internal.cli.OptionValue; import jdk.jpackage.internal.util.CompositeProxy; public interface MacApplication extends Application, MacApplicationMixin { @@ -76,7 +81,14 @@ public interface MacApplication extends Application, MacApplicationMixin { @Override default Map extraAppImageFileData() { - return Stream.of(ExtraAppImageFileField.values()).collect(toMap(ExtraAppImageFileField::fieldName, x -> x.asString(this))); + return Stream.of(ExtraAppImageFileField.values()).map(field -> { + return field.findStringValue(this).map(value -> { + return Map.entry(field.fieldName(), value); + }); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } public static MacApplication create(Application app, MacApplicationMixin mixin) { @@ -84,23 +96,31 @@ public interface MacApplication extends Application, MacApplicationMixin { } public enum ExtraAppImageFileField { - SIGNED("signed", app -> Boolean.toString(app.sign())), - APP_STORE("app-store", app -> Boolean.toString(app.appStore())); + SIGNED(MAC_SIGNED, app -> { + return Optional.of(Boolean.toString(app.sign())); + }), + APP_STORE(MAC_APP_STORE, app -> { + return Optional.of(Boolean.toString(app.appStore())); + }), + APP_CLASS(MAC_MAIN_CLASS, app -> { + return app.mainLauncher().flatMap(Launcher::startupInfo).map(LauncherStartupInfo::qualifiedClassName); + }), + ; - ExtraAppImageFileField(String fieldName, Function getter) { - this.fieldName = fieldName; + ExtraAppImageFileField(OptionValue option, Function> getter) { + this.fieldName = option.getName(); this.getter = getter; } - public String fieldName() { + String fieldName() { return fieldName; } - String asString(MacApplication app) { + Optional findStringValue(MacApplication app) { return getter.apply(app); } private final String fieldName; - private final Function getter; + private final Function> getter; } } 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 afa71d84d5c..7ce20925439 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 @@ -23,12 +23,6 @@ # questions. # # - -app.bundler.name=Mac Application Image -store.bundler.name=Mac App Store Ready Bundler -dmg.bundler.name=Mac DMG Package -pkg.bundler.name=Mac PKG Package - error.invalid-cfbundle-version.advice=Set a compatible 'app-version' value. Valid versions are one to three integers separated by dots. error.explicit-sign-no-cert=Signature explicitly requested but no signing certificate found error.explicit-sign-no-cert.advice=Specify a valid mac-signing-key-user-name and mac-signing-keychain @@ -60,7 +54,6 @@ resource.pkg-background-image=pkg background image resource.pkg-pdf=project definition file resource.launchd-plist-file=launchd plist file - message.bundle-name-too-long-warning={0} is set to ''{1}'', which is longer than 16 characters. For a better Mac experience consider shortening it. message.preparing-info-plist=Preparing Info.plist: {0}. message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not be used. The default icon will be used in it's place. @@ -70,7 +63,6 @@ message.creating-association-with-null-extension=Creating association with null message.ignoring.symlink=Warning: codesign is skipping the symlink {0}. message.already.signed=File already signed: {0}. message.keychain.error=Error: unable to get keychain list. -message.building-bundle=Building Mac App Store Package for {0}. message.invalid-identifier=invalid mac bundle identifier [{0}]. message.invalid-identifier.advice=specify identifier with "--mac-package-identifier". message.building-dmg=Building DMG package for {0}. diff --git a/src/jdk.jpackage/macosx/classes/module-info.java.extra b/src/jdk.jpackage/macosx/classes/module-info.java.extra index 1496167cd4a..e6202dd1156 100644 --- a/src/jdk.jpackage/macosx/classes/module-info.java.extra +++ b/src/jdk.jpackage/macosx/classes/module-info.java.extra @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -23,8 +23,5 @@ * questions. */ -provides jdk.jpackage.internal.Bundler with - jdk.jpackage.internal.MacAppBundler, - jdk.jpackage.internal.MacDmgBundler, - jdk.jpackage.internal.MacPkgBundler; - +provides jdk.jpackage.internal.cli.CliBundlingEnvironment with + jdk.jpackage.internal.MacBundlingEnvironment; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java deleted file mode 100644 index 93d037c6a45..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (c) 2018, 2023, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.List; -import java.util.Optional; - -import jdk.internal.util.OperatingSystem; - -import jdk.jpackage.internal.Arguments.CLIOptions; -import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_DATA; -import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME; - -/* - * AddLauncherArguments - * - * Processes a add-launcher properties file to create the Map of - * bundle params applicable to the add-launcher: - * - * BundlerParams p = (new AddLauncherArguments(file)).getLauncherMap(); - * - * A add-launcher is another executable program generated by either the - * create-app-image mode or the create-installer mode. - * The add-launcher may be the same program with different configuration, - * or a completely different program created from the same files. - * - * There may be multiple add-launchers, each created by using the - * command line arg "--add-launcher - * - * The add-launcher properties file may have any of: - * - * appVersion - * description - * module - * main-jar - * main-class - * icon - * arguments - * java-options - * launcher-as-service - * win-console - * win-shortcut - * win-menu - * linux-app-category - * linux-shortcut - * - */ -class AddLauncherArguments { - - private final String name; - private final String filename; - private Map allArgs; - private Map bundleParams; - - AddLauncherArguments(String name, String filename) { - this.name = name; - this.filename = filename; - } - - private void initLauncherMap() { - if (bundleParams != null) { - return; - } - - allArgs = Arguments.getPropertiesFromFile(filename); - allArgs.put(CLIOptions.NAME.getId(), name); - - bundleParams = new HashMap<>(); - String mainJar = getOptionValue(CLIOptions.MAIN_JAR); - String mainClass = getOptionValue(CLIOptions.APPCLASS); - String module = getOptionValue(CLIOptions.MODULE); - - if (module != null && mainClass != null) { - Arguments.putUnlessNull(bundleParams, CLIOptions.MODULE.getId(), - module + "/" + mainClass); - } else if (module != null) { - Arguments.putUnlessNull(bundleParams, CLIOptions.MODULE.getId(), - module); - } else { - Arguments.putUnlessNull(bundleParams, CLIOptions.MAIN_JAR.getId(), - mainJar); - Arguments.putUnlessNull(bundleParams, CLIOptions.APPCLASS.getId(), - mainClass); - } - - Arguments.putUnlessNull(bundleParams, CLIOptions.NAME.getId(), - getOptionValue(CLIOptions.NAME)); - - Arguments.putUnlessNull(bundleParams, CLIOptions.VERSION.getId(), - getOptionValue(CLIOptions.VERSION)); - - Arguments.putUnlessNull(bundleParams, CLIOptions.DESCRIPTION.getId(), - getOptionValue(CLIOptions.DESCRIPTION)); - - Arguments.putUnlessNull(bundleParams, CLIOptions.RELEASE.getId(), - getOptionValue(CLIOptions.RELEASE)); - - Arguments.putUnlessNull(bundleParams, CLIOptions.ICON.getId(), - Optional.ofNullable(getOptionValue(CLIOptions.ICON)).map( - Path::of).orElse(null)); - - Arguments.putUnlessNull(bundleParams, - CLIOptions.LAUNCHER_AS_SERVICE.getId(), getOptionValue( - CLIOptions.LAUNCHER_AS_SERVICE)); - - if (OperatingSystem.isWindows()) { - Arguments.putUnlessNull(bundleParams, - CLIOptions.WIN_CONSOLE_HINT.getId(), - getOptionValue(CLIOptions.WIN_CONSOLE_HINT)); - Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_SHORTCUT_HINT.getId(), - getOptionValue(CLIOptions.WIN_SHORTCUT_HINT)); - Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_MENU_HINT.getId(), - getOptionValue(CLIOptions.WIN_MENU_HINT)); - } - - if (OperatingSystem.isLinux()) { - Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(), - getOptionValue(CLIOptions.LINUX_CATEGORY)); - Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_SHORTCUT_HINT.getId(), - getOptionValue(CLIOptions.LINUX_SHORTCUT_HINT)); - } - - // "arguments" and "java-options" even if value is null: - if (allArgs.containsKey(CLIOptions.ARGUMENTS.getId())) { - String argumentStr = getOptionValue(CLIOptions.ARGUMENTS); - bundleParams.put(CLIOptions.ARGUMENTS.getId(), - Arguments.getArgumentList(argumentStr)); - } - - if (allArgs.containsKey(CLIOptions.JAVA_OPTIONS.getId())) { - String jvmargsStr = getOptionValue(CLIOptions.JAVA_OPTIONS); - bundleParams.put(CLIOptions.JAVA_OPTIONS.getId(), - Arguments.getArgumentList(jvmargsStr)); - } - } - - private String getOptionValue(CLIOptions option) { - if (option == null || allArgs == null) { - return null; - } - - String id = option.getId(); - - if (allArgs.containsKey(id)) { - return allArgs.get(id); - } - - return null; - } - - Map getLauncherMap() { - initLauncherMap(); - return bundleParams; - } - - static Map merge( - Map original, - Map additional, String... exclude) { - Map tmp = new HashMap<>(original); - List.of(exclude).forEach(tmp::remove); - - // remove LauncherData from map so it will be re-computed - tmp.remove(LAUNCHER_DATA.getID()); - // remove "application-name" so it will be re-computed - tmp.remove(APP_NAME.getID()); - - if (additional.containsKey(CLIOptions.MODULE.getId())) { - tmp.remove(CLIOptions.MAIN_JAR.getId()); - tmp.remove(CLIOptions.APPCLASS.getId()); - } else if (additional.containsKey(CLIOptions.MAIN_JAR.getId())) { - tmp.remove(CLIOptions.MODULE.getId()); - } - if (additional.containsKey(CLIOptions.ARGUMENTS.getId())) { - // if add launcher properties file contains "arguments", even with - // null value, disregard the "arguments" from command line - tmp.remove(CLIOptions.ARGUMENTS.getId()); - } - if (additional.containsKey(CLIOptions.JAVA_OPTIONS.getId())) { - // same thing for java-options - tmp.remove(CLIOptions.JAVA_OPTIONS.getId()); - } - tmp.putAll(additional); - return tmp; - } - -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java deleted file mode 100644 index 192630a5656..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (c) 2015, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.StandardBundlerParam.APP_NAME; -import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_DATA; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.MessageFormat; -import java.util.Map; -import java.util.Objects; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.PackagerException; - - -class AppImageBundler extends AbstractBundler { - - @Override - public final String getName() { - return I18N.getString("app.bundler.name"); - } - - @Override - public final String getID() { - return "app"; - } - - @Override - public final String getBundleType() { - return "IMAGE"; - } - - @Override - public final boolean validate(Map params) - throws ConfigException { - try { - Objects.requireNonNull(params); - - if (!params.containsKey(PREDEFINED_APP_IMAGE.getID()) - && !StandardBundlerParam.isRuntimeInstaller(params)) { - LAUNCHER_DATA.fetchFrom(params); - } - - if (paramsValidator != null) { - paramsValidator.validate(params); - } - } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } - } - - return true; - } - - @Override - public final Path execute(Map params, - Path outputParentDir) throws PackagerException { - - final var predefinedAppImage = PREDEFINED_APP_IMAGE.fetchFrom(params); - - try { - if (predefinedAppImage == null) { - Path rootDirectory = createRoot(params, outputParentDir); - appImageSupplier.prepareApplicationFiles(params, rootDirectory); - return rootDirectory; - } else { - appImageSupplier.prepareApplicationFiles(params, predefinedAppImage); - return predefinedAppImage; - } - } catch (PackagerException pe) { - throw pe; - } catch (RuntimeException|IOException ex) { - Log.verbose(ex); - throw new PackagerException(ex); - } - } - - @Override - public final boolean supported(boolean runtimeInstaller) { - return true; - } - - @Override - public final boolean isDefault() { - return false; - } - - @FunctionalInterface - static interface AppImageSupplier { - - void prepareApplicationFiles(Map params, - Path root) throws PackagerException, IOException; - } - - final AppImageBundler setAppImageSupplier(AppImageSupplier v) { - appImageSupplier = v; - return this; - } - - final AppImageBundler setParamsValidator(ParamsValidator v) { - paramsValidator = v; - return this; - } - - @FunctionalInterface - interface ParamsValidator { - void validate(Map params) throws ConfigException; - } - - private Path createRoot(Map params, - Path outputDirectory) throws PackagerException, IOException { - - IOUtils.writableOutputDir(outputDirectory); - - String imageName = APP_NAME.fetchFrom(params); - if (OperatingSystem.isMacOS()) { - imageName = imageName + ".app"; - } - - Log.verbose(MessageFormat.format( - I18N.getString("message.creating-app-bundle"), - imageName, outputDirectory.toAbsolutePath())); - - // Create directory structure - Path rootDirectory = outputDirectory.resolve(imageName); - if (Files.exists(rootDirectory)) { - throw new PackagerException("error.root-exists", - rootDirectory.toAbsolutePath().toString()); - } - - Files.createDirectories(rootDirectory); - - return rootDirectory; - } - - private ParamsValidator paramsValidator; - private AppImageSupplier appImageSupplier; -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java index 8b8a22edc56..ec8c9a31173 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java @@ -25,148 +25,148 @@ package jdk.jpackage.internal; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toMap; -import static java.util.stream.Collectors.toSet; +import static java.util.stream.Collectors.toUnmodifiableMap; +import static java.util.stream.Collectors.toUnmodifiableSet; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.APP_VERSION; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_AS_SERVICE; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_NAME; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.cli.OptionValue; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardAppImageFileOption.AppImageFileOptionScope; +import jdk.jpackage.internal.cli.StandardAppImageFileOption.InvalidOptionValueException; +import jdk.jpackage.internal.cli.StandardAppImageFileOption.MissingMandatoryOptionException; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.ExternalApplication; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.util.XmlUtils; +import jdk.jpackage.internal.util.function.ExceptionBox; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; -final class AppImageFile implements ExternalApplication { +final class AppImageFile { AppImageFile(Application app) { - this(new ApplicationData(app)); - } - - private AppImageFile(ApplicationData app) { - - appVersion = app.version(); - launcherName = app.mainLauncherName(); - mainClass = app.mainLauncherMainClassName(); - extra = app.extra; - creatorVersion = getVersion(); - creatorPlatform = getPlatform(); - addLauncherInfos = app.additionalLaunchers; - } - - @Override - public List getAddLaunchers() { - return addLauncherInfos; - } - - @Override - public String getAppVersion() { - return appVersion; - } - - @Override - public String getAppName() { - return launcherName; - } - - @Override - public String getLauncherName() { - return launcherName; - } - - @Override - public String getMainClass() { - return mainClass; - } - - @Override - public Map getExtra() { - return extra; + appVersion = Objects.requireNonNull(app.version()); + extra = Objects.requireNonNull(app.extraAppImageFileData()); + launcherInfos = app.launchers().stream().map(LauncherInfo::new).toList(); } /** - * Saves file with application image info in application image using values - * from this instance. + * Writes the values captured in this instance into the application image info + * file in the given application layout. + *

+ * It is an equivalent to calling + * {@link #save(ApplicationLayout, OperatingSystem)} method with + * {@code OperatingSystem.current()} for the second parameter. + * + * @param appLayout the application layout + * @throws IOException if an I/O error occurs when writing */ void save(ApplicationLayout appLayout) throws IOException { + save(appLayout, OperatingSystem.current()); + } + + /** + * Writes the values captured in this instance into the application image info + * file in the given application layout. + * + * @param appLayout the application layout + * @param os the target OS + * @throws IOException if an I/O error occurs when writing + */ + void save(ApplicationLayout appLayout, OperatingSystem os) throws IOException { XmlUtils.createXml(getPathInAppImage(appLayout), xml -> { xml.writeStartElement("jpackage-state"); - xml.writeAttribute("version", creatorVersion); - xml.writeAttribute("platform", creatorPlatform); + xml.writeAttribute("version", getVersion()); + xml.writeAttribute("platform", getPlatform(os)); xml.writeStartElement("app-version"); xml.writeCharacters(appVersion); xml.writeEndElement(); - xml.writeStartElement("main-launcher"); - xml.writeCharacters(launcherName); - xml.writeEndElement(); - - xml.writeStartElement("main-class"); - xml.writeCharacters(mainClass); - xml.writeEndElement(); - for (var extraKey : extra.keySet().stream().sorted().toList()) { xml.writeStartElement(extraKey); xml.writeCharacters(extra.get(extraKey)); xml.writeEndElement(); } - for (var li : addLauncherInfos) { - xml.writeStartElement("add-launcher"); - xml.writeAttribute("name", li.name()); - xml.writeAttribute("service", Boolean.toString(li.service())); - for (var extraKey : li.extra().keySet().stream().sorted().toList()) { - xml.writeStartElement(extraKey); - xml.writeCharacters(li.extra().get(extraKey)); - xml.writeEndElement(); - } - xml.writeEndElement(); + launcherInfos.getFirst().save(xml, "main-launcher"); + + for (var li : launcherInfos.subList(1, launcherInfos.size())) { + li.save(xml, "add-launcher"); } }); } /** - * Returns path to application image info file. - * @param appLayout - application layout + * Returns the path to the application image info file in the given application layout. + * + * @param appLayout the application layout */ static Path getPathInAppImage(ApplicationLayout appLayout) { return appLayout.appDirectory().resolve(FILENAME); } /** - * Loads application image info from application image. - * @param appImageDir - path at which to resolve the given application layout - * @param appLayout - application layout + * Loads application image info from the specified application layout. + *

+ * It is an equivalent to calling + * {@link #load(ApplicationLayout, OperatingSystem)} method with + * {@code OperatingSystem.current()} for the second parameter. + * + * @param appLayout the application layout */ - static AppImageFile load(Path appImageDir, ApplicationLayout appLayout) throws ConfigException, IOException { - var srcFilePath = getPathInAppImage(appLayout.resolveAt(appImageDir)); + static ExternalApplication load(ApplicationLayout appLayout) { + return load(appLayout, OperatingSystem.current()); + } + + /** + * Loads application image info from the specified application layout and OS. + * + * @param appLayout the application layout + * @param os the OS defining extra properties of the application and + * additional launchers + */ + static ExternalApplication load(ApplicationLayout appLayout, OperatingSystem os) { + Objects.requireNonNull(appLayout); + Objects.requireNonNull(os); + + final var appImageDir = appLayout.rootDirectory(); + final var appImageFilePath = getPathInAppImage(appLayout); + final var relativeAppImageFilePath = appImageDir.relativize(appImageFilePath); + try { - final Document doc = XmlUtils.initDocumentBuilder().parse(Files.newInputStream(srcFilePath)); + final Document doc = XmlUtils.initDocumentBuilder().parse(Files.newInputStream(appImageFilePath)); final XPath xPath = XPathFactory.newInstance().newXPath(); final var isPlatformValid = XmlUtils.queryNodes(doc, xPath, "/jpackage-state/@platform").findFirst().map( - Node::getNodeValue).map(getPlatform()::equals).orElse(false); + Node::getNodeValue).map(getPlatform(os)::equals).orElse(false); if (!isPlatformValid) { throw new InvalidAppImageFileException(); } @@ -177,102 +177,73 @@ final class AppImageFile implements ExternalApplication { throw new InvalidAppImageFileException(); } - final AppImageProperties props; + final var appOptions = AppImageFileOptionScope.APP.parse(appImageFilePath, AppImageProperties.main(doc, xPath), os); + + final var mainLauncherOptions = LauncherElement.MAIN.readAll(doc, xPath).stream().reduce((_, second) -> { + return second; + }).map(launcherProps -> { + return AppImageFileOptionScope.LAUNCHER.parse(appImageFilePath, launcherProps, os); + }).orElseThrow(InvalidAppImageFileException::new); + + final var addLauncherOptions = LauncherElement.ADDITIONAL.readAll(doc, xPath).stream().map(launcherProps -> { + return AppImageFileOptionScope.LAUNCHER.parse(appImageFilePath, launcherProps, os); + }).toList(); + try { - props = AppImageProperties.main(doc, xPath); - } catch (IllegalArgumentException ex) { + return ExternalApplication.create(Options.concat(appOptions, mainLauncherOptions), addLauncherOptions, os); + } catch (NoSuchElementException ex) { throw new InvalidAppImageFileException(ex); } - final var additionalLaunchers = AppImageProperties.launchers(doc, xPath).stream().map(launcherProps -> { - try { - return new LauncherInfo(launcherProps.get("name"), - launcherProps.find("service").map(Boolean::parseBoolean).orElse(false), launcherProps.getExtra()); - } catch (IllegalArgumentException ex) { - throw new InvalidAppImageFileException(ex); - } - }).toList(); - - return new AppImageFile(new ApplicationData(props.get("app-version"), props.get("main-launcher"), - props.get("main-class"), props.getExtra(), additionalLaunchers)); - } catch (XPathExpressionException ex) { // This should never happen as XPath expressions should be correct - throw new RuntimeException(ex); + throw ExceptionBox.rethrowUnchecked(ex); } catch (SAXException ex) { - // Exception reading input XML (probably malformed XML) - throw new IOException(ex); + // Malformed input XML + throw new JPackageException(I18N.format("error.malformed-app-image-file", relativeAppImageFilePath, appImageDir), ex); } catch (NoSuchFileException ex) { - throw I18N.buildConfigException("error.foreign-app-image", appImageDir).create(); - } catch (InvalidAppImageFileException ex) { + // Don't save the original exception as its error message is redundant. + throw new JPackageException(I18N.format("error.missing-app-image-file", relativeAppImageFilePath, appImageDir)); + } catch (InvalidAppImageFileException|InvalidOptionValueException|MissingMandatoryOptionException ex) { // Invalid input XML - throw I18N.buildConfigException("error.invalid-app-image", appImageDir, srcFilePath).create(); + throw new JPackageException(I18N.format("error.invalid-app-image-file", relativeAppImageFilePath, appImageDir), ex); + } catch (IOException ex) { + throw new JPackageException(I18N.format("error.reading-app-image-file", relativeAppImageFilePath, appImageDir), ex); } } - static boolean getBooleanExtraFieldValue(String fieldId, ExternalApplication appImageFile) { - Objects.requireNonNull(fieldId); - Objects.requireNonNull(appImageFile); - return Optional.ofNullable(appImageFile.getExtra().get(fieldId)).map(Boolean::parseBoolean).orElse(false); - } - static String getVersion() { return System.getProperty("java.version"); } - static String getPlatform() { - return PLATFORM_LABELS.get(OperatingSystem.current()); + static String getPlatform(OperatingSystem os) { + return Objects.requireNonNull(PLATFORM_LABELS.get(Objects.requireNonNull(os))); } + private static final class AppImageProperties { - private AppImageProperties(Map data, Set stdKeys) { - this.data = data; - this.stdKeys = stdKeys; + + static Map main(Document xml, XPath xPath) throws XPathExpressionException { + return queryProperties(xml.getDocumentElement(), xPath, MAIN_PROPERTIES_XPATH_QUERY); } - static AppImageProperties main(Document xml, XPath xPath) throws XPathExpressionException { - final var data = queryProperties(xml.getDocumentElement(), xPath, MAIN_PROPERTIES_XPATH_QUERY); - return new AppImageProperties(data, MAIN_ELEMENT_NAMES); - } + static Map launcher(Element launcherNode, XPath xPath) throws XPathExpressionException { + final var attrData = XmlUtils.toStream(launcherNode.getAttributes()) + .collect(toUnmodifiableMap(Node::getNodeName, Node::getNodeValue)); - static AppImageProperties launcher(Element addLauncherNode, XPath xPath) throws XPathExpressionException { - final var attrData = XmlUtils.toStream(addLauncherNode.getAttributes()) - .collect(toMap(Node::getNodeName, Node::getNodeValue)); - - final var extraData = queryProperties(addLauncherNode, xPath, LAUNCHER_PROPERTIES_XPATH_QUERY); + final var extraData = queryProperties(launcherNode, xPath, LAUNCHER_PROPERTIES_XPATH_QUERY); final Map data = new HashMap<>(attrData); data.putAll(extraData); - return new AppImageProperties(data, LAUNCHER_ATTR_NAMES); - } - - static List launchers(Document xml, XPath xPath) throws XPathExpressionException { - return XmlUtils.queryNodes(xml, xPath, "/jpackage-state/add-launcher") - .map(Element.class::cast).map(toFunction(e -> { - return launcher(e, xPath); - })).toList(); - } - - String get(String name) { - return find(name).orElseThrow(InvalidAppImageFileException::new); - } - - Optional find(String name) { - return Optional.ofNullable(data.get(name)); - } - - Map getExtra() { - Map extra = new HashMap<>(data); - stdKeys.forEach(extra::remove); - return extra; + return data; } private static Map queryProperties(Element e, XPath xPath, String xpathExpr) throws XPathExpressionException { return XmlUtils.queryNodes(e, xPath, xpathExpr) .map(Element.class::cast) - .collect(toMap(Node::getNodeName, selectedElement -> { + .collect(toUnmodifiableMap(Node::getNodeName, selectedElement -> { return selectedElement.getTextContent(); }, (a, b) -> b)); } @@ -285,13 +256,14 @@ final class AppImageFile implements ExternalApplication { return String.format("*[(%s) and not(*)]", otherElementNames); } - private final Map data; - private final Set stdKeys; - - private static final Set LAUNCHER_ATTR_NAMES = Set.of("name", "service"); + private static final Set LAUNCHER_ATTR_NAMES = Stream.of( + LAUNCHER_NAME + ).map(OptionValue::getName).collect(toUnmodifiableSet()); private static final String LAUNCHER_PROPERTIES_XPATH_QUERY = xpathQueryForExtraProperties(LAUNCHER_ATTR_NAMES); - private static final Set MAIN_ELEMENT_NAMES = Set.of("app-version", "main-launcher", "main-class"); + private static final Set MAIN_ELEMENT_NAMES = Stream.of( + APP_VERSION + ).map(OptionValue::getName).collect(toUnmodifiableSet()); private static final String MAIN_PROPERTIES_XPATH_QUERY; static { @@ -301,40 +273,64 @@ final class AppImageFile implements ExternalApplication { MAIN_PROPERTIES_XPATH_QUERY = String.format("%s|/jpackage-state/%s", nonEmptyMainElements, xpathQueryForExtraProperties(Stream.concat(MAIN_ELEMENT_NAMES.stream(), - Stream.of("add-launcher")).collect(toSet()))); + Stream.of("main-launcher", "add-launcher")).collect(toUnmodifiableSet()))); } } - private record ApplicationData(String version, String mainLauncherName, String mainLauncherMainClassName, - Map extra, List additionalLaunchers) { - ApplicationData { - Objects.requireNonNull(version); - Objects.requireNonNull(mainLauncherName); - Objects.requireNonNull(mainLauncherMainClassName); - Objects.requireNonNull(extra); - Objects.requireNonNull(additionalLaunchers); + private enum LauncherElement { + MAIN("main-launcher"), + ADDITIONAL("add-launcher"); - for (final var property : List.of(version, mainLauncherName, mainLauncherMainClassName)) { - if (property.isBlank()) { - throw new IllegalArgumentException(); - } + LauncherElement(String elementName) { + this.elementName = Objects.requireNonNull(elementName); + } + + List> readAll(Document xml, XPath xPath) throws XPathExpressionException { + return XmlUtils.queryNodes(xml, xPath, "/jpackage-state/" + elementName + "[@name]") + .map(Element.class::cast).map(toFunction(e -> { + return AppImageProperties.launcher(e, xPath); + })).toList(); + } + + private final String elementName; + } + + private record LauncherInfo(String name, Map properties) { + LauncherInfo { + Objects.requireNonNull(name); + Objects.requireNonNull(properties); + } + + LauncherInfo(Launcher launcher) { + this(launcher.name(), properties(launcher)); + } + + void save(XMLStreamWriter xml, String elementName) throws IOException, XMLStreamException { + xml.writeStartElement(elementName); + xml.writeAttribute("name", name()); + for (var key : properties().keySet().stream().sorted().toList()) { + xml.writeStartElement(key); + xml.writeCharacters(properties().get(key)); + xml.writeEndElement(); } + xml.writeEndElement(); } - ApplicationData(Application app) { - this(app, app.mainLauncher().orElseThrow()); - } + private static Map properties(Launcher launcher) { + List> standardProps = new ArrayList<>(); + if (launcher.isService()) { + standardProps.add(Map.entry(LAUNCHER_AS_SERVICE.getName(), Boolean.TRUE.toString())); + } - private ApplicationData(Application app, Launcher mainLauncher) { - this(app.version(), mainLauncher.name(), mainLauncher.startupInfo().orElseThrow().qualifiedClassName(), - app.extraAppImageFileData(), app.additionalLaunchers().stream().map(launcher -> { - return new LauncherInfo(launcher.name(), launcher.isService(), - launcher.extraAppImageFileData()); - }).toList()); + return Stream.concat( + standardProps.stream(), + launcher.extraAppImageFileData().entrySet().stream() + ).collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } } + private static class InvalidAppImageFileException extends RuntimeException { InvalidAppImageFileException() { @@ -347,13 +343,10 @@ final class AppImageFile implements ExternalApplication { private static final long serialVersionUID = 1L; } + private final String appVersion; - private final String launcherName; - private final String mainClass; private final Map extra; - private final List addLauncherInfos; - private final String creatorVersion; - private final String creatorPlatform; + private final List launcherInfos; private static final String FILENAME = ".jpackage.xml"; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java index 76a5fc1a50c..e9324f65671 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java @@ -35,12 +35,13 @@ import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import jdk.jpackage.internal.model.AppImageLayout; 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.ExternalApplication.LauncherInfo; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.LauncherIcon; import jdk.jpackage.internal.model.LauncherStartupInfo; @@ -55,11 +56,9 @@ final class ApplicationBuilder { final var launchersAsList = Optional.ofNullable(launchers).map( ApplicationLaunchers::asList).orElseGet(List::of); - final var launcherCount = launchersAsList.size(); - - if (launcherCount != launchersAsList.stream().map(Launcher::name).distinct().count()) { - throw buildConfigException("ERR_NoUniqueName").create(); - } + launchersAsList.stream().collect(Collectors.toMap(Launcher::name, x -> x, (a, b) -> { + throw new JPackageException(I18N.format("error.launcher-duplicate-name", a.name())); + })); final String effectiveName; if (name != null) { @@ -88,25 +87,8 @@ final class ApplicationBuilder { return this; } - ApplicationBuilder initFromExternalApplication(ExternalApplication app, - Function mapper) { - - externalApp = Objects.requireNonNull(app); - - if (version == null) { - version = app.getAppVersion(); - } - if (name == null) { - name = app.getAppName(); - } - runtimeBuilder = null; - - var mainLauncherInfo = new LauncherInfo(app.getLauncherName(), false, Map.of()); - - launchers = new ApplicationLaunchers( - mapper.apply(mainLauncherInfo), - app.getAddLaunchers().stream().map(mapper).toList()); - + ApplicationBuilder externalApplication(ExternalApplication v) { + externalApp = v; return this; } @@ -123,15 +105,6 @@ final class ApplicationBuilder { return Optional.ofNullable(externalApp); } - Optional mainLauncherClassName() { - return launchers() - .map(ApplicationLaunchers::mainLauncher) - .flatMap(Launcher::startupInfo) - .map(LauncherStartupInfo::qualifiedClassName).or(() -> { - return externalApplication().map(ExternalApplication::getMainClass); - }); - } - ApplicationBuilder appImageLayout(AppImageLayout v) { appImageLayout = v; return this; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayoutUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayoutUtils.java deleted file mode 100644 index 0ea72ae160c..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayoutUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ApplicationLayout; - - -final class ApplicationLayoutUtils { - - public static final ApplicationLayout PLATFORM_APPLICATION_LAYOUT; - - private static final ApplicationLayout WIN_APPLICATION_LAYOUT = ApplicationLayout.build() - .setAll("") - .appDirectory("app") - .runtimeDirectory("runtime") - .appModsDirectory(Path.of("app", "mods")) - .create(); - - private static final ApplicationLayout MAC_APPLICATION_LAYOUT = ApplicationLayout.build() - .launchersDirectory("Contents/MacOS") - .appDirectory("Contents/app") - .runtimeDirectory("Contents/runtime/Contents/Home") - .desktopIntegrationDirectory("Contents/Resources") - .appModsDirectory("Contents/app/mods") - .contentDirectory("Contents") - .create(); - - private static final ApplicationLayout LINUX_APPLICATION_LAYOUT = ApplicationLayout.build() - .launchersDirectory("bin") - .appDirectory("lib/app") - .runtimeDirectory("lib/runtime") - .desktopIntegrationDirectory("lib") - .appModsDirectory("lib/app/mods") - .contentDirectory("lib") - .create(); - - static { - switch (OperatingSystem.current()) { - case WINDOWS -> PLATFORM_APPLICATION_LAYOUT = WIN_APPLICATION_LAYOUT; - case MACOS -> PLATFORM_APPLICATION_LAYOUT = MAC_APPLICATION_LAYOUT; - case LINUX -> PLATFORM_APPLICATION_LAYOUT = LINUX_APPLICATION_LAYOUT; - default -> { - throw new UnsupportedOperationException(); - } - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java deleted file mode 100644 index f0323bbd841..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java +++ /dev/null @@ -1,867 +0,0 @@ -/* - * Copyright (c) 2018, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.PackagerException; -import java.io.IOException; -import java.io.Reader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; -import java.util.ResourceBundle; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Arguments - * - * This class encapsulates and processes the command line arguments, - * in effect, implementing all the work of jpackage tool. - * - * The primary entry point, processArguments(): - * Processes and validates command line arguments, constructing DeployParams. - * Validates the DeployParams, and generate the BundleParams. - * Generates List of Bundlers from BundleParams valid for this platform. - * Executes each Bundler in the list. - */ -public class Arguments { - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.MainResources"); - - private static final String FA_EXTENSIONS = "extension"; - private static final String FA_CONTENT_TYPE = "mime-type"; - private static final String FA_DESCRIPTION = "description"; - private static final String FA_ICON = "icon"; - - // Mac specific file association keys - // String - public static final String MAC_CFBUNDLETYPEROLE = "mac.CFBundleTypeRole"; - public static final String MAC_LSHANDLERRANK = "mac.LSHandlerRank"; - public static final String MAC_NSSTORETYPEKEY = "mac.NSPersistentStoreTypeKey"; - public static final String MAC_NSDOCUMENTCLASS = "mac.NSDocumentClass"; - // Boolean - public static final String MAC_LSTYPEISPACKAGE = "mac.LSTypeIsPackage"; - public static final String MAC_LSDOCINPLACE = "mac.LSSupportsOpeningDocumentsInPlace"; - public static final String MAC_UIDOCBROWSER = "mac.UISupportsDocumentBrowser"; - // Array of strings - public static final String MAC_NSEXPORTABLETYPES = "mac.NSExportableTypes"; - public static final String MAC_UTTYPECONFORMSTO = "mac.UTTypeConformsTo"; - - // regexp for parsing args (for example, for additional launchers) - private static Pattern pattern = Pattern.compile( - "(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++"); - - private DeployParams deployParams = null; - - private int pos = 0; - private List argList = null; - - private List allOptions = null; - - private boolean hasMainJar = false; - private boolean hasMainClass = false; - private boolean hasMainModule = false; - public boolean userProvidedBuildRoot = false; - - private String buildRoot = null; - private String mainJarPath = null; - - private boolean runtimeInstaller = false; - - private List addLaunchers = null; - - private static final Map argIds = new HashMap<>(); - private static final Map argShortIds = new HashMap<>(); - - static { - // init maps for parsing arguments - (EnumSet.allOf(CLIOptions.class)).forEach(option -> { - argIds.put(option.getIdWithPrefix(), option); - if (option.getShortIdWithPrefix() != null) { - argShortIds.put(option.getShortIdWithPrefix(), option); - } - }); - } - - private static final InheritableThreadLocal instance = - new InheritableThreadLocal(); - - public Arguments(String[] args) { - instance.set(this); - - argList = new ArrayList(args.length); - for (String arg : args) { - argList.add(arg); - } - - pos = 0; - - deployParams = new DeployParams(); - - allOptions = new ArrayList<>(); - - addLaunchers = new ArrayList<>(); - } - - // CLIOptions is public for DeployParamsTest - public enum CLIOptions { - PACKAGE_TYPE("type", "t", OptionCategories.PROPERTY, () -> { - var type = popArg(); - context().deployParams.setTargetFormat(type); - setOptionValue("type", type); - }), - - INPUT ("input", "i", OptionCategories.PROPERTY, () -> { - setOptionValue("input", popArg()); - }), - - OUTPUT ("dest", "d", OptionCategories.PROPERTY, () -> { - var path = Path.of(popArg()); - setOptionValue("dest", path); - }), - - DESCRIPTION ("description", OptionCategories.PROPERTY), - - VENDOR ("vendor", OptionCategories.PROPERTY), - - APPCLASS ("main-class", OptionCategories.PROPERTY, () -> { - context().hasMainClass = true; - setOptionValue("main-class", popArg()); - }), - - NAME ("name", "n", OptionCategories.PROPERTY), - - VERBOSE ("verbose", OptionCategories.PROPERTY, () -> { - setOptionValue("verbose", true); - Log.setVerbose(); - }), - - RESOURCE_DIR("resource-dir", - OptionCategories.PROPERTY, () -> { - String resourceDir = popArg(); - setOptionValue("resource-dir", resourceDir); - }), - - DMG_CONTENT ("mac-dmg-content", OptionCategories.PROPERTY, () -> { - List content = getArgumentList(popArg()); - content.forEach(a -> setOptionValue("mac-dmg-content", a)); - }), - - ARGUMENTS ("arguments", OptionCategories.PROPERTY, () -> { - List arguments = getArgumentList(popArg()); - setOptionValue("arguments", arguments); - }), - - JLINK_OPTIONS ("jlink-options", OptionCategories.PROPERTY, () -> { - List options = getArgumentList(popArg()); - setOptionValue("jlink-options", options); - }), - - ICON ("icon", OptionCategories.PROPERTY), - - COPYRIGHT ("copyright", OptionCategories.PROPERTY), - - LICENSE_FILE ("license-file", OptionCategories.PROPERTY), - - VERSION ("app-version", OptionCategories.PROPERTY), - - RELEASE ("linux-app-release", OptionCategories.PROPERTY), - - ABOUT_URL ("about-url", OptionCategories.PROPERTY), - - JAVA_OPTIONS ("java-options", OptionCategories.PROPERTY, () -> { - List args = getArgumentList(popArg()); - args.forEach(a -> setOptionValue("java-options", a)); - }), - - APP_CONTENT ("app-content", OptionCategories.PROPERTY, () -> { - getArgumentList(popArg()).forEach( - a -> setOptionValue("app-content", a)); - }), - - FILE_ASSOCIATIONS ("file-associations", - OptionCategories.PROPERTY, () -> { - Map args = new HashMap<>(); - - // load .properties file - Map initialMap = getPropertiesFromFile(popArg()); - - putUnlessNull(args, StandardBundlerParam.FA_EXTENSIONS.getID(), - initialMap.get(FA_EXTENSIONS)); - - putUnlessNull(args, StandardBundlerParam.FA_CONTENT_TYPE.getID(), - initialMap.get(FA_CONTENT_TYPE)); - - putUnlessNull(args, StandardBundlerParam.FA_DESCRIPTION.getID(), - initialMap.get(FA_DESCRIPTION)); - - putUnlessNull(args, StandardBundlerParam.FA_ICON.getID(), - initialMap.get(FA_ICON)); - - // Mac extended file association arguments - putUnlessNull(args, MAC_CFBUNDLETYPEROLE, - initialMap.get(MAC_CFBUNDLETYPEROLE)); - - putUnlessNull(args, MAC_LSHANDLERRANK, - initialMap.get(MAC_LSHANDLERRANK)); - - putUnlessNull(args, MAC_NSSTORETYPEKEY, - initialMap.get(MAC_NSSTORETYPEKEY)); - - putUnlessNull(args, MAC_NSDOCUMENTCLASS, - initialMap.get(MAC_NSDOCUMENTCLASS)); - - putUnlessNull(args, MAC_LSTYPEISPACKAGE, - initialMap.get(MAC_LSTYPEISPACKAGE)); - - putUnlessNull(args, MAC_LSDOCINPLACE, - initialMap.get(MAC_LSDOCINPLACE)); - - putUnlessNull(args, MAC_UIDOCBROWSER, - initialMap.get(MAC_UIDOCBROWSER)); - - putUnlessNull(args, MAC_NSEXPORTABLETYPES, - initialMap.get(MAC_NSEXPORTABLETYPES)); - - putUnlessNull(args, MAC_UTTYPECONFORMSTO, - initialMap.get(MAC_UTTYPECONFORMSTO)); - - ArrayList> associationList = - new ArrayList>(); - - associationList.add(args); - - // check that we really add _another_ value to the list - setOptionValue("file-associations", associationList); - - }), - - ADD_LAUNCHER ("add-launcher", - OptionCategories.PROPERTY, () -> { - String spec = popArg(); - String name = null; - String filename = spec; - if (spec.contains("=")) { - String[] values = spec.split("=", 2); - name = values[0]; - filename = values[1]; - } - context().addLaunchers.add( - new AddLauncherArguments(name, filename)); - }), - - TEMP_ROOT ("temp", OptionCategories.PROPERTY, () -> { - context().buildRoot = popArg(); - context().userProvidedBuildRoot = true; - setOptionValue("temp", context().buildRoot); - }), - - INSTALL_DIR ("install-dir", OptionCategories.PROPERTY), - - PREDEFINED_APP_IMAGE ("app-image", OptionCategories.PROPERTY), - - PREDEFINED_RUNTIME_IMAGE ("runtime-image", OptionCategories.PROPERTY), - - MAIN_JAR ("main-jar", OptionCategories.PROPERTY, () -> { - context().mainJarPath = popArg(); - context().hasMainJar = true; - setOptionValue("main-jar", context().mainJarPath); - }), - - MODULE ("module", "m", OptionCategories.MODULAR, () -> { - context().hasMainModule = true; - setOptionValue("module", popArg()); - }), - - ADD_MODULES ("add-modules", OptionCategories.MODULAR), - - MODULE_PATH ("module-path", "p", OptionCategories.MODULAR), - - LAUNCHER_AS_SERVICE ("launcher-as-service", OptionCategories.PROPERTY, () -> { - setOptionValue("launcher-as-service", true); - }), - - MAC_SIGN ("mac-sign", "s", OptionCategories.PLATFORM_MAC, () -> { - setOptionValue("mac-sign", true); - }), - - MAC_APP_STORE ("mac-app-store", OptionCategories.PLATFORM_MAC, () -> { - setOptionValue("mac-app-store", true); - }), - - MAC_CATEGORY ("mac-app-category", OptionCategories.PLATFORM_MAC), - - MAC_BUNDLE_NAME ("mac-package-name", OptionCategories.PLATFORM_MAC), - - MAC_BUNDLE_IDENTIFIER("mac-package-identifier", - OptionCategories.PLATFORM_MAC), - - MAC_BUNDLE_SIGNING_PREFIX ("mac-package-signing-prefix", - OptionCategories.PLATFORM_MAC), - - MAC_SIGNING_KEY_NAME ("mac-signing-key-user-name", - OptionCategories.PLATFORM_MAC), - - MAC_APP_IMAGE_SIGN_IDENTITY ("mac-app-image-sign-identity", - OptionCategories.PLATFORM_MAC), - - MAC_INSTALLER_SIGN_IDENTITY ("mac-installer-sign-identity", - OptionCategories.PLATFORM_MAC), - - MAC_SIGNING_KEYCHAIN ("mac-signing-keychain", - OptionCategories.PLATFORM_MAC), - - MAC_ENTITLEMENTS ("mac-entitlements", OptionCategories.PLATFORM_MAC), - - WIN_HELP_URL ("win-help-url", OptionCategories.PLATFORM_WIN), - - WIN_UPDATE_URL ("win-update-url", OptionCategories.PLATFORM_WIN), - - WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, - createArgumentWithOptionalValueAction("win-menu")), - - WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), - - WIN_SHORTCUT_HINT ("win-shortcut", OptionCategories.PLATFORM_WIN, - createArgumentWithOptionalValueAction("win-shortcut")), - - WIN_SHORTCUT_PROMPT ("win-shortcut-prompt", - OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-shortcut-prompt", true); - }), - - WIN_PER_USER_INSTALLATION ("win-per-user-install", - OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-per-user-install", false); - }), - - WIN_DIR_CHOOSER ("win-dir-chooser", - OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-dir-chooser", true); - }), - - WIN_UPGRADE_UUID ("win-upgrade-uuid", - OptionCategories.PLATFORM_WIN), - - WIN_CONSOLE_HINT ("win-console", OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-console", true); - }), - - LINUX_BUNDLE_NAME ("linux-package-name", - OptionCategories.PLATFORM_LINUX), - - LINUX_DEB_MAINTAINER ("linux-deb-maintainer", - OptionCategories.PLATFORM_LINUX), - - LINUX_CATEGORY ("linux-app-category", - OptionCategories.PLATFORM_LINUX), - - LINUX_RPM_LICENSE_TYPE ("linux-rpm-license-type", - OptionCategories.PLATFORM_LINUX), - - LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", - OptionCategories.PLATFORM_LINUX), - - LINUX_SHORTCUT_HINT ("linux-shortcut", OptionCategories.PLATFORM_LINUX, - createArgumentWithOptionalValueAction("linux-shortcut")), - - LINUX_MENU_GROUP ("linux-menu-group", OptionCategories.PLATFORM_LINUX); - - private final String id; - private final String shortId; - private final OptionCategories category; - private final Runnable action; - private static Arguments argContext; - - private CLIOptions(String id, OptionCategories category) { - this(id, null, category, null); - } - - private CLIOptions(String id, String shortId, - OptionCategories category) { - this(id, shortId, category, null); - } - - private CLIOptions(String id, - OptionCategories category, Runnable action) { - this(id, null, category, action); - } - - private CLIOptions(String id, String shortId, - OptionCategories category, Runnable action) { - this.id = id; - this.shortId = shortId; - this.action = action; - this.category = category; - } - - public static Arguments context() { - return instance.get(); - } - - public String getId() { - return this.id; - } - - String getIdWithPrefix() { - return "--" + this.id; - } - - String getShortIdWithPrefix() { - return this.shortId == null ? null : "-" + this.shortId; - } - - void execute() { - if (action != null) { - action.run(); - } else { - defaultAction(); - } - } - - private void defaultAction() { - context().deployParams.addBundleArgument(id, popArg()); - } - - private static void setOptionValue(String option, Object value) { - context().deployParams.addBundleArgument(option, value); - } - - private static String popArg() { - nextArg(); - return (context().pos >= context().argList.size()) ? - "" : context().argList.get(context().pos); - } - - private static String getArg() { - return (context().pos >= context().argList.size()) ? - "" : context().argList.get(context().pos); - } - - private static void nextArg() { - context().pos++; - } - - private static void prevArg() { - Objects.checkIndex(context().pos, context().argList.size()); - context().pos--; - } - - private static boolean hasNextArg() { - return context().pos < context().argList.size(); - } - - private static Runnable createArgumentWithOptionalValueAction(String option) { - Objects.requireNonNull(option); - return () -> { - nextArg(); - if (hasNextArg()) { - var value = getArg(); - if (value.startsWith("-")) { - prevArg(); - setOptionValue(option, true); - } else { - setOptionValue(option, value); - } - } else { - setOptionValue(option, true); - } - }; - } - } - - enum OptionCategories { - MODULAR, - PROPERTY, - PLATFORM_MAC, - PLATFORM_WIN, - PLATFORM_LINUX; - } - - public boolean processArguments() { - try { - // parse cmd line - String arg; - CLIOptions option; - for (; CLIOptions.hasNextArg(); CLIOptions.nextArg()) { - arg = CLIOptions.getArg(); - if ((option = toCLIOption(arg)) != null) { - // found a CLI option - allOptions.add(option); - option.execute(); - } else { - throw new PackagerException("ERR_InvalidOption", arg); - } - } - - // display error for arguments that are not supported - // for current configuration. - - validateArguments(); - - List> launchersAsMap = - new ArrayList<>(); - - for (AddLauncherArguments sl : addLaunchers) { - launchersAsMap.add(sl.getLauncherMap()); - } - - deployParams.addBundleArgument( - StandardBundlerParam.ADD_LAUNCHERS.getID(), - launchersAsMap); - - // at this point deployParams should be already configured - - deployParams.validate(); - - BundleParams bp = deployParams.getBundleParams(); - - // validate name(s) - ArrayList usedNames = new ArrayList(); - usedNames.add(bp.getName()); // add main app name - - for (AddLauncherArguments sl : addLaunchers) { - Map slMap = sl.getLauncherMap(); - String slName = - (String) slMap.get(Arguments.CLIOptions.NAME.getId()); - if (slName == null) { - throw new PackagerException("ERR_NoAddLauncherName"); - } - // same rules apply to additional launcher names as app name - DeployParams.validateName(slName, false); - for (String usedName : usedNames) { - if (slName.equals(usedName)) { - throw new PackagerException("ERR_NoUniqueName"); - } - } - usedNames.add(slName); - } - - generateBundle(bp.getBundleParamsAsMap()); - return true; - } catch (Exception e) { - Log.verbose(e); - String msg1 = e.getMessage(); - Log.fatalError(msg1); - if (e.getCause() != null && e.getCause() != e) { - String msg2 = e.getCause().getMessage(); - if (msg2 != null && !msg1.contains(msg2)) { - Log.fatalError(msg2); - } - } - return false; - } - } - - private void validateArguments() throws PackagerException { - String type = deployParams.getTargetFormat(); - String ptype = (type != null) ? type : "default"; - boolean imageOnly = deployParams.isTargetAppImage(); - boolean hasAppImage = allOptions.contains( - CLIOptions.PREDEFINED_APP_IMAGE); - boolean hasRuntime = allOptions.contains( - CLIOptions.PREDEFINED_RUNTIME_IMAGE); - boolean installerOnly = !imageOnly && hasAppImage; - boolean isMac = OperatingSystem.isMacOS(); - runtimeInstaller = !imageOnly && hasRuntime && !hasAppImage && - !hasMainModule && !hasMainJar; - - for (CLIOptions option : allOptions) { - if (!ValidOptions.checkIfSupported(option)) { - // includes option valid only on different platform - throw new PackagerException("ERR_UnsupportedOption", - option.getIdWithPrefix()); - } - if ((imageOnly && !isMac) || (imageOnly && !hasAppImage && isMac)) { - if (!ValidOptions.checkIfImageSupported(option)) { - throw new PackagerException("ERR_InvalidTypeOption", - option.getIdWithPrefix(), type); - } - } else if (imageOnly && hasAppImage && isMac) { // Signing app image - if (!ValidOptions.checkIfSigningSupported(option)) { - throw new PackagerException( - "ERR_InvalidOptionWithAppImageSigning", - option.getIdWithPrefix()); - } - } else if (installerOnly || runtimeInstaller) { - if (!ValidOptions.checkIfInstallerSupported(option)) { - if (runtimeInstaller) { - throw new PackagerException("ERR_NoInstallerEntryPoint", - option.getIdWithPrefix()); - } else { - throw new PackagerException("ERR_InvalidTypeOption", - option.getIdWithPrefix(), ptype); - } - } - } - } - if (hasRuntime) { - if (hasAppImage) { - // note --runtime-image is only for image or runtime installer. - throw new PackagerException("ERR_MutuallyExclusiveOptions", - CLIOptions.PREDEFINED_RUNTIME_IMAGE.getIdWithPrefix(), - CLIOptions.PREDEFINED_APP_IMAGE.getIdWithPrefix()); - } - if (allOptions.contains(CLIOptions.ADD_MODULES)) { - throw new PackagerException("ERR_MutuallyExclusiveOptions", - CLIOptions.PREDEFINED_RUNTIME_IMAGE.getIdWithPrefix(), - CLIOptions.ADD_MODULES.getIdWithPrefix()); - } - if (allOptions.contains(CLIOptions.JLINK_OPTIONS)) { - throw new PackagerException("ERR_MutuallyExclusiveOptions", - CLIOptions.PREDEFINED_RUNTIME_IMAGE.getIdWithPrefix(), - CLIOptions.JLINK_OPTIONS.getIdWithPrefix()); - } - } - if (allOptions.contains(CLIOptions.MAC_SIGNING_KEY_NAME) && - allOptions.contains(CLIOptions.MAC_APP_IMAGE_SIGN_IDENTITY)) { - throw new PackagerException("ERR_MutuallyExclusiveOptions", - CLIOptions.MAC_SIGNING_KEY_NAME.getIdWithPrefix(), - CLIOptions.MAC_APP_IMAGE_SIGN_IDENTITY.getIdWithPrefix()); - } - if (allOptions.contains(CLIOptions.MAC_SIGNING_KEY_NAME) && - allOptions.contains(CLIOptions.MAC_INSTALLER_SIGN_IDENTITY)) { - throw new PackagerException("ERR_MutuallyExclusiveOptions", - CLIOptions.MAC_SIGNING_KEY_NAME.getIdWithPrefix(), - CLIOptions.MAC_INSTALLER_SIGN_IDENTITY.getIdWithPrefix()); - } - if (isMac && (imageOnly || "dmg".equals(type)) && - allOptions.contains(CLIOptions.MAC_INSTALLER_SIGN_IDENTITY)) { - throw new PackagerException("ERR_InvalidTypeOption", - CLIOptions.MAC_INSTALLER_SIGN_IDENTITY.getIdWithPrefix(), - type); - } - if (allOptions.contains(CLIOptions.DMG_CONTENT) - && !("dmg".equals(type))) { - throw new PackagerException("ERR_InvalidTypeOption", - CLIOptions.DMG_CONTENT.getIdWithPrefix(), ptype); - } - if (hasMainJar && hasMainModule) { - throw new PackagerException("ERR_BothMainJarAndModule"); - } - if (imageOnly && !hasAppImage && !hasMainJar && !hasMainModule) { - throw new PackagerException("ERR_NoEntryPoint"); - } - } - - private jdk.jpackage.internal.Bundler getPlatformBundler() { - boolean appImage = deployParams.isTargetAppImage(); - String type = deployParams.getTargetFormat(); - String bundleType = (appImage ? "IMAGE" : "INSTALLER"); - - for (jdk.jpackage.internal.Bundler bundler : - Bundlers.createBundlersInstance().getBundlers(bundleType)) { - if (type == null) { - if (bundler.isDefault()) { - return bundler; - } - } else { - if (appImage || type.equalsIgnoreCase(bundler.getID())) { - return bundler; - } - } - } - return null; - } - - private void generateBundle(Map params) - throws PackagerException { - - // the temp dir needs to be fetched from the params early, - // to prevent each copy of the params (such as may be used for - // additional launchers) from generating a separate temp dir when - // the default is used (the default is a new temp directory) - // The bundler.cleanup() below would not otherwise be able to - // clean these extra (and unneeded) temp directories. - StandardBundlerParam.TEMP_ROOT.fetchFrom(params); - - // determine what bundler to run - jdk.jpackage.internal.Bundler bundler = getPlatformBundler(); - - if (bundler == null || !bundler.supported(runtimeInstaller)) { - String type = Optional.ofNullable(bundler).map(Bundler::getID).orElseGet( - () -> deployParams.getTargetFormat()); - throw new PackagerException("ERR_InvalidInstallerType", type); - } - - Map localParams = new HashMap<>(params); - try { - Path result = executeBundler(bundler, params, localParams); - if (result == null) { - throw new PackagerException("MSG_BundlerFailed", - bundler.getID(), bundler.getName()); - } - Log.verbose(MessageFormat.format( - I18N.getString("message.bundle-created"), - bundler.getName())); - } catch (ConfigException e) { - Log.verbose(e); - if (e.getAdvice() != null) { - throw new PackagerException(e, "MSG_BundlerConfigException", - bundler.getName(), e.getMessage(), e.getAdvice()); - } else { - throw new PackagerException(e, - "MSG_BundlerConfigExceptionNoAdvice", - bundler.getName(), e.getMessage()); - } - } catch (RuntimeException re) { - Log.verbose(re); - throw new PackagerException(re, "MSG_BundlerRuntimeException", - bundler.getName(), re.toString()); - } finally { - if (userProvidedBuildRoot) { - Log.verbose(MessageFormat.format( - I18N.getString("message.debug-working-directory"), - (Path.of(buildRoot)).toAbsolutePath().toString())); - } else { - // always clean up the temporary directory created - // when --temp option not used. - bundler.cleanup(localParams); - } - } - } - - private static Path executeBundler(Bundler bundler, Map params, - Map localParams) throws ConfigException, PackagerException { - try { - bundler.validate(localParams); - return bundler.execute(localParams, StandardBundlerParam.OUTPUT_DIR.fetchFrom(params)); - } catch (ConfigException|PackagerException ex) { - throw ex; - } catch (RuntimeException ex) { - if (ex.getCause() instanceof ConfigException cfgEx) { - throw cfgEx; - } else if (ex.getCause() instanceof PackagerException pkgEx) { - throw pkgEx; - } else { - throw ex; - } - } - } - - static CLIOptions toCLIOption(String arg) { - CLIOptions option; - if ((option = argIds.get(arg)) == null) { - option = argShortIds.get(arg); - } - return option; - } - - static Map getPropertiesFromFile(String filename) { - Map map = new HashMap<>(); - // load properties file - Properties properties = new Properties(); - try (Reader reader = Files.newBufferedReader(Path.of(filename))) { - properties.load(reader); - } catch (IOException e) { - Log.error("Exception: " + e.getMessage()); - } - - for (final String name: properties.stringPropertyNames()) { - map.put(name, properties.getProperty(name)); - } - - return map; - } - - static List getArgumentList(String inputString) { - List list = new ArrayList<>(); - if (inputString == null || inputString.isEmpty()) { - return list; - } - - // The "pattern" regexp attempts to abide to the rule that - // strings are delimited by whitespace unless surrounded by - // quotes, then it is anything (including spaces) in the quotes. - Matcher m = pattern.matcher(inputString); - while (m.find()) { - String s = inputString.substring(m.start(), m.end()).trim(); - // Ensure we do not have an empty string. trim() will take care of - // whitespace only strings. The regex preserves quotes and escaped - // chars so we need to clean them before adding to the List - if (!s.isEmpty()) { - list.add(unquoteIfNeeded(s)); - } - } - return list; - } - - static void putUnlessNull(Map params, - String param, Object value) { - if (value != null) { - params.put(param, value); - } - } - - private static String unquoteIfNeeded(String in) { - if (in == null) { - return null; - } - - if (in.isEmpty()) { - return ""; - } - - // Use code points to preserve non-ASCII chars - StringBuilder sb = new StringBuilder(); - int codeLen = in.codePointCount(0, in.length()); - int quoteChar = -1; - for (int i = 0; i < codeLen; i++) { - int code = in.codePointAt(i); - if (code == '"' || code == '\'') { - // If quote is escaped make sure to copy it - if (i > 0 && in.codePointAt(i - 1) == '\\') { - sb.deleteCharAt(sb.length() - 1); - sb.appendCodePoint(code); - continue; - } - if (quoteChar != -1) { - if (code == quoteChar) { - // close quote, skip char - quoteChar = -1; - } else { - sb.appendCodePoint(code); - } - } else { - // opening quote, skip char - quoteChar = code; - } - } else { - sb.appendCodePoint(code); - } - } - return sb.toString(); - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BasicBundlers.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BasicBundlers.java deleted file mode 100644 index 7f444fe7337..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BasicBundlers.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2014, 2019, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.ServiceLoader; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * BasicBundlers - * - * A basic bundlers collection that loads the default bundlers. - * Loads the common bundlers. - *

    - *
  • Windows file image
  • - *
  • Mac .app
  • - *
  • Linux file image
  • - *
  • Windows MSI
  • - *
  • Windows EXE
  • - *
  • Mac DMG
  • - *
  • Mac PKG
  • - *
  • Linux DEB
  • - *
  • Linux RPM
  • - * - *
- */ -public class BasicBundlers implements Bundlers { - - boolean defaultsLoaded = false; - - private final Collection bundlers = new CopyOnWriteArrayList<>(); - - @Override - public Collection getBundlers() { - return Collections.unmodifiableCollection(bundlers); - } - - @Override - public Collection getBundlers(String type) { - if (type == null) return Collections.emptySet(); - switch (type) { - case "NONE": - return Collections.emptySet(); - case "ALL": - return getBundlers(); - default: - return Arrays.asList(getBundlers().stream() - .filter(b -> type.equalsIgnoreCase(b.getBundleType())) - .toArray(Bundler[]::new)); - } - } - - // Loads bundlers from the META-INF/services direct - @Override - public void loadBundlersFromServices(ClassLoader cl) { - ServiceLoader loader = ServiceLoader.load(Bundler.class, cl); - for (Bundler aLoader : loader) { - bundlers.add(aLoader); - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java new file mode 100644 index 00000000000..8faabe97a3d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli.StandardOption.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.RESOURCE_DIR; +import static jdk.jpackage.internal.cli.StandardOption.TEMP_ROOT; +import static jdk.jpackage.internal.cli.StandardOption.VERBOSE; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.model.Application; +import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.Package; +import jdk.jpackage.internal.model.RuntimeLayout; + +final class BuildEnvFromOptions { + + BuildEnvFromOptions() { + predefinedRuntimeImageLayout(RuntimeLayout.DEFAULT); + } + + BuildEnvFromOptions predefinedAppImageLayout(Function v) { + predefinedAppImageLayout = v; + return this; + } + + BuildEnvFromOptions predefinedAppImageLayout(ApplicationLayout v) { + return predefinedAppImageLayout(path -> v.resolveAt(path)); + } + + BuildEnvFromOptions predefinedRuntimeImageLayout(Function v) { + predefinedRuntimeImageLayout = v; + return this; + } + + BuildEnvFromOptions predefinedRuntimeImageLayout(RuntimeLayout v) { + return predefinedRuntimeImageLayout(path -> v.resolveAt(path)); + } + + BuildEnv create(Options options, Application app) { + return create(options, app, Optional.empty()); + } + + BuildEnv create(Options options, Package pkg) { + return create(options, pkg.app(), Optional.of(pkg)); + } + + private BuildEnv create(Options options, Application app, Optional pkg) { + Objects.requireNonNull(options); + Objects.requireNonNull(app); + Objects.requireNonNull(pkg); + Objects.requireNonNull(predefinedAppImageLayout); + Objects.requireNonNull(predefinedRuntimeImageLayout); + + final var builder = new BuildEnvBuilder(TEMP_ROOT.getFrom(options)); + + RESOURCE_DIR.ifPresentIn(options, builder::resourceDir); + VERBOSE.ifPresentIn(options, builder::verbose); + + if (app.isRuntime()) { + var path = PREDEFINED_RUNTIME_IMAGE.getFrom(options); + builder.appImageLayout(predefinedRuntimeImageLayout.apply(path)); + } else if (PREDEFINED_APP_IMAGE.containsIn(options)) { + var path = PREDEFINED_APP_IMAGE.getFrom(options); + builder.appImageLayout(predefinedAppImageLayout.apply(path)); + } else { + pkg.ifPresentOrElse(builder::appImageDirFor, () -> { + builder.appImageDirFor(app); + }); + } + + return builder.create(); + } + + private Function predefinedAppImageLayout; + private Function predefinedRuntimeImageLayout; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromParams.java deleted file mode 100644 index 6fb1a342fbf..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromParams.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT; -import static jdk.jpackage.internal.StandardBundlerParam.VERBOSE; - -import java.nio.file.Path; -import java.util.Map; -import java.util.function.Function; -import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.RuntimeLayout; - -final class BuildEnvFromParams { - - static BuildEnv create(Map params, - Function predefinedAppImageLayoutProvider, - Function predefinedRuntimeImageLayoutProvider) throws ConfigException { - - final var builder = new BuildEnvBuilder(TEMP_ROOT.fetchFrom(params)); - - RESOURCE_DIR.copyInto(params, builder::resourceDir); - VERBOSE.copyInto(params, builder::verbose); - - final var app = FromParams.APPLICATION.findIn(params).orElseThrow(); - - final var pkg = FromParams.getCurrentPackage(params); - - if (app.isRuntime()) { - var layout = predefinedRuntimeImageLayoutProvider.apply(PREDEFINED_RUNTIME_IMAGE.findIn(params).orElseThrow()); - builder.appImageLayout(layout); - } else if (StandardBundlerParam.hasPredefinedAppImage(params)) { - var layout = predefinedAppImageLayoutProvider.apply(PREDEFINED_APP_IMAGE.findIn(params).orElseThrow()); - builder.appImageLayout(layout); - } else if (pkg.isPresent()) { - builder.appImageDirFor(pkg.orElseThrow()); - } else { - builder.appImageDirFor(app); - } - - return builder.create(); - } - - static final BundlerParamInfo BUILD_ENV = BundlerParamInfo.createBundlerParam(BuildEnv.class, params -> { - return create(params, PLATFORM_APPLICATION_LAYOUT::resolveAt, RuntimeLayout.DEFAULT::resolveAt); - }); -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundleParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundleParams.java deleted file mode 100644 index 937a3655e8b..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundleParams.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2012, 2022, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.HashMap; -import java.util.Map; -import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME; - -public class BundleParams { - - protected final Map params; - - /** - * create a new bundle with all default values - */ - public BundleParams() { - params = new HashMap<>(); - } - - /** - * Create a bundle params with a copy of the params - * @param params map of initial parameters to be copied in. - */ - public BundleParams(Map params) { - this.params = new HashMap<>(params); - } - - public void addAllBundleParams(Map params) { - this.params.putAll(params); - } - - // NOTE: we do not care about application parameters here - // as they will be embedded into jar file manifest and - // java launcher will take care of them! - - public Map getBundleParamsAsMap() { - return new HashMap<>(params); - } - - public String getName() { - return APP_NAME.fetchFrom(params); - } - - private void putUnlessNull(String param, Object value) { - if (value != null) { - params.put(param, value); - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundler.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundler.java deleted file mode 100644 index 0d29677e826..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundler.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2014, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.PackagerException; - -/** - * Bundler - * - * The basic interface implemented by all Bundlers. - */ -public interface Bundler { - /** - * @return User Friendly name of this bundler. - */ - String getName(); - - /** - * @return Command line identifier of the bundler. Should be unique. - */ - String getID(); - - /** - * @return The bundle type of the bundle that is created by this bundler. - */ - String getBundleType(); - - /** - * Determines if this bundler will execute with the given parameters. - * - * @param params The parameters to be validate. Validation may modify - * the map, so if you are going to be using the same map - * across multiple bundlers you should pass in a deep copy. - * @return true if valid - * @throws ConfigException If the configuration params are incorrect. The - * exception may contain advice on how to modify the params map - * to make it valid. - */ - public boolean validate(Map params) - throws ConfigException; - - /** - * Creates a bundle from existing content. - * - * If a call to {@link #validate(java.util.Map)} date} returns true with - * the parameters map, then you can expect a valid output. - * However if an exception was thrown out of validate or it returned - * false then you should not expect sensible results from this call. - * It may or may not return a value, and it may or may not throw an - * exception. But any output should not be considered valid or sane. - * - * @param params The Bundle parameters, - * Keyed by the id from the ParamInfo. Execution may - * modify the map, so if you are going to be using the - * same map across multiple bundlers you should pass - * in a deep copy. - * @param outputParentDir - * The parent dir that the returned bundle will be placed in. - * @return The resulting bundled file - * - * For a bundler that produces a single artifact file this will be the - * location of that artifact (.exe file, .deb file, etc) - * - * For a bundler that produces a specific directory format output this will - * be the location of that specific directory (.app file, etc). - * - * For a bundler that produce multiple files, this will be a parent - * directory of those files (linux and windows images), whose name is not - * relevant to the result. - * - * @throws java.lang.IllegalArgumentException for any of the following - * reasons: - *
    - *
  • A required parameter is not found in the params list, for - * example missing the main class.
  • - *
  • A parameter has the wrong type of an object, for example a - * String where a File is required
  • - *
  • Bundler specific incompatibilities with the parameters, for - * example a bad version number format or an application id with - * forward slashes.
  • - *
- */ - public Path execute(Map params, - Path outputParentDir) throws PackagerException; - - /** - * Removes temporary files that are used for bundling. - */ - public void cleanup(Map params); - - /** - * Returns "true" if this bundler is supported on current platform. - */ - public boolean supported(boolean runtimeInstaller); - - /** - * Returns "true" if this bundler is he default for the current platform. - */ - public boolean isDefault(); -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundlerParamInfo.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundlerParamInfo.java deleted file mode 100644 index 81030b18f6d..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BundlerParamInfo.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2014, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.nio.file.Path; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import jdk.jpackage.internal.util.function.ThrowingFunction; - -/** - * BundlerParamInfo - * - * A BundlerParamInfo encapsulates an individual bundler parameter of type . - * - * @param id The command line and hashmap name of the parameter - * - * @param valueType Type of the parameter - * - * @param defaultValueFunction If the value is not set, and no fallback value is found, the - * parameter uses the value returned by the producer. - * - * @param stringConverter An optional string converter for command line arguments. - */ -record BundlerParamInfo(String id, Class valueType, - Function, T> defaultValueFunction, - BiFunction, T> stringConverter) { - - BundlerParamInfo { - Objects.requireNonNull(id); - Objects.requireNonNull(valueType); - } - - static BundlerParamInfo createStringBundlerParam(String id) { - return new BundlerParamInfo<>(id, String.class, null, null); - } - - static BundlerParamInfo createBooleanBundlerParam(String id) { - return new BundlerParamInfo<>(id, Boolean.class, null, BundlerParamInfo::toBoolean); - } - - static BundlerParamInfo createPathBundlerParam(String id) { - return new BundlerParamInfo<>(id, Path.class, null, BundlerParamInfo::toPath); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - static BundlerParamInfo createBundlerParam(String id, Class valueType, - ThrowingFunction, U> valueCtor) { - return new BundlerParamInfo(id, valueType, ThrowingFunction.toFunction(valueCtor), null); - } - - static BundlerParamInfo createBundlerParam(Class valueType, - ThrowingFunction, U> valueCtor) { - return createBundlerParam(valueType.getName(), valueType, valueCtor); - } - - static boolean toBoolean(String value, Map params) { - if (value == null || "null".equalsIgnoreCase(value)) { - return false; - } else { - return Boolean.valueOf(value); - } - } - - static Path toPath(String value, Map params) { - return Path.of(value); - } - - String getID() { - return id; - } - - Class getValueType() { - return valueType; - } - - /** - * Returns true if value was not provided on command line for this parameter. - * - * @param params - params from which value will be fetch - * @return true if value was not provided on command line, false otherwise - */ - boolean getIsDefaultValue(Map params) { - Object o = params.get(getID()); - if (o != null) { - return false; // We have user provided value - } - - if (params.containsKey(getID())) { - return false; // explicit nulls are allowed for provided value - } - - return true; - } - - Function, T> getDefaultValueFunction() { - return defaultValueFunction; - } - - BiFunction, T> getStringConverter() { - return stringConverter; - } - - final T fetchFrom(Map params) { - return fetchFrom(params, true); - } - - @SuppressWarnings("unchecked") - final T fetchFrom(Map params, - boolean invokeDefault) { - Object o = params.get(getID()); - if (o instanceof String && getStringConverter() != null) { - return getStringConverter().apply((String) o, params); - } - - Class klass = getValueType(); - if (klass.isInstance(o)) { - return (T) o; - } - if (o != null) { - throw new IllegalArgumentException("Param " + getID() - + " should be of type " + getValueType() - + " but is a " + o.getClass()); - } - if (params.containsKey(getID())) { - // explicit nulls are allowed - return null; - } - - if (invokeDefault && (getDefaultValueFunction() != null)) { - T result = getDefaultValueFunction().apply(params); - if (result != null) { - params.put(getID(), result); - } - return result; - } - - // ultimate fallback - return null; - } - - Optional findIn(Map params) { - if (params.containsKey(getID())) { - return Optional.of(fetchFrom(params, true)); - } else { - return Optional.empty(); - } - } - - void copyInto(Map params, Consumer consumer) { - findIn(params).ifPresent(consumer); - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundlers.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundlers.java deleted file mode 100644 index 955952d563d..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Bundlers.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2014, 2019, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.Collection; -import java.util.Iterator; -import java.util.ServiceLoader; - -/** - * Bundlers - * - * The interface implemented by BasicBundlers - */ -public interface Bundlers { - - /** - * This convenience method will call - * {@link #createBundlersInstance(ClassLoader)} - * with the classloader that this Bundlers is loaded from. - * - * @return an instance of Bundlers loaded and configured from - * the current ClassLoader. - */ - public static Bundlers createBundlersInstance() { - return createBundlersInstance(Bundlers.class.getClassLoader()); - } - - /** - * This convenience method will automatically load a Bundlers instance - * from either META-INF/services or the default - * {@link BasicBundlers} if none are found in - * the services meta-inf. - * - * After instantiating the bundlers instance it will load the default - * bundlers via {@link #loadDefaultBundlers()} as well as requesting - * the services loader to load any other bundelrs via - * {@link #loadBundlersFromServices(ClassLoader)}. - - * - * @param servicesClassLoader the classloader to search for - * META-INF/service registered bundlers - * @return an instance of Bundlers loaded and configured from - * the specified ClassLoader - */ - public static Bundlers createBundlersInstance( - ClassLoader servicesClassLoader) { - ServiceLoader bundlersLoader = - ServiceLoader.load(Bundlers.class, servicesClassLoader); - Bundlers bundlers = null; - Iterator iter = bundlersLoader.iterator(); - if (iter.hasNext()) { - bundlers = iter.next(); - } - if (bundlers == null) { - bundlers = new BasicBundlers(); - } - - bundlers.loadBundlersFromServices(servicesClassLoader); - return bundlers; - } - - /** - * Returns all of the preconfigured, requested, and manually - * configured bundlers loaded with this instance. - * - * @return a read-only collection of the requested bundlers - */ - Collection getBundlers(); - - /** - * Returns all of the preconfigured, requested, and manually - * configured bundlers loaded with this instance that are of - * a specific BundleType, such as disk images, installers, or - * remote installers. - * - * @return a read-only collection of the requested bundlers - */ - Collection getBundlers(String type); - - /** - * Loads bundlers from the META-INF/services directly. - * - * This method is called from the - * {@link #createBundlersInstance(ClassLoader)} - * and {@link #createBundlersInstance()} methods. - */ - void loadBundlersFromServices(ClassLoader cl); - -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CLIHelp.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CLIHelp.java deleted file mode 100644 index 7790ebb3ebb..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CLIHelp.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2018, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 jdk.internal.util.OperatingSystem; - -import java.util.ResourceBundle; -import java.io.File; -import java.text.MessageFormat; - - -/** - * CLIHelp - * - * Generate and show the command line interface help message(s). - */ -public class CLIHelp { - - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.HelpResources"); - - // generates --help for jpackage's CLI - public static void showHelp(boolean noArgs) { - - if (noArgs) { - Log.info(I18N.getString("MSG_Help_no_args")); - } else { - OperatingSystem platform = OperatingSystem.current(); - String types; - String pLaunchOptions; - String pInstallOptions; - String pInstallDir; - String pAppImageDescription; - String pSignSampleUsage; - String pAppContentNote; - switch (platform) { - case MACOS: - types = "{\"app-image\", \"dmg\", \"pkg\"}"; - pLaunchOptions = I18N.getString("MSG_Help_mac_launcher"); - pInstallOptions = I18N.getString("MSG_Help_mac_install"); - pInstallDir - = I18N.getString("MSG_Help_mac_linux_install_dir"); - pAppImageDescription - = I18N.getString("MSG_Help_mac_app_image"); - pSignSampleUsage - = I18N.getString("MSG_Help_mac_sign_sample_usage"); - pAppContentNote - = I18N.getString("MSG_Help_mac_app_content_note"); - break; - case LINUX: - types = "{\"app-image\", \"rpm\", \"deb\"}"; - pLaunchOptions = ""; - pInstallOptions = I18N.getString("MSG_Help_linux_install"); - pInstallDir - = I18N.getString("MSG_Help_mac_linux_install_dir"); - pAppImageDescription - = I18N.getString("MSG_Help_default_app_image"); - pSignSampleUsage = ""; - pAppContentNote = ""; - break; - case WINDOWS: - types = "{\"app-image\", \"exe\", \"msi\"}"; - pLaunchOptions = I18N.getString("MSG_Help_win_launcher"); - pInstallOptions = I18N.getString("MSG_Help_win_install"); - pInstallDir - = I18N.getString("MSG_Help_win_install_dir"); - pAppImageDescription - = I18N.getString("MSG_Help_default_app_image"); - pSignSampleUsage = ""; - pAppContentNote = ""; - break; - default: - types = "{\"app-image\", \"exe\", \"msi\", \"rpm\", \"deb\", \"pkg\", \"dmg\"}"; - pLaunchOptions = I18N.getString("MSG_Help_win_launcher") - + I18N.getString("MSG_Help_mac_launcher"); - pInstallOptions = I18N.getString("MSG_Help_win_install") - + I18N.getString("MSG_Help_linux_install") - + I18N.getString("MSG_Help_mac_install"); - pInstallDir - = I18N.getString("MSG_Help_default_install_dir"); - pAppImageDescription - = I18N.getString("MSG_Help_default_app_image"); - pSignSampleUsage = ""; - pAppContentNote = ""; - break; - } - Log.info(MessageFormat.format(I18N.getString("MSG_Help"), - File.pathSeparator, types, pLaunchOptions, - pInstallOptions, pInstallDir, pAppImageDescription, - pSignSampleUsage, pAppContentNote)); - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java index 6b4ba3c4410..9cb9fb5cba0 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java @@ -30,7 +30,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.stream.Stream; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; @@ -47,10 +47,14 @@ final class CfgFile { CfgFile(Application app, Launcher launcher) { startupInfo = launcher.startupInfo().orElseThrow(); outputFileName = launcher.executableName() + ".cfg"; - version = app.version(); + version = Objects.requireNonNull(app.version()); } void create(ApplicationLayout appLayout) throws IOException { + Objects.requireNonNull(appLayout); + + Objects.requireNonNull(startupInfo.qualifiedClassName()); + List> content = new ArrayList<>(); final var refs = new Referencies(appLayout); @@ -58,7 +62,7 @@ final class CfgFile { content.add(Map.entry("[Application]", SECTION_TAG)); if (startupInfo instanceof LauncherModularStartupInfo modularStartupInfo) { - content.add(Map.entry("app.mainmodule", modularStartupInfo.moduleName() + content.add(Map.entry("app.mainmodule", Objects.requireNonNull(modularStartupInfo.moduleName()) + "/" + startupInfo.qualifiedClassName())); } else if (startupInfo instanceof LauncherJarStartupInfo jarStartupInfo) { Path mainJarPath = refs.appDirectory().resolve(jarStartupInfo.jarPath()); @@ -67,16 +71,13 @@ final class CfgFile { content.add(Map.entry("app.mainjar", mainJarPath)); } else { content.add(Map.entry("app.classpath", mainJarPath)); - } - - if (!jarStartupInfo.isJarWithMainClass()) { content.add(Map.entry("app.mainclass", startupInfo.qualifiedClassName())); } } else { throw new UnsupportedOperationException(); } - for (var value : Optional.ofNullable(startupInfo.classPath()).orElseGet(List::of)) { + for (var value : startupInfo.classPath()) { content.add(Map.entry("app.classpath", refs.appDirectory().resolve(value).toString())); } @@ -88,7 +89,7 @@ final class CfgFile { "java-options", "-Djpackage.app-version=" + version)); // add user supplied java options if there are any - for (var value : Optional.ofNullable(startupInfo.javaOptions()).orElseGet(List::of)) { + for (var value : startupInfo.javaOptions()) { content.add(Map.entry("java-options", value)); } @@ -98,7 +99,7 @@ final class CfgFile { content.add(Map.entry("java-options", refs.appModsDirectory())); } - var arguments = Optional.ofNullable(startupInfo.defaultParameters()).orElseGet(List::of); + var arguments = startupInfo.defaultParameters(); if (!arguments.isEmpty()) { content.add(Map.entry("[ArgOptions]", SECTION_TAG)); for (var value : arguments) { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java new file mode 100644 index 00000000000..0bf7d6f41b2 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 java.util.stream.Collectors.toMap; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.cli.CliBundlingEnvironment; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardBundlingOperation; +import jdk.jpackage.internal.model.AppImagePackageType; +import jdk.jpackage.internal.model.Application; +import jdk.jpackage.internal.model.BundlingOperationDescriptor; +import jdk.jpackage.internal.model.JPackageException; +import jdk.jpackage.internal.model.Package; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.Result; + +class DefaultBundlingEnvironment implements CliBundlingEnvironment { + + DefaultBundlingEnvironment(Builder builder) { + this(Optional.ofNullable(builder.defaultOperationSupplier), builder.bundlers); + } + + DefaultBundlingEnvironment(Optional>> defaultOperationSupplier, + Map>>> bundlers) { + + this.bundlers = bundlers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> { + return new CachingSupplier<>(e.getValue()); + })); + + this.defaultOperationSupplier = Objects.requireNonNull(defaultOperationSupplier).map(CachingSupplier::new); + } + + + static final class Builder { + + Builder defaultOperation(Supplier> v) { + defaultOperationSupplier = v; + return this; + } + + Builder defaultOperation(StandardBundlingOperation v) { + return defaultOperation(() -> Optional.of(v.descriptor())); + } + + Builder bundler(StandardBundlingOperation op, Supplier>> bundlerSupplier) { + bundlers.put(Objects.requireNonNull(op.descriptor()), Objects.requireNonNull(bundlerSupplier)); + return this; + } + + Builder bundler(StandardBundlingOperation op, + Supplier> sysEnvResultSupplier, BiConsumer bundler) { + return bundler(op, createBundlerSupplier(sysEnvResultSupplier, bundler)); + } + + Builder bundler(StandardBundlingOperation op, Consumer bundler) { + Objects.requireNonNull(bundler); + return bundler(op, () -> Result.ofValue(bundler)); + } + + private Supplier> defaultOperationSupplier; + private final Map>>> bundlers = new HashMap<>(); + } + + + static Builder build() { + return new Builder(); + } + + static Supplier>> createBundlerSupplier( + Supplier> sysEnvResultSupplier, BiConsumer bundler) { + Objects.requireNonNull(sysEnvResultSupplier); + Objects.requireNonNull(bundler); + return () -> { + return sysEnvResultSupplier.get().map(sysEnv -> { + return options -> { + bundler.accept(options, sysEnv); + }; + }); + }; + } + + static void createApplicationImage(Options options, Application app, PackagingPipeline.Builder pipelineBuilder) { + Objects.requireNonNull(options); + Objects.requireNonNull(app); + Objects.requireNonNull(pipelineBuilder); + + final var outputDir = OptionUtils.outputDir(options).resolve(app.appImageDirName()); + + IOUtils.writableOutputDir(outputDir.getParent()); + + final var env = new BuildEnvFromOptions() + .predefinedAppImageLayout(app.asApplicationLayout().orElseThrow()) + .create(options, app); + + Log.verbose(I18N.format("message.creating-app-bundle", outputDir.getFileName(), outputDir.toAbsolutePath().getParent())); + + if (Files.exists(outputDir)) { + throw new JPackageException(I18N.format("error.root-exists", outputDir.toAbsolutePath())); + } + + pipelineBuilder.excludeDirFromCopying(outputDir.getParent()) + .create().execute(BuildEnv.withAppImageDir(env, outputDir), app); + } + + static void createNativePackage(Options options, + Function createPackage, + BiFunction createBuildEnv, + PackagingPipeline.Builder pipelineBuilder, + Packager.PipelineBuilderMutatorFactory pipelineBuilderMutatorFactory) { + + Objects.requireNonNull(pipelineBuilder); + createNativePackage(options, createPackage, createBuildEnv, _ -> pipelineBuilder, pipelineBuilderMutatorFactory); + } + + static void createNativePackage(Options options, + Function createPackage, + BiFunction createBuildEnv, + Function createPipelineBuilder, + Packager.PipelineBuilderMutatorFactory pipelineBuilderMutatorFactory) { + + Objects.requireNonNull(options); + Objects.requireNonNull(createPackage); + Objects.requireNonNull(createBuildEnv); + Objects.requireNonNull(createPipelineBuilder); + Objects.requireNonNull(pipelineBuilderMutatorFactory); + + var pkg = Objects.requireNonNull(createPackage.apply(options)); + + Packager.build().pkg(pkg) + .outputDir(OptionUtils.outputDir(options)) + .env(Objects.requireNonNull(createBuildEnv.apply(options, pkg))) + .pipelineBuilderMutatorFactory(pipelineBuilderMutatorFactory) + .execute(Objects.requireNonNull(createPipelineBuilder.apply(pkg))); + } + + @Override + public Optional defaultOperation() { + return defaultOperationSupplier.flatMap(Supplier::get); + } + + @Override + public void createBundle(BundlingOperationDescriptor op, Options cmdline) { + final var bundler = getBundlerSupplier(op).get().orElseThrow(); + Optional permanentWorkDirectory = Optional.empty(); + try (var tempDir = new TempDirectory(cmdline)) { + if (!tempDir.deleteOnClose()) { + permanentWorkDirectory = Optional.of(tempDir.path()); + } + bundler.accept(tempDir.options()); + + var packageType = OptionUtils.bundlingOperation(cmdline).packageType(); + + Log.verbose(I18N.format("message.bundle-created", I18N.getString(bundleTypeDescription(packageType, op.os())))); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } finally { + permanentWorkDirectory.ifPresent(workDir -> { + Log.verbose(I18N.format("message.debug-working-directory", workDir.toAbsolutePath())); + }); + } + } + + @Override + public Collection configurationErrors(BundlingOperationDescriptor op) { + return getBundlerSupplier(op).get().errors(); + } + + private Supplier>> getBundlerSupplier(BundlingOperationDescriptor op) { + return Optional.ofNullable(bundlers.get(op)).orElseThrow(NoSuchElementException::new); + } + + private String bundleTypeDescription(PackageType type, OperatingSystem os) { + switch (type) { + case StandardPackageType stdType -> { + switch (stdType) { + case WIN_MSI -> { + return "bundle-type.win-msi"; + } + case WIN_EXE -> { + return "bundle-type.win-exe"; + } + case LINUX_DEB -> { + return "bundle-type.linux-deb"; + } + case LINUX_RPM -> { + return "bundle-type.linux-rpm"; + } + case MAC_DMG -> { + return "bundle-type.mac-dmg"; + } + case MAC_PKG -> { + return "bundle-type.mac-pkg"; + } + default -> { + throw new AssertionError(); + } + } + } + case AppImagePackageType appImageType -> { + switch (os) { + case WINDOWS -> { + return "bundle-type.win-app"; + } + case LINUX -> { + return "bundle-type.linux-app"; + } + case MACOS -> { + return "bundle-type.mac-app"; + } + default -> { + throw new AssertionError(); + } + } + } + default -> { + throw new AssertionError(); + } + } + } + + + private static final class CachingSupplier implements Supplier { + + CachingSupplier(Supplier getter) { + this.getter = Objects.requireNonNull(getter); + } + + @Override + public T get() { + return cachedValue.updateAndGet(v -> { + return Optional.ofNullable(v).orElseGet(getter); + }); + } + + private final Supplier getter; + private final AtomicReference cachedValue = new AtomicReference<>(); + } + + + private final Map>>> bundlers; + private final Optional>> defaultOperationSupplier; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DeployParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DeployParams.java deleted file mode 100644 index d7b4052d34a..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DeployParams.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright (c) 2011, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.InvalidPathException; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Stream; -import jdk.jpackage.internal.model.PackagerException; - -/** - * DeployParams - * - * This class is generated and used in Arguments.processArguments() as - * intermediate step in generating the BundleParams and ultimately the Bundles - */ -public class DeployParams { - - String targetFormat = null; // means default type for this platform - - // raw arguments to the bundler - Map bundlerArguments = new LinkedHashMap<>(); - - static class Template { - Path in; - Path out; - - Template(Path in, Path out) { - this.in = in; - this.out = out; - } - } - - // we need to expand as in some cases - // (most notably jpackage) - // we may get "." as filename and assumption is we include - // everything in the given folder - // (IOUtils.copyfiles() have recursive behavior) - List expandFileset(Path root) throws IOException { - List files = new LinkedList<>(); - if (!Files.isSymbolicLink(root)) { - if (Files.isDirectory(root)) { - try (Stream stream = Files.list(root)) { - List children = stream.toList(); - if (children != null && children.size() > 0) { - children.forEach(f -> { - try { - files.addAll(expandFileset(f)); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - }); - } else { - // Include empty folders - files.add(root); - } - } - } else { - files.add(root); - } - } - return files; - } - - static void validateName(String s, boolean forApp) - throws PackagerException { - - String exceptionKey = forApp ? - "ERR_InvalidAppName" : "ERR_InvalidSLName"; - - if (s == null) { - if (forApp) { - return; - } else { - throw new PackagerException(exceptionKey); - } - } - if (s.length() == 0 || s.charAt(s.length() - 1) == '\\') { - throw new PackagerException(exceptionKey, s); - } - try { - // name must be valid path element for this file system - Path p = Path.of(s); - // and it must be a single name element in a path - if (p.getNameCount() != 1) { - throw new PackagerException(exceptionKey, s); - } - } catch (InvalidPathException ipe) { - throw new PackagerException(ipe, exceptionKey, s); - } - - for (int i = 0; i < s.length(); i++) { - char a = s.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 > '~') { - // Accept anything else including special chars like copyright - // symbols. Note: space will be included by ASCII check above, - // but other whitespace like tabs or new line will be rejected. - if (Character.isISOControl(a) || - Character.isWhitespace(a)) { - throw new PackagerException(exceptionKey, s); - } - } else if (a == '"' || a == '%') { - throw new PackagerException(exceptionKey, s); - } - } - } - - @SuppressWarnings("unchecked") - public void validate() throws PackagerException { - boolean hasModule = (bundlerArguments.get( - Arguments.CLIOptions.MODULE.getId()) != null); - boolean hasAppImage = (bundlerArguments.get( - Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null); - boolean hasMain = (bundlerArguments.get( - Arguments.CLIOptions.MAIN_JAR.getId()) != null); - boolean hasRuntimeImage = (bundlerArguments.get( - Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId()) != null); - boolean hasInput = (bundlerArguments.get( - Arguments.CLIOptions.INPUT.getId()) != null); - boolean hasModulePath = (bundlerArguments.get( - Arguments.CLIOptions.MODULE_PATH.getId()) != null); - boolean hasMacAppStore = (bundlerArguments.get( - Arguments.CLIOptions.MAC_APP_STORE.getId()) != null); - boolean runtimeInstaller = !isTargetAppImage() && - !hasAppImage && !hasModule && !hasMain && hasRuntimeImage; - - if (isTargetAppImage()) { - // Module application requires --runtime-image or --module-path - if (hasModule) { - if (!hasModulePath && !hasRuntimeImage && !hasAppImage) { - throw new PackagerException("ERR_MissingArgument", - "--runtime-image or --module-path"); - } - } else { - if (!hasInput && !hasAppImage) { - throw new PackagerException("error.no-input-parameter"); - } - } - } else { - if (!runtimeInstaller) { - if (hasModule) { - if (!hasModulePath && !hasRuntimeImage && !hasAppImage) { - throw new PackagerException("ERR_MissingArgument", - "--runtime-image, --module-path or --app-image"); - } - } else { - if (!hasInput && !hasAppImage) { - throw new PackagerException("ERR_MissingArgument", - "--input or --app-image"); - } - } - } - } - - // if bundling non-modular image, or installer without app-image - // then we need some resources and a main class - if (!hasModule && !hasAppImage && !runtimeInstaller && !hasMain) { - throw new PackagerException("ERR_MissingArgument", "--main-jar"); - } - - String name = (String)bundlerArguments.get( - Arguments.CLIOptions.NAME.getId()); - validateName(name, true); - - // Validate app image if set - String appImage = (String)bundlerArguments.get( - Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()); - if (appImage != null) { - Path appImageDir = Path.of(appImage); - if (!Files.exists(appImageDir) - || appImageDir.toFile().list() == null - || appImageDir.toFile().list().length == 0) { - throw new PackagerException("ERR_AppImageNotExist", appImage); - } - } - - // Validate temp dir - String root = (String)bundlerArguments.get( - Arguments.CLIOptions.TEMP_ROOT.getId()); - if (root != null && Files.exists(Path.of(root))) { - try (Stream stream = Files.walk(Path.of(root), 1)) { - Path [] contents = stream.toArray(Path[]::new); - // contents.length > 1 because Files.walk(path) includes path - if (contents != null && contents.length > 1) { - throw new PackagerException( - "ERR_BuildRootInvalid", root); - } - } catch (IOException ioe) { - throw new PackagerException(ioe); - } - } - - // Validate resource dir - String resources = (String)bundlerArguments.get( - Arguments.CLIOptions.RESOURCE_DIR.getId()); - if (resources != null) { - if (!(Files.exists(Path.of(resources)))) { - throw new PackagerException( - "message.resource-dir-does-not-exist", - Arguments.CLIOptions.RESOURCE_DIR.getId(), resources); - } - } - - // Validate predefined runtime dir - String runtime = (String)bundlerArguments.get( - Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId()); - if (runtime != null) { - if (!(Files.exists(Path.of(runtime)))) { - throw new PackagerException( - "message.runtime-image-dir-does-not-exist", - Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), - runtime); - } - } - - - // Validate license file if set - String license = (String)bundlerArguments.get( - Arguments.CLIOptions.LICENSE_FILE.getId()); - if (license != null) { - if (!(Files.exists(Path.of(license)))) { - throw new PackagerException("ERR_LicenseFileNotExit"); - } - } - - // Validate icon file if set - String icon = (String)bundlerArguments.get( - Arguments.CLIOptions.ICON.getId()); - if (icon != null) { - if (!(Files.exists(Path.of(icon)))) { - throw new PackagerException("ERR_IconFileNotExit", - Path.of(icon).toAbsolutePath().toString()); - } - } - - - if (hasMacAppStore) { - // Validate jlink-options if mac-app-store is set - Object jlinkOptions = bundlerArguments.get( - Arguments.CLIOptions.JLINK_OPTIONS.getId()); - if (jlinkOptions instanceof List) { - List options = (List) jlinkOptions; - if (!options.contains("--strip-native-commands")) { - throw new PackagerException( - "ERR_MissingJLinkOptMacAppStore", - "--strip-native-commands"); - } - } - } - } - - void setTargetFormat(String t) { - targetFormat = t; - } - - String getTargetFormat() { - return targetFormat; - } - - boolean isTargetAppImage() { - return ("app-image".equals(targetFormat)); - } - - private static final Set multi_args = new TreeSet<>(Arrays.asList( - StandardBundlerParam.JAVA_OPTIONS.getID(), - StandardBundlerParam.ARGUMENTS.getID(), - StandardBundlerParam.MODULE_PATH.getID(), - StandardBundlerParam.ADD_MODULES.getID(), - StandardBundlerParam.LIMIT_MODULES.getID(), - StandardBundlerParam.FILE_ASSOCIATIONS.getID(), - StandardBundlerParam.DMG_CONTENT.getID(), - StandardBundlerParam.APP_CONTENT.getID(), - StandardBundlerParam.JLINK_OPTIONS.getID() - )); - - @SuppressWarnings("unchecked") - public void addBundleArgument(String key, Object value) { - // special hack for multi-line arguments - if (multi_args.contains(key)) { - Object existingValue = bundlerArguments.get(key); - if (existingValue instanceof String && value instanceof String) { - String delim = "\n\n"; - if (key.equals(StandardBundlerParam.MODULE_PATH.getID())) { - delim = File.pathSeparator; - } else if ( - key.equals(StandardBundlerParam.DMG_CONTENT.getID()) || - key.equals(StandardBundlerParam.APP_CONTENT.getID()) || - key.equals(StandardBundlerParam.ADD_MODULES.getID())) { - delim = ","; - } - bundlerArguments.put(key, existingValue + delim + value); - } else if (existingValue instanceof List && value instanceof List) { - ((List)existingValue).addAll((List)value); - } else if (existingValue instanceof Map && - value instanceof String && ((String)value).contains("=")) { - String[] mapValues = ((String)value).split("=", 2); - ((Map)existingValue).put(mapValues[0], mapValues[1]); - } else { - bundlerArguments.put(key, value); - } - } else { - bundlerArguments.put(key, value); - } - } - - BundleParams getBundleParams() { - BundleParams bundleParams = new BundleParams(); - - // check for collisions - TreeSet keys = new TreeSet<>(bundlerArguments.keySet()); - keys.retainAll(bundleParams.getBundleParamsAsMap().keySet()); - - if (!keys.isEmpty()) { - throw new RuntimeException("Deploy Params and Bundler Arguments " - + "overlap in the following values:" + keys.toString()); - } - - bundleParams.addAllBundleParams(bundlerArguments); - - return bundleParams; - } - - @Override - public String toString() { - return "DeployParams {" + "output: " + "}"; - } - -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java index 349a09f237c..ed89ffe1ef6 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java @@ -111,6 +111,10 @@ final record FileAssociationGroup(List items) { return this; } + Optional description() { + return Optional.ofNullable(description); + } + Builder mimeTypes(Collection v) { mimeTypes = Set.copyOf(v); return this; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromOptions.java new file mode 100644 index 00000000000..6b74cab4e65 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromOptions.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.ApplicationBuilder.normalizeIcons; +import static jdk.jpackage.internal.JLinkRuntimeBuilder.ensureBaseModuleInModulePath; +import static jdk.jpackage.internal.OptionUtils.isRuntimeInstaller; +import static jdk.jpackage.internal.cli.StandardOption.ABOUT_URL; +import static jdk.jpackage.internal.cli.StandardOption.ADDITIONAL_LAUNCHERS; +import static jdk.jpackage.internal.cli.StandardOption.ADD_MODULES; +import static jdk.jpackage.internal.cli.StandardOption.APP_CONTENT; +import static jdk.jpackage.internal.cli.StandardOption.APP_VERSION; +import static jdk.jpackage.internal.cli.StandardOption.COPYRIGHT; +import static jdk.jpackage.internal.cli.StandardOption.DESCRIPTION; +import static jdk.jpackage.internal.cli.StandardOption.INPUT; +import static jdk.jpackage.internal.cli.StandardOption.INSTALL_DIR; +import static jdk.jpackage.internal.cli.StandardOption.JLINK_OPTIONS; +import static jdk.jpackage.internal.cli.StandardOption.LICENSE_FILE; +import static jdk.jpackage.internal.cli.StandardOption.MODULE_PATH; +import static jdk.jpackage.internal.cli.StandardOption.NAME; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.RESOURCE_DIR; +import static jdk.jpackage.internal.cli.StandardOption.VENDOR; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.model.Application; +import jdk.jpackage.internal.model.ApplicationLaunchers; +import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LauncherModularStartupInfo; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.RuntimeLayout; + +final class FromOptions { + + static ApplicationBuilderBuilder buildApplicationBuilder() { + return new ApplicationBuilderBuilder(); + } + + static PackageBuilder createPackageBuilder(Options options, Application app, PackageType type) { + + final var builder = new PackageBuilder(app, type); + + NAME.ifPresentIn(options, builder::name); + DESCRIPTION.ifPresentIn(options, builder::description); + APP_VERSION.ifPresentIn(options, builder::version); + ABOUT_URL.ifPresentIn(options, builder::aboutURL); + LICENSE_FILE.ifPresentIn(options, builder::licenseFile); + PREDEFINED_APP_IMAGE.ifPresentIn(options, builder::predefinedAppImage); + PREDEFINED_RUNTIME_IMAGE.ifPresentIn(options, builder::predefinedAppImage); + INSTALL_DIR.ifPresentIn(options, builder::installDir); + + return builder; + } + + + static final class ApplicationBuilderBuilder { + + private ApplicationBuilderBuilder() { + } + + ApplicationBuilder create(Options options, + Function launcherCtor, + BiFunction launcherOverrideCtor, + ApplicationLayout appLayout) { + + final Optional thePredefinedRuntimeLayout; + if (PREDEFINED_RUNTIME_IMAGE.containsIn(options)) { + thePredefinedRuntimeLayout = Optional.ofNullable( + predefinedRuntimeLayout).or(() -> Optional.of(RuntimeLayout.DEFAULT)); + } else { + thePredefinedRuntimeLayout = Optional.empty(); + } + + final var transfomer = new OptionsTransformer(options, appLayout); + final var appBuilder = createApplicationBuilder( + transfomer.appOptions(), + launcherCtor, + launcherOverrideCtor, + appLayout, + Optional.ofNullable(runtimeLayout).orElse(RuntimeLayout.DEFAULT), + thePredefinedRuntimeLayout); + + transfomer.externalApp().ifPresent(appBuilder::externalApplication); + + return appBuilder; + } + + /** + * Sets the layout of the predefined runtime image. + * @param v the layout of the predefined runtime image. Null is permitted. + * @return this + */ + ApplicationBuilderBuilder predefinedRuntimeLayout(RuntimeLayout v) { + predefinedRuntimeLayout = v; + return this; + } + + /** + * Sets the layout of a runtime bundle. + * @param v the layout of a runtime bundle. Null is permitted. + * @return this + */ + ApplicationBuilderBuilder runtimeLayout(RuntimeLayout v) { + runtimeLayout = v; + return this; + } + + private RuntimeLayout runtimeLayout; + private RuntimeLayout predefinedRuntimeLayout; + } + + + private static ApplicationBuilder createApplicationBuilder(Options options, + Function launcherCtor, + BiFunction launcherOverrideCtor, + ApplicationLayout appLayout, RuntimeLayout runtimeLayout, + Optional predefinedRuntimeLayout) { + + final var appBuilder = new ApplicationBuilder(); + + final var isRuntimeInstaller = isRuntimeInstaller(options); + + final var predefinedRuntimeImage = PREDEFINED_RUNTIME_IMAGE.findIn(options); + + final var predefinedRuntimeDirectory = predefinedRuntimeLayout.flatMap(layout -> { + return predefinedRuntimeImage.map(layout::resolveAt); + }).map(RuntimeLayout::runtimeDirectory); + + NAME.findIn(options).or(() -> { + if (isRuntimeInstaller) { + return predefinedRuntimeImage.map(Path::getFileName).map(Path::toString); + } else { + return Optional.empty(); + } + }).ifPresent(appBuilder::name); + DESCRIPTION.ifPresentIn(options, appBuilder::description); + APP_VERSION.ifPresentIn(options, appBuilder::version); + VENDOR.ifPresentIn(options, appBuilder::vendor); + COPYRIGHT.ifPresentIn(options, appBuilder::copyright); + INPUT.ifPresentIn(options, appBuilder::srcDir); + APP_CONTENT.ifPresentIn(options, appBuilder::contentDirs); + + if (isRuntimeInstaller) { + appBuilder.appImageLayout(runtimeLayout); + } else { + appBuilder.appImageLayout(appLayout); + + final var launchers = createLaunchers(options, launcherCtor); + + if (PREDEFINED_APP_IMAGE.containsIn(options)) { + appBuilder.launchers(launchers); + } else { + appBuilder.launchers(normalizeIcons(launchers, RESOURCE_DIR.findIn(options), launcherOverrideCtor)); + + final var runtimeBuilderBuilder = new RuntimeBuilderBuilder(); + + runtimeBuilderBuilder.modulePath(ensureBaseModuleInModulePath(MODULE_PATH.findIn(options).orElseGet(List::of))); + + if (!APP_VERSION.containsIn(options)) { + // Version is not specified explicitly. Try to get it from the app's module. + launchers.mainLauncher().startupInfo().ifPresent(startupInfo -> { + if (startupInfo instanceof LauncherModularStartupInfo modularStartupInfo) { + modularStartupInfo.moduleVersion().ifPresent(moduleVersion -> { + appBuilder.version(moduleVersion); + Log.verbose(I18N.format("message.module-version", + moduleVersion, modularStartupInfo.moduleName())); + }); + } + }); + } + + predefinedRuntimeDirectory.ifPresentOrElse(runtimeBuilderBuilder::forRuntime, () -> { + final var startupInfos = launchers.asList().stream() + .map(Launcher::startupInfo) + .map(Optional::orElseThrow).toList(); + final var jlinkOptionsBuilder = runtimeBuilderBuilder.forNewRuntime(startupInfos); + ADD_MODULES.findIn(options).map(Set::copyOf).ifPresent(jlinkOptionsBuilder::addModules); + JLINK_OPTIONS.ifPresentIn(options, jlinkOptionsBuilder::options); + jlinkOptionsBuilder.apply(); + }); + + appBuilder.runtimeBuilder(runtimeBuilderBuilder.create()); + } + } + + return appBuilder; + } + + private static ApplicationLaunchers createLaunchers(Options options, Function launcherCtor) { + var launchers = ADDITIONAL_LAUNCHERS.getFrom(options); + + var mainLauncher = launcherCtor.apply(options); + + // + // Additional launcher should: + // - Use description from the main launcher by default. + // + var mainLauncherDefaults = Options.of(Map.of(DESCRIPTION, mainLauncher.description())); + + var additionalLaunchers = launchers.stream().map(launcherOptions -> { + return launcherOptions.copyWithParent(mainLauncherDefaults); + }).map(launcherCtor).toList(); + + return new ApplicationLaunchers(mainLauncher, additionalLaunchers); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java deleted file mode 100644 index cee7492dbc7..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.Arguments.CLIOptions.LINUX_SHORTCUT_HINT; -import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_MENU_HINT; -import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_SHORTCUT_HINT; -import static jdk.jpackage.internal.StandardBundlerParam.ABOUT_URL; -import static jdk.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS; -import static jdk.jpackage.internal.StandardBundlerParam.ADD_MODULES; -import static jdk.jpackage.internal.StandardBundlerParam.APP_CONTENT; -import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME; -import static jdk.jpackage.internal.StandardBundlerParam.COPYRIGHT; -import static jdk.jpackage.internal.StandardBundlerParam.DESCRIPTION; -import static jdk.jpackage.internal.StandardBundlerParam.FILE_ASSOCIATIONS; -import static jdk.jpackage.internal.StandardBundlerParam.ICON; -import static jdk.jpackage.internal.StandardBundlerParam.INSTALLER_NAME; -import static jdk.jpackage.internal.StandardBundlerParam.INSTALL_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.JLINK_OPTIONS; -import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_AS_SERVICE; -import static jdk.jpackage.internal.StandardBundlerParam.LICENSE_FILE; -import static jdk.jpackage.internal.StandardBundlerParam.LIMIT_MODULES; -import static jdk.jpackage.internal.StandardBundlerParam.MODULE_PATH; -import static jdk.jpackage.internal.StandardBundlerParam.NAME; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE_FILE; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; -import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.SOURCE_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.VENDOR; -import static jdk.jpackage.internal.StandardBundlerParam.VERSION; -import static jdk.jpackage.internal.StandardBundlerParam.hasPredefinedAppImage; -import static jdk.jpackage.internal.StandardBundlerParam.isRuntimeInstaller; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Function; -import jdk.jpackage.internal.model.Application; -import jdk.jpackage.internal.model.ApplicationLaunchers; -import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.ExternalApplication; -import jdk.jpackage.internal.model.ExternalApplication.LauncherInfo; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.LauncherShortcut; -import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; -import jdk.jpackage.internal.model.PackageType; -import jdk.jpackage.internal.model.ParseUtils; -import jdk.jpackage.internal.model.RuntimeLayout; -import jdk.jpackage.internal.util.function.ThrowingFunction; - -final class FromParams { - - static ApplicationBuilder createApplicationBuilder(Map params, - Function, Launcher> launcherMapper, - BiFunction launcherOverrideCtor, - ApplicationLayout appLayout) throws ConfigException, IOException { - return createApplicationBuilder(params, launcherMapper, launcherOverrideCtor, appLayout, RuntimeLayout.DEFAULT, Optional.of(RuntimeLayout.DEFAULT)); - } - - static ApplicationBuilder createApplicationBuilder(Map params, - Function, Launcher> launcherMapper, - BiFunction launcherOverrideCtor, - ApplicationLayout appLayout, RuntimeLayout runtimeLayout, - Optional predefinedRuntimeLayout) throws ConfigException, IOException { - - final var appBuilder = new ApplicationBuilder(); - - APP_NAME.copyInto(params, appBuilder::name); - DESCRIPTION.copyInto(params, appBuilder::description); - appBuilder.version(VERSION.fetchFrom(params)); - VENDOR.copyInto(params, appBuilder::vendor); - COPYRIGHT.copyInto(params, appBuilder::copyright); - SOURCE_DIR.copyInto(params, appBuilder::srcDir); - APP_CONTENT.copyInto(params, appBuilder::contentDirs); - - final var isRuntimeInstaller = isRuntimeInstaller(params); - - final var predefinedRuntimeImage = PREDEFINED_RUNTIME_IMAGE.findIn(params); - - final var predefinedRuntimeDirectory = predefinedRuntimeLayout.flatMap( - layout -> predefinedRuntimeImage.map(layout::resolveAt)).map(RuntimeLayout::runtimeDirectory); - - if (isRuntimeInstaller) { - appBuilder.appImageLayout(runtimeLayout); - } else { - appBuilder.appImageLayout(appLayout); - - if (hasPredefinedAppImage(params)) { - final var appImageFile = PREDEFINED_APP_IMAGE_FILE.fetchFrom(params); - appBuilder.initFromExternalApplication(appImageFile, launcherInfo -> { - var launcherParams = mapLauncherInfo(appImageFile, launcherInfo); - return launcherMapper.apply(mergeParams(params, launcherParams)); - }); - } else { - final var launchers = createLaunchers(params, launcherMapper); - - final var runtimeBuilderBuilder = new RuntimeBuilderBuilder(); - - runtimeBuilderBuilder.modulePath(MODULE_PATH.fetchFrom(params)); - - predefinedRuntimeDirectory.ifPresentOrElse(runtimeBuilderBuilder::forRuntime, () -> { - final var startupInfos = launchers.asList().stream() - .map(Launcher::startupInfo) - .map(Optional::orElseThrow).toList(); - final var jlinkOptionsBuilder = runtimeBuilderBuilder.forNewRuntime(startupInfos); - ADD_MODULES.copyInto(params, jlinkOptionsBuilder::addModules); - LIMIT_MODULES.copyInto(params, jlinkOptionsBuilder::limitModules); - JLINK_OPTIONS.copyInto(params, jlinkOptionsBuilder::options); - jlinkOptionsBuilder.apply(); - }); - - final var normalizedLaunchers = ApplicationBuilder.normalizeIcons(launchers, RESOURCE_DIR.findIn(params), launcherOverrideCtor); - - appBuilder.launchers(normalizedLaunchers).runtimeBuilder(runtimeBuilderBuilder.create()); - } - } - - return appBuilder; - } - - static PackageBuilder createPackageBuilder( - Map params, Application app, - PackageType type) throws ConfigException { - - final var builder = new PackageBuilder(app, type); - - builder.name(INSTALLER_NAME.fetchFrom(params)); - DESCRIPTION.copyInto(params, builder::description); - VERSION.copyInto(params, builder::version); - ABOUT_URL.copyInto(params, builder::aboutURL); - LICENSE_FILE.findIn(params).map(Path::of).ifPresent(builder::licenseFile); - PREDEFINED_APP_IMAGE.findIn(params).ifPresent(builder::predefinedAppImage); - PREDEFINED_RUNTIME_IMAGE.findIn(params).ifPresent(builder::predefinedAppImage); - INSTALL_DIR.findIn(params).map(Path::of).ifPresent(builder::installDir); - - return builder; - } - - static BundlerParamInfo createApplicationBundlerParam( - ThrowingFunction, T> ctor) { - return BundlerParamInfo.createBundlerParam(Application.class, ctor); - } - - static BundlerParamInfo createPackageBundlerParam( - ThrowingFunction, T> ctor) { - return BundlerParamInfo.createBundlerParam(jdk.jpackage.internal.model.Package.class, ctor); - } - - static Optional getCurrentPackage(Map params) { - return Optional.ofNullable((jdk.jpackage.internal.model.Package)params.get( - jdk.jpackage.internal.model.Package.class.getName())); - } - - static Optional findLauncherShortcut( - BundlerParamInfo shortcutParam, - Map mainParams, - Map launcherParams) { - - Optional launcherValue; - if (launcherParams == mainParams) { - // The main launcher - launcherValue = Optional.empty(); - } else { - launcherValue = shortcutParam.findIn(launcherParams); - } - - return launcherValue.map(ParseUtils::parseLauncherShortcutForAddLauncher).or(() -> { - return Optional.ofNullable(mainParams.get(shortcutParam.getID())).map(toFunction(value -> { - if (value instanceof Boolean) { - return new LauncherShortcut(LauncherShortcutStartupDirectory.DEFAULT); - } else { - try { - return ParseUtils.parseLauncherShortcutForMainLauncher((String)value); - } catch (IllegalArgumentException ex) { - throw I18N.buildConfigException("error.invalid-option-value", value, "--" + shortcutParam.getID()).create(); - } - } - })); - }); - } - - private static ApplicationLaunchers createLaunchers( - Map params, - Function, Launcher> launcherMapper) { - var launchers = ADD_LAUNCHERS.findIn(params).orElseGet(List::of); - - var mainLauncher = launcherMapper.apply(params); - var additionalLaunchers = launchers.stream().map(launcherParams -> { - return launcherMapper.apply(mergeParams(params, launcherParams)); - }).toList(); - - return new ApplicationLaunchers(mainLauncher, additionalLaunchers); - } - - private static Map mapLauncherInfo(ExternalApplication appImageFile, LauncherInfo launcherInfo) { - Map launcherParams = new HashMap<>(); - launcherParams.put(NAME.getID(), launcherInfo.name()); - if (!appImageFile.getLauncherName().equals(launcherInfo.name())) { - // This is not the main launcher, accept the value - // of "launcher-as-service" from the app image file (.jpackage.xml). - launcherParams.put(LAUNCHER_AS_SERVICE.getID(), Boolean.toString(launcherInfo.service())); - } - launcherParams.putAll(launcherInfo.extra()); - return launcherParams; - } - - private static Map mergeParams(Map mainParams, - Map launcherParams) { - if (!launcherParams.containsKey(DESCRIPTION.getID())) { - launcherParams = new HashMap<>(launcherParams); -// FIXME: this is a good improvement but it fails existing tests -// launcherParams.put(DESCRIPTION.getID(), String.format("%s (%s)", DESCRIPTION.fetchFrom( -// mainParams), APP_NAME.fetchFrom(launcherParams))); - launcherParams.put(DESCRIPTION.getID(), DESCRIPTION.fetchFrom(mainParams)); - } - return AddLauncherArguments.merge(mainParams, launcherParams, ICON.getID(), - ADD_LAUNCHERS.getID(), FILE_ASSOCIATIONS.getID(), WIN_MENU_HINT.getId(), - WIN_SHORTCUT_HINT.getId(), LINUX_SHORTCUT_HINT.getId()); - } - - static final BundlerParamInfo APPLICATION = createApplicationBundlerParam(null); -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java index 427051719bb..aac113d7777 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java @@ -32,7 +32,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; -import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.model.JPackageException; /** * IOUtils @@ -90,19 +90,17 @@ final class IOUtils { } } - static void writableOutputDir(Path outdir) throws PackagerException { + static void writableOutputDir(Path outdir) { if (!Files.isDirectory(outdir)) { try { Files.createDirectories(outdir); } catch (IOException ex) { - throw new PackagerException("error.cannot-create-output-dir", - outdir.toAbsolutePath().toString()); + throw new JPackageException(I18N.format("error.cannot-create-output-dir", outdir.toAbsolutePath())); } } if (!Files.isWritable(outdir)) { - throw new PackagerException("error.cannot-write-to-output-dir", - outdir.toAbsolutePath().toString()); + throw new JPackageException(I18N.format("error.cannot-write-to-output-dir", outdir.toAbsolutePath())); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java index 6ac9758e179..50d8049bc2d 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java @@ -36,7 +36,6 @@ import java.lang.module.ModuleReference; import java.lang.module.ResolvedModule; import java.nio.file.Files; import java.nio.file.Path; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -52,9 +51,9 @@ import java.util.stream.Stream; import jdk.internal.module.ModulePath; import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherModularStartupInfo; import jdk.jpackage.internal.model.LauncherStartupInfo; -import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.model.RuntimeBuilder; final class JLinkRuntimeBuilder implements RuntimeBuilder { @@ -64,7 +63,7 @@ final class JLinkRuntimeBuilder implements RuntimeBuilder { } @Override - public void create(AppImageLayout appImageLayout) throws PackagerException { + public void create(AppImageLayout appImageLayout) { var args = new ArrayList(); args.add("--output"); args.add(appImageLayout.runtimeDirectory().toString()); @@ -79,7 +78,7 @@ final class JLinkRuntimeBuilder implements RuntimeBuilder { args.add(0, "jlink"); Log.verbose(args, List.of(jlinkOut), retVal, -1); if (retVal != 0) { - throw new PackagerException("error.jlink.failed", jlinkOut); + throw new JPackageException(I18N.format("error.jlink.failed", jlinkOut)); } } @@ -182,8 +181,7 @@ final class JLinkRuntimeBuilder implements RuntimeBuilder { for (String option : options) { switch (option) { case "--output", "--add-modules", "--module-path" -> { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.blocked.option"), option), null); + throw I18N.buildConfigException("error.blocked.option", option).create(); } default -> { args.add(option); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JPackageToolProvider.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JPackageToolProvider.java deleted file mode 100644 index 079d03a076c..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JPackageToolProvider.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2017, 2022, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.io.PrintWriter; -import java.util.Optional; -import java.util.spi.ToolProvider; - -/** - * JPackageToolProvider - * - * This is the ToolProvider implementation exported - * to java.util.spi.ToolProvider and ultimately javax.tools.ToolProvider - */ -public class JPackageToolProvider implements ToolProvider { - - public String name() { - return "jpackage"; - } - - public Optional description() { - return Optional.of(jdk.jpackage.main.Main.I18N.getString("jpackage.description")); - } - - public synchronized int run( - PrintWriter out, PrintWriter err, String... args) { - try { - return new jdk.jpackage.main.Main().execute(out, err, args); - } catch (RuntimeException re) { - Log.fatalError(re.getMessage()); - Log.verbose(re); - return 1; - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java deleted file mode 100644 index 488e9106479..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (c) 2020, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 jdk.jpackage.internal.model.ConfigException; -import java.io.File; -import java.io.IOException; -import java.lang.module.ModuleReference; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.text.MessageFormat; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; - -/** - * Extracts data needed to run application from parameters. - */ -final class LauncherData { - boolean isModular() { - return moduleInfo != null; - } - - String qualifiedClassName() { - return qualifiedClassName; - } - - boolean isClassNameFromMainJar() { - return jarMainClass != null; - } - - String packageName() { - int sepIdx = qualifiedClassName.lastIndexOf('.'); - if (sepIdx < 0) { - return ""; - } - return qualifiedClassName.substring(sepIdx + 1); - } - - String moduleName() { - verifyIsModular(true); - return moduleInfo.name(); - } - - List modulePath() { - verifyIsModular(true); - return modulePath; - } - - Path mainJarName() { - verifyIsModular(false); - return mainJarName; - } - - List classPath() { - return classPath; - } - - String getAppVersion() { - if (isModular()) { - return moduleInfo.version().orElse(null); - } - - return null; - } - - private LauncherData() { - } - - private void verifyIsModular(boolean isModular) { - if ((moduleInfo == null) == isModular) { - throw new IllegalStateException(); - } - } - - static LauncherData create(Map params) throws - ConfigException, IOException { - - final String mainModule = getMainModule(params); - final LauncherData result; - if (mainModule == null) { - result = createNonModular(params); - } else { - result = createModular(mainModule, params); - } - result.initClasspath(params); - return result; - } - - private static LauncherData createModular(String mainModule, - Map params) throws ConfigException, - IOException { - - LauncherData launcherData = new LauncherData(); - - final int sepIdx = mainModule.indexOf("/"); - final String moduleName; - if (sepIdx > 0) { - launcherData.qualifiedClassName = mainModule.substring(sepIdx + 1); - moduleName = mainModule.substring(0, sepIdx); - } else { - moduleName = mainModule; - } - launcherData.modulePath = getModulePath(params); - - // Try to find module in the specified module path list. - ModuleReference moduleRef = JLinkRuntimeBuilder.createModuleFinder( - launcherData.modulePath).find(moduleName).orElse(null); - - if (moduleRef != null) { - launcherData.moduleInfo = ModuleInfo.fromModuleReference(moduleRef); - } else if (params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID())) { - // Failed to find module in the specified module path list and - // there is external runtime given to jpackage. - // Lookup module in this runtime. - Path cookedRuntime = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); - launcherData.moduleInfo = ModuleInfo.fromCookedRuntime(moduleName, - cookedRuntime).orElse(null); - } - - if (launcherData.moduleInfo == null) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.no-module-in-path"), moduleName), null); - } - - if (launcherData.qualifiedClassName == null) { - launcherData.qualifiedClassName = launcherData.moduleInfo.mainClass().orElse(null); - if (launcherData.qualifiedClassName == null) { - throw new ConfigException(I18N.getString("ERR_NoMainClass"), null); - } - } - - return launcherData; - } - - private static LauncherData createNonModular( - Map params) throws ConfigException, IOException { - LauncherData launcherData = new LauncherData(); - - launcherData.qualifiedClassName = getMainClass(params); - - launcherData.mainJarName = getMainJarName(params); - - Path mainJarDir = StandardBundlerParam.SOURCE_DIR.fetchFrom(params); - - final Path mainJarPath; - if (launcherData.mainJarName != null && mainJarDir != null) { - mainJarPath = mainJarDir.resolve(launcherData.mainJarName); - if (!Files.exists(mainJarPath)) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.main-jar-does-not-exist"), - launcherData.mainJarName), I18N.getString( - "error.main-jar-does-not-exist.advice")); - } - } else { - mainJarPath = null; - } - - if (launcherData.qualifiedClassName == null) { - if (mainJarPath == null) { - throw new ConfigException(I18N.getString("error.no-main-class"), - I18N.getString("error.no-main-class.advice")); - } - - try (JarFile jf = new JarFile(mainJarPath.toFile())) { - Manifest m = jf.getManifest(); - Attributes attrs = (m != null) ? m.getMainAttributes() : null; - if (attrs != null) { - launcherData.qualifiedClassName = attrs.getValue( - Attributes.Name.MAIN_CLASS); - launcherData.jarMainClass = launcherData.qualifiedClassName; - } - } - } - - if (launcherData.qualifiedClassName == null) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.no-main-class-with-main-jar"), - launcherData.mainJarName), MessageFormat.format( - I18N.getString( - "error.no-main-class-with-main-jar.advice"), - launcherData.mainJarName)); - } - - return launcherData; - } - - private void initClasspath(Map params) - throws IOException { - Path inputDir = StandardBundlerParam.SOURCE_DIR.fetchFrom(params); - if (inputDir == null) { - classPath = Collections.emptyList(); - } else { - try (Stream walk = Files.walk(inputDir, Integer.MAX_VALUE)) { - Set jars = walk.filter(Files::isRegularFile) - .filter(file -> file.toString().endsWith(".jar")) - .map(p -> inputDir.toAbsolutePath() - .relativize(p.toAbsolutePath())) - .collect(Collectors.toSet()); - jars.remove(mainJarName); - classPath = jars.stream().sorted().toList(); - } - } - } - - private static String getMainClass(Map params) { - return getStringParam(params, Arguments.CLIOptions.APPCLASS.getId()); - } - - private static Path getMainJarName(Map params) - throws ConfigException { - return getPathParam(params, Arguments.CLIOptions.MAIN_JAR.getId()); - } - - private static String getMainModule(Map params) { - return getStringParam(params, Arguments.CLIOptions.MODULE.getId()); - } - - private static String getStringParam(Map params, - String paramName) { - Optional value = Optional.ofNullable(params.get(paramName)); - return value.map(Object::toString).orElse(null); - } - - private static T getPathParam(String paramName, Supplier func) throws ConfigException { - try { - return func.get(); - } catch (InvalidPathException ex) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.not-path-parameter"), paramName, - ex.getLocalizedMessage()), null, ex); - } - } - - private static Path getPathParam(Map params, - String paramName) throws ConfigException { - return getPathParam(paramName, () -> { - String value = getStringParam(params, paramName); - Path result = null; - if (value != null) { - result = Path.of(value); - } - return result; - }); - } - - private static List getModulePath(Map params) - throws ConfigException { - List modulePath = getPathListParameter(Arguments.CLIOptions.MODULE_PATH.getId(), params); - - if (params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID())) { - Path runtimePath = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); - runtimePath = runtimePath.resolve("lib"); - modulePath = Stream.of(modulePath, List.of(runtimePath)) - .flatMap(List::stream) - .toList(); - } - - return modulePath; - } - - private static List getPathListParameter(String paramName, - Map params) throws ConfigException { - return getPathParam(paramName, () -> - params.get(paramName) instanceof String value ? - Stream.of(value.split(File.pathSeparator)).map(Path::of).toList() : List.of()); - } - - private String qualifiedClassName; - private String jarMainClass; - private Path mainJarName; - private List classPath; - private List modulePath; - private ModuleInfo moduleInfo; -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromOptions.java new file mode 100644 index 00000000000..0749cc48b9b --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromOptions.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli.StandardOption.APPCLASS; +import static jdk.jpackage.internal.cli.StandardOption.ARGUMENTS; +import static jdk.jpackage.internal.cli.StandardOption.DESCRIPTION; +import static jdk.jpackage.internal.cli.StandardOption.FILE_ASSOCIATIONS; +import static jdk.jpackage.internal.cli.StandardOption.ICON; +import static jdk.jpackage.internal.cli.StandardOption.INPUT; +import static jdk.jpackage.internal.cli.StandardOption.JAVA_OPTIONS; +import static jdk.jpackage.internal.cli.StandardOption.LAUNCHER_AS_SERVICE; +import static jdk.jpackage.internal.cli.StandardOption.MAIN_JAR; +import static jdk.jpackage.internal.cli.StandardOption.MODULE; +import static jdk.jpackage.internal.cli.StandardOption.MODULE_PATH; +import static jdk.jpackage.internal.cli.StandardOption.NAME; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.stream.IntStream; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.FileAssociationGroup.FileAssociationException; +import jdk.jpackage.internal.FileAssociationGroup.FileAssociationNoExtensionsException; +import jdk.jpackage.internal.FileAssociationGroup.FileAssociationNoMimesException; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardFaOption; +import jdk.jpackage.internal.model.CustomLauncherIcon; +import jdk.jpackage.internal.model.DefaultLauncherIcon; +import jdk.jpackage.internal.model.FileAssociation; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LauncherIcon; + +final class LauncherFromOptions { + + LauncherFromOptions() { + } + + LauncherFromOptions faGroupBuilderMutator(BiConsumer v) { + faGroupBuilderMutator = v; + return this; + } + + LauncherFromOptions faMapper(BiFunction v) { + faMapper = v; + return this; + } + + LauncherFromOptions faWithDefaultDescription() { + return faGroupBuilderMutator((faGroupBuilder, launcherBuilder) -> { + if (faGroupBuilder.description().isEmpty()) { + var description = String.format("%s association", launcherBuilder.create().name()); + faGroupBuilder.description(description); + } + }); + } + + Launcher create(Options options) { + final var builder = new LauncherBuilder().defaultIconResourceName(defaultIconResourceName()); + + DESCRIPTION.ifPresentIn(options, builder::description); + builder.icon(toLauncherIcon(ICON.findIn(options).orElse(null))); + LAUNCHER_AS_SERVICE.ifPresentIn(options, builder::isService); + NAME.ifPresentIn(options, builder::name); + + if (PREDEFINED_APP_IMAGE.findIn(options).isEmpty()) { + final var startupInfoBuilder = new LauncherStartupInfoBuilder(); + + INPUT.ifPresentIn(options, startupInfoBuilder::inputDir); + ARGUMENTS.ifPresentIn(options, startupInfoBuilder::defaultParameters); + JAVA_OPTIONS.ifPresentIn(options, startupInfoBuilder::javaOptions); + MAIN_JAR.ifPresentIn(options, startupInfoBuilder::mainJar); + APPCLASS.ifPresentIn(options, startupInfoBuilder::mainClassName); + MODULE.ifPresentIn(options, startupInfoBuilder::moduleName); + MODULE_PATH.ifPresentIn(options, startupInfoBuilder::modulePath); + PREDEFINED_RUNTIME_IMAGE.ifPresentIn(options, startupInfoBuilder::predefinedRuntimeImage); + + builder.startupInfo(startupInfoBuilder.create()); + } + + final var faOptionsList = FILE_ASSOCIATIONS.findIn(options).orElseGet(List::of); + + final var faGroups = IntStream.range(0, faOptionsList.size()).mapToObj(idx -> { + final var faOptions = faOptionsList.get(idx); + + final var faGroupBuilder = FileAssociationGroup.build(); + + StandardFaOption.DESCRIPTION.ifPresentIn(faOptions, faGroupBuilder::description); + StandardFaOption.ICON.ifPresentIn(faOptions, faGroupBuilder::icon); + StandardFaOption.EXTENSIONS.ifPresentIn(faOptions, faGroupBuilder::extensions); + StandardFaOption.CONTENT_TYPE.ifPresentIn(faOptions, faGroupBuilder::mimeTypes); + + faGroupBuilderMutator().ifPresent(mutator -> { + mutator.accept(faGroupBuilder, builder); + }); + + final var faID = idx + 1; + + final FileAssociationGroup faGroup; + try { + faGroup = faGroupBuilder.create(); + } catch (FileAssociationNoMimesException ex) { + throw I18N.buildConfigException() + .message("error.no-content-types-for-file-association", faID) + .advice("error.no-content-types-for-file-association.advice", faID) + .create(); + } catch (FileAssociationNoExtensionsException ex) { + // TODO: Must do something about this condition! + throw new AssertionError(); + } catch (FileAssociationException ex) { + // Should never happen + throw new UnsupportedOperationException(ex); + } + + return faMapper().map(mapper -> { + return new FileAssociationGroup(faGroup.items().stream().map(fa -> { + return mapper.apply(faOptions, fa); + }).toList()); + }).orElse(faGroup); + + }).toList(); + + return builder.faGroups(faGroups).create(); + } + + private Optional> faGroupBuilderMutator() { + return Optional.ofNullable(faGroupBuilderMutator); + } + + private Optional> faMapper() { + return Optional.ofNullable(faMapper); + } + + private static LauncherIcon toLauncherIcon(Path launcherIconPath) { + if (launcherIconPath == null) { + return DefaultLauncherIcon.INSTANCE; + } else if (launcherIconPath.toString().isEmpty()) { + return null; + } else { + return CustomLauncherIcon.create(launcherIconPath); + } + } + + private static String defaultIconResourceName() { + switch (OperatingSystem.current()) { + case WINDOWS -> { + return "JavaApp.ico"; + } + case LINUX -> { + return "JavaApp.png"; + } + case MACOS -> { + return "JavaApp.icns"; + } + default -> { + throw new UnsupportedOperationException(); + } + } + } + + private BiConsumer faGroupBuilderMutator; + private BiFunction faMapper; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromParams.java deleted file mode 100644 index b4ce1cdb200..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherFromParams.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (c) 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.I18N.buildConfigException; -import static jdk.jpackage.internal.StandardBundlerParam.ARGUMENTS; -import static jdk.jpackage.internal.StandardBundlerParam.DESCRIPTION; -import static jdk.jpackage.internal.StandardBundlerParam.FA_CONTENT_TYPE; -import static jdk.jpackage.internal.StandardBundlerParam.FA_DESCRIPTION; -import static jdk.jpackage.internal.StandardBundlerParam.FA_EXTENSIONS; -import static jdk.jpackage.internal.StandardBundlerParam.FA_ICON; -import static jdk.jpackage.internal.StandardBundlerParam.FILE_ASSOCIATIONS; -import static jdk.jpackage.internal.StandardBundlerParam.ICON; -import static jdk.jpackage.internal.StandardBundlerParam.JAVA_OPTIONS; -import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_AS_SERVICE; -import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_DATA; -import static jdk.jpackage.internal.StandardBundlerParam.NAME; -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; - -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.stream.IntStream; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.CustomLauncherIcon; -import jdk.jpackage.internal.model.DefaultLauncherIcon; -import jdk.jpackage.internal.model.FileAssociation; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.LauncherIcon; - -record LauncherFromParams(Optional, FileAssociation>> faExtension) { - - LauncherFromParams { - Objects.requireNonNull(faExtension); - } - - LauncherFromParams() { - this(Optional.empty()); - } - - Launcher create(Map params) throws ConfigException { - final var builder = new LauncherBuilder().defaultIconResourceName(defaultIconResourceName()); - - DESCRIPTION.copyInto(params, builder::description); - builder.icon(toLauncherIcon(ICON.findIn(params).orElse(null))); - LAUNCHER_AS_SERVICE.copyInto(params, builder::isService); - NAME.copyInto(params, builder::name); - - if (PREDEFINED_APP_IMAGE.findIn(params).isEmpty()) { - final var startupInfoBuilder = new LauncherStartupInfoBuilder(); - - startupInfoBuilder.launcherData(LAUNCHER_DATA.fetchFrom(params)); - ARGUMENTS.copyInto(params, startupInfoBuilder::defaultParameters); - JAVA_OPTIONS.copyInto(params, startupInfoBuilder::javaOptions); - - builder.startupInfo(startupInfoBuilder.create()); - } - - final var faParamsList = FILE_ASSOCIATIONS.findIn(params).orElseGet(List::of); - - final var faGroups = IntStream.range(0, faParamsList.size()).mapToObj(idx -> { - final var faParams = faParamsList.get(idx); - return toSupplier(() -> { - final var faGroupBuilder = FileAssociationGroup.build(); - - if (OperatingSystem.current() == OperatingSystem.MACOS) { - FA_DESCRIPTION.copyInto(faParams, faGroupBuilder::description); - } else { - faGroupBuilder.description(FA_DESCRIPTION.findIn(faParams).orElseGet(() -> { - return String.format("%s association", toSupplier(builder::create).get().name()); - })); - } - - FA_ICON.copyInto(faParams, faGroupBuilder::icon); - FA_EXTENSIONS.copyInto(faParams, faGroupBuilder::extensions); - FA_CONTENT_TYPE.copyInto(faParams, faGroupBuilder::mimeTypes); - - final var faID = idx + 1; - - final FileAssociationGroup faGroup; - try { - faGroup = faGroupBuilder.create(); - } catch (FileAssociationGroup.FileAssociationNoMimesException ex) { - throw buildConfigException() - .message("error.no-content-types-for-file-association", faID) - .advice("error.no-content-types-for-file-association.advice", faID) - .create(); - } - - if (faExtension.isPresent()) { - return new FileAssociationGroup(faGroup.items().stream().map(fa -> { - return faExtension.get().apply(fa, faParams); - }).toList()); - } else { - return faGroup; - } - }).get(); - }).toList(); - - return builder.faGroups(faGroups).create(); - } - - private static LauncherIcon toLauncherIcon(Path launcherIconPath) { - if (launcherIconPath == null) { - return DefaultLauncherIcon.INSTANCE; - } else if (launcherIconPath.toString().isEmpty()) { - return null; - } else { - return CustomLauncherIcon.create(launcherIconPath); - } - } - - private static String defaultIconResourceName() { - switch (OperatingSystem.current()) { - case WINDOWS -> { - return "JavaApp.ico"; - } - case LINUX -> { - return "JavaApp.png"; - } - case MACOS -> { - return "JavaApp.icns"; - } - default -> { - throw new UnsupportedOperationException(); - } - } - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java index 5273f2d251c..b2fc48af9e4 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java @@ -24,69 +24,216 @@ */ package jdk.jpackage.internal; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.function.UnaryOperator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherJarStartupInfo; import jdk.jpackage.internal.model.LauncherJarStartupInfoMixin; import jdk.jpackage.internal.model.LauncherModularStartupInfo; import jdk.jpackage.internal.model.LauncherModularStartupInfoMixin; -import jdk.jpackage.internal.model.LauncherStartupInfo.Stub; import jdk.jpackage.internal.model.LauncherStartupInfo; final class LauncherStartupInfoBuilder { LauncherStartupInfo create() { - return decorator.apply(new Stub(qualifiedClassName, javaOptions, - defaultParameters, classPath)); + if (moduleName != null) { + return createModular(); + } else if (mainJar != null) { + return createNonModular(); + } else { + throw new JPackageException(I18N.format("ERR_NoEntryPoint")); + } } - LauncherStartupInfoBuilder launcherData(LauncherData launcherData) { - if (launcherData.isModular()) { - decorator = new ModuleStartupInfo(launcherData.moduleName()); - } else { - decorator = new JarStartupInfo(launcherData.mainJarName(), - launcherData.isClassNameFromMainJar()); - } - classPath = launcherData.classPath(); - qualifiedClassName = launcherData.qualifiedClassName(); + LauncherStartupInfoBuilder inputDir(Path v) { + inputDir = v; return this; } LauncherStartupInfoBuilder javaOptions(List v) { + if (v != null) { + v.forEach(Objects::requireNonNull); + } javaOptions = v; return this; } LauncherStartupInfoBuilder defaultParameters(List v) { + if (v != null) { + v.forEach(Objects::requireNonNull); + } defaultParameters = v; return this; } - private static record ModuleStartupInfo(String moduleName) implements UnaryOperator { + LauncherStartupInfoBuilder mainJar(Path v) { + mainJar = v; + return this; + } - @Override - public LauncherStartupInfo apply(LauncherStartupInfo base) { - return LauncherModularStartupInfo.create(base, - new LauncherModularStartupInfoMixin.Stub(moduleName)); + LauncherStartupInfoBuilder mainClassName(String v) { + mainClassName = v; + return this; + } + + LauncherStartupInfoBuilder predefinedRuntimeImage(Path v) { + cookedRuntimePath = v; + return this; + } + + LauncherStartupInfoBuilder moduleName(String v) { + if (v == null) { + moduleName = null; + } else { + var slashIdx = v.indexOf('/'); + if (slashIdx < 0) { + moduleName = v; + } else { + moduleName = v.substring(0, slashIdx); + if (slashIdx < v.length() - 1) { + mainClassName(v.substring(slashIdx + 1)); + } + } + } + return this; + } + + LauncherStartupInfoBuilder modulePath(List v) { + modulePath = v; + return this; + } + + private Optional inputDir() { + return Optional.ofNullable(inputDir); + } + + private Optional mainClassName() { + return Optional.ofNullable(mainClassName); + } + + private Optional cookedRuntimePath() { + return Optional.ofNullable(cookedRuntimePath); + } + + private LauncherStartupInfo createLauncherStartupInfo(String mainClassName, List classpath) { + Objects.requireNonNull(mainClassName); + classpath.forEach(Objects::requireNonNull); + return new LauncherStartupInfo.Stub(mainClassName, + Optional.ofNullable(javaOptions).orElseGet(List::of), + Optional.ofNullable(defaultParameters).orElseGet(List::of), + classpath); + } + + private static List createClasspath(Path inputDir, Set excludes) { + excludes.forEach(Objects::requireNonNull); + try (final var walk = Files.walk(inputDir)) { + return walk.filter(Files::isRegularFile) + .filter(file -> file.getFileName().toString().endsWith(".jar")) + .map(inputDir::relativize) + .filter(Predicate.not(excludes::contains)) + .distinct() + .toList(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); } } - private static record JarStartupInfo(Path jarPath, - boolean isClassNameFromMainJar) implements - UnaryOperator { + private LauncherModularStartupInfo createModular() { + final var fullModulePath = getFullModulePath(); - @Override - public LauncherStartupInfo apply(LauncherStartupInfo base) { - return LauncherJarStartupInfo.create(base, - new LauncherJarStartupInfoMixin.Stub(jarPath, - isClassNameFromMainJar)); - } + // Try to find the module in the specified module path list. + final var moduleInfo = JLinkRuntimeBuilder.createModuleFinder(fullModulePath).find(moduleName) + .map(ModuleInfo::fromModuleReference).or(() -> { + // Failed to find the module in the specified module path list. + return cookedRuntimePath().flatMap(cookedRuntime -> { + // Lookup the module in the external runtime. + return ModuleInfo.fromCookedRuntime(moduleName, cookedRuntime); + }); + }).orElseThrow(() -> { + return I18N.buildConfigException("error.no-module-in-path", moduleName).create(); + }); + + final var effectiveMainClassName = mainClassName().or(moduleInfo::mainClass).orElseThrow(() -> { + return I18N.buildConfigException("ERR_NoMainClass").create(); + }); + + // If module is located in the file system, exclude it from the classpath. + final var classpath = inputDir().map(theInputDir -> { + var classpathExcludes = moduleInfo.fileLocation().filter(moduleFile -> { + return moduleFile.startsWith(theInputDir); + }).map(theInputDir::relativize).map(Set::of).orElseGet(Set::of); + return createClasspath(theInputDir, classpathExcludes); + }).orElseGet(List::of); + + return LauncherModularStartupInfo.create( + createLauncherStartupInfo(effectiveMainClassName, classpath), + new LauncherModularStartupInfoMixin.Stub(moduleInfo.name(), moduleInfo.version())); } - private String qualifiedClassName; + private List getFullModulePath() { + return cookedRuntimePath().map(runtimeImage -> { + return Stream.of(modulePath(), List.of(runtimeImage.resolve("lib"))).flatMap(List::stream).toList(); + }).orElse(modulePath()); + } + + private List modulePath() { + return Optional.ofNullable(modulePath).orElseGet(List::of); + } + + private LauncherJarStartupInfo createNonModular() { + final var theInputDir = inputDir().orElseThrow(); + + final var mainJarPath = theInputDir.resolve(mainJar); + + if (!Files.exists(mainJarPath)) { + throw I18N.buildConfigException() + .message("error.main-jar-does-not-exist", mainJar) + .advice("error.main-jar-does-not-exist.advice") + .create(); + } + + final var effectiveMainClassName = mainClassName().or(() -> { + try (final var jf = new JarFile(mainJarPath.toFile())) { + return Optional.ofNullable(jf.getManifest()).map(Manifest::getMainAttributes).map(attrs -> { + return attrs.getValue(Attributes.Name.MAIN_CLASS); + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }).orElseThrow(() -> { + return I18N.buildConfigException() + .message("error.no-main-class-with-main-jar", mainJar) + .advice("error.no-main-class-with-main-jar.advice", mainJar) + .create(); + }); + + return LauncherJarStartupInfo.create( + createLauncherStartupInfo(effectiveMainClassName, createClasspath(theInputDir, Set.of(mainJar))), + new LauncherJarStartupInfoMixin.Stub(mainJar, mainClassName().isEmpty())); + } + + // Modular options + private String moduleName; + private List modulePath; + + // Non-modular options + private Path mainJar; + + // Common options + private Path inputDir; + private String mainClassName; private List javaOptions; private List defaultParameters; - private List classPath; - private UnaryOperator decorator; + private Path cookedRuntimePath; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java new file mode 100644 index 00000000000..97e1274f078 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli.StandardOption.BUNDLING_OPERATION_DESCRIPTOR; +import static jdk.jpackage.internal.cli.StandardOption.DEST; +import static jdk.jpackage.internal.cli.StandardOption.MAIN_JAR; +import static jdk.jpackage.internal.cli.StandardOption.MODULE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; + +import java.nio.file.Path; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardBundlingOperation; + +final class OptionUtils { + + static boolean isRuntimeInstaller(Options options) { + return PREDEFINED_RUNTIME_IMAGE.containsIn(options) + && !PREDEFINED_APP_IMAGE.containsIn(options) + && !MAIN_JAR.containsIn(options) + && !MODULE.containsIn(options); + } + + static Path outputDir(Options options) { + return DEST.getFrom(options); + } + + static StandardBundlingOperation bundlingOperation(Options options) { + return StandardBundlingOperation.valueOf(BUNDLING_OPERATION_DESCRIPTOR.getFrom(options)).orElseThrow(); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionsTransformer.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionsTransformer.java new file mode 100644 index 00000000000..b146e5a7f8d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionsTransformer.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli.StandardOption.ADDITIONAL_LAUNCHERS; +import static jdk.jpackage.internal.cli.StandardOption.APP_VERSION; +import static jdk.jpackage.internal.cli.StandardOption.DESCRIPTION; +import static jdk.jpackage.internal.cli.StandardOption.ICON; +import static jdk.jpackage.internal.cli.StandardOption.NAME; +import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.WithOptionIdentifier; +import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.ExternalApplication; + +record OptionsTransformer(Options mainOptions, Optional externalApp) { + + OptionsTransformer { + Objects.requireNonNull(mainOptions); + Objects.requireNonNull(externalApp); + } + + OptionsTransformer(Options mainOptions, ApplicationLayout appLayout) { + this(mainOptions, PREDEFINED_APP_IMAGE.findIn(mainOptions).map(appLayout::resolveAt).map(AppImageFile::load)); + } + + Options appOptions() { + return externalApp.map(ea -> { + var overrideOptions = Map.of( + NAME, ea.appName(), + APP_VERSION, ea.appVersion(), + ADDITIONAL_LAUNCHERS, ea.addLaunchers().stream().map(li -> { + return Options.concat(li.extra(), Options.of(Map.of( + NAME, li.name(), + // This should prevent the code building the Launcher instance + // from the Options object from trying to create a startup info object. + PREDEFINED_APP_IMAGE, PREDEFINED_APP_IMAGE.getFrom(mainOptions), + // + // For backward compatibility, descriptions of the additional + // launchers in the predefined app image will be set to + // the application description, if available, or to the name + // of the main launcher in the predefined app image. + // + // All launchers in the predefined app image will have the same description. + // This is wrong and should be revised. + // + DESCRIPTION, DESCRIPTION.findIn(mainOptions).orElseGet(ea::appName) + ))); + }).toList() + ); + return Options.concat( + Options.of(overrideOptions), + ea.extra(), + // Remove icon if any from the application/launcher options. + // If the icon is specified in the main options, it for the installer. + mainOptions.copyWithout(ICON.id()) + ); + }).orElse(mainOptions); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java index 501fd64bdca..8e47b046eb1 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java @@ -29,7 +29,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.model.PackagerException; final class Packager { @@ -69,7 +68,7 @@ final class Packager { return Objects.requireNonNull(env); } - Path execute(PackagingPipeline.Builder pipelineBuilder) throws PackagerException { + Path execute(PackagingPipeline.Builder pipelineBuilder) { Objects.requireNonNull(pkg); Objects.requireNonNull(env); Objects.requireNonNull(outputDir); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java index 6f4e0d0d2d8..a2750fee260 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -46,7 +46,6 @@ import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.pipeline.DirectedEdge; import jdk.jpackage.internal.pipeline.FixedDAG; import jdk.jpackage.internal.pipeline.TaskPipelineBuilder; @@ -62,7 +61,7 @@ final class PackagingPipeline { * @param env the build environment * @param app the application */ - void execute(BuildEnv env, Application app) throws PackagerException { + void execute(BuildEnv env, Application app) { execute(contextMapper.apply(createTaskContext(env, app))); } @@ -81,7 +80,7 @@ final class PackagingPipeline { * @param pkg the package * @param outputDir the output directory for the package file */ - void execute(BuildEnv env, Package pkg, Path outputDir) throws PackagerException { + void execute(BuildEnv env, Package pkg, Path outputDir) { execute((StartupParameters)createPackagingTaskContext(env, pkg, outputDir, taskConfig)); } @@ -91,7 +90,7 @@ final class PackagingPipeline { * * @param startupParameters the pipeline startup parameters */ - void execute(StartupParameters startupParameters) throws PackagerException { + void execute(StartupParameters startupParameters) { execute(contextMapper.apply(createTaskContext((PackagingTaskContext)startupParameters))); } @@ -132,7 +131,7 @@ final class PackagingPipeline { } interface TaskContext extends Predicate { - void execute(TaskAction taskAction) throws IOException, PackagerException; + void execute(TaskAction taskAction) throws IOException; } record AppImageBuildEnv(BuildEnv env, T app) { @@ -161,27 +160,27 @@ final class PackagingPipeline { @FunctionalInterface interface ApplicationImageTaskAction extends TaskAction { - void execute(AppImageBuildEnv env) throws IOException, PackagerException; + void execute(AppImageBuildEnv env) throws IOException; } @FunctionalInterface interface AppImageTaskAction extends TaskAction { - void execute(AppImageBuildEnv env) throws IOException, PackagerException; + void execute(AppImageBuildEnv env) throws IOException; } @FunctionalInterface interface CopyAppImageTaskAction extends TaskAction { - void execute(T pkg, AppImageLayout srcAppImage, AppImageLayout dstAppImage) throws IOException, PackagerException; + void execute(T pkg, AppImageLayout srcAppImage, AppImageLayout dstAppImage) throws IOException; } @FunctionalInterface interface PackageTaskAction extends TaskAction { - void execute(PackageBuildEnv env) throws IOException, PackagerException; + void execute(PackageBuildEnv env) throws IOException; } @FunctionalInterface interface NoArgTaskAction extends TaskAction { - void execute() throws IOException, PackagerException; + void execute() throws IOException; } record TaskConfig(Optional action) { @@ -493,7 +492,7 @@ final class PackagingPipeline { return new PackagingTaskContext(BuildEnv.withAppImageLayout(env, dstLayout), pkg, outputDir, srcLayout); } - private void execute(TaskContext context) throws PackagerException { + private void execute(TaskContext context) { final Map> tasks = taskConfig.entrySet().stream().collect(toMap(Map.Entry::getKey, task -> { return createTask(context, task.getKey(), task.getValue()); })); @@ -508,14 +507,8 @@ final class PackagingPipeline { try { builder.create().call(); - } catch (ExceptionBox ex) { - throw new PackagerException(ex.getCause()); - } catch (RuntimeException ex) { - throw ex; - } catch (PackagerException ex) { - throw ex; } catch (Exception ex) { - throw new PackagerException(ex); + throw ExceptionBox.rethrowUnchecked(ex); } } @@ -546,7 +539,7 @@ final class PackagingPipeline { @SuppressWarnings("unchecked") @Override - public void execute(TaskAction taskAction) throws IOException, PackagerException { + public void execute(TaskAction taskAction) throws IOException { if (taskAction instanceof PackageTaskAction) { ((PackageTaskAction)taskAction).execute(pkgBuildEnv()); } else if (taskAction instanceof CopyAppImageTaskAction) { @@ -600,7 +593,7 @@ final class PackagingPipeline { @SuppressWarnings("unchecked") @Override - public void execute(TaskAction taskAction) throws IOException, PackagerException { + public void execute(TaskAction taskAction) throws IOException { if (taskAction instanceof AppImageTaskAction) { final var taskEnv = pkg.map(PackagingTaskContext::appImageBuildEnv).orElseGet(this::appBuildEnv); ((AppImageTaskAction)taskAction).execute(taskEnv); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java deleted file mode 100644 index 2b35a6830f8..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java +++ /dev/null @@ -1,513 +0,0 @@ -/* - * Copyright (c) 2014, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.ExternalApplication; -import static jdk.jpackage.internal.ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT; - -/** - * Standard bundler parameters. - * - * Contains static definitions of all of the common bundler parameters. - * (additional platform specific and mode specific bundler parameters - * are defined in each of the specific bundlers) - * - * Also contains static methods that operate on maps of parameters. - */ -final class StandardBundlerParam { - - private static final String DEFAULT_VERSION = "1.0"; - private static final String DEFAULT_RELEASE = "1"; - private static final String[] DEFAULT_JLINK_OPTIONS = { - "--strip-native-commands", - "--strip-debug", - "--no-man-pages", - "--no-header-files"}; - - static final BundlerParamInfo LAUNCHER_DATA = BundlerParamInfo.createBundlerParam( - LauncherData.class, LauncherData::create); - - static final BundlerParamInfo SOURCE_DIR = - new BundlerParamInfo<>( - Arguments.CLIOptions.INPUT.getId(), - Path.class, - p -> null, - (s, p) -> Path.of(s) - ); - - static final BundlerParamInfo OUTPUT_DIR = - new BundlerParamInfo<>( - Arguments.CLIOptions.OUTPUT.getId(), - Path.class, - p -> Path.of("").toAbsolutePath(), - (s, p) -> Path.of(s) - ); - - // note that each bundler is likely to replace this one with - // their own converter - static final BundlerParamInfo MAIN_JAR = - new BundlerParamInfo<>( - Arguments.CLIOptions.MAIN_JAR.getId(), - Path.class, - params -> LAUNCHER_DATA.fetchFrom(params).mainJarName(), - null - ); - - static final BundlerParamInfo PREDEFINED_APP_IMAGE = - new BundlerParamInfo<>( - Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId(), - Path.class, - params -> null, - (s, p) -> Path.of(s)); - - static final BundlerParamInfo PREDEFINED_APP_IMAGE_FILE = BundlerParamInfo.createBundlerParam( - ExternalApplication.class, params -> { - if (hasPredefinedAppImage(params)) { - var appImage = PREDEFINED_APP_IMAGE.fetchFrom(params); - return AppImageFile.load(appImage, PLATFORM_APPLICATION_LAYOUT); - } else { - return null; - } - }); - - static final BundlerParamInfo MAIN_CLASS = - new BundlerParamInfo<>( - Arguments.CLIOptions.APPCLASS.getId(), - String.class, - params -> { - if (isRuntimeInstaller(params)) { - return null; - } else if (hasPredefinedAppImage(params)) { - PREDEFINED_APP_IMAGE_FILE.fetchFrom(params).getMainClass(); - } - return LAUNCHER_DATA.fetchFrom(params).qualifiedClassName(); - }, - (s, p) -> s - ); - - static final BundlerParamInfo PREDEFINED_RUNTIME_IMAGE = - new BundlerParamInfo<>( - Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), - Path.class, - params -> null, - (s, p) -> Path.of(s) - ); - - // this is the raw --app-name arg - used in APP_NAME and INSTALLER_NAME - static final BundlerParamInfo NAME = - new BundlerParamInfo<>( - Arguments.CLIOptions.NAME.getId(), - String.class, - params -> null, - (s, p) -> s - ); - - // this is the application name, either from the app-image (if given), - // the name (if given) derived from the main-class, or the runtime image - static final BundlerParamInfo APP_NAME = - new BundlerParamInfo<>( - "application-name", - String.class, - params -> { - String appName = NAME.fetchFrom(params); - if (hasPredefinedAppImage(params)) { - appName = PREDEFINED_APP_IMAGE_FILE.fetchFrom(params).getLauncherName(); - } else if (appName == null) { - String s = MAIN_CLASS.fetchFrom(params); - if (s != null) { - int idx = s.lastIndexOf("."); - appName = (idx < 0) ? s : s.substring(idx+1); - } else if (isRuntimeInstaller(params)) { - Path f = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); - if (f != null) { - appName = f.getFileName().toString(); - } - } - } - return appName; - }, - (s, p) -> s - ); - - static final BundlerParamInfo INSTALLER_NAME = - new BundlerParamInfo<>( - "installer-name", - String.class, - params -> { - String installerName = NAME.fetchFrom(params); - return (installerName != null) ? installerName : - APP_NAME.fetchFrom(params); - }, - (s, p) -> s - ); - - static final BundlerParamInfo ICON = - new BundlerParamInfo<>( - Arguments.CLIOptions.ICON.getId(), - Path.class, - params -> null, - (s, p) -> Path.of(s) - ); - - static final BundlerParamInfo ABOUT_URL = - new BundlerParamInfo<>( - Arguments.CLIOptions.ABOUT_URL.getId(), - String.class, - params -> null, - (s, p) -> s - ); - - static final BundlerParamInfo VENDOR = - new BundlerParamInfo<>( - Arguments.CLIOptions.VENDOR.getId(), - String.class, - params -> I18N.getString("param.vendor.default"), - (s, p) -> s - ); - - static final BundlerParamInfo DESCRIPTION = - new BundlerParamInfo<>( - Arguments.CLIOptions.DESCRIPTION.getId(), - String.class, - params -> params.containsKey(APP_NAME.getID()) - ? APP_NAME.fetchFrom(params) - : I18N.getString("param.description.default"), - (s, p) -> s - ); - - static final BundlerParamInfo COPYRIGHT = - new BundlerParamInfo<>( - Arguments.CLIOptions.COPYRIGHT.getId(), - String.class, - params -> MessageFormat.format(I18N.getString( - "param.copyright.default"), new Date()), - (s, p) -> s - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> ARGUMENTS = - new BundlerParamInfo<>( - Arguments.CLIOptions.ARGUMENTS.getId(), - (Class>) (Object) List.class, - params -> Collections.emptyList(), - (s, p) -> null - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> JAVA_OPTIONS = - new BundlerParamInfo<>( - Arguments.CLIOptions.JAVA_OPTIONS.getId(), - (Class>) (Object) List.class, - params -> Collections.emptyList(), - (s, p) -> Arrays.asList(s.split("\n\n")) - ); - - static final BundlerParamInfo VERSION = - new BundlerParamInfo<>( - Arguments.CLIOptions.VERSION.getId(), - String.class, - StandardBundlerParam::getDefaultAppVersion, - (s, p) -> s - ); - - static final BundlerParamInfo RELEASE = - new BundlerParamInfo<>( - Arguments.CLIOptions.RELEASE.getId(), - String.class, - params -> DEFAULT_RELEASE, - (s, p) -> s - ); - - public static final BundlerParamInfo LICENSE_FILE = - new BundlerParamInfo<>( - Arguments.CLIOptions.LICENSE_FILE.getId(), - String.class, - params -> null, - (s, p) -> s - ); - - static final BundlerParamInfo TEMP_ROOT = - new BundlerParamInfo<>( - Arguments.CLIOptions.TEMP_ROOT.getId(), - Path.class, - params -> { - try { - return Files.createTempDirectory("jdk.jpackage"); - } catch (IOException ioe) { - return null; - } - }, - (s, p) -> Path.of(s) - ); - - public static final BundlerParamInfo CONFIG_ROOT = - new BundlerParamInfo<>( - "configRoot", - Path.class, - params -> { - Path root = TEMP_ROOT.fetchFrom(params).resolve("config"); - try { - Files.createDirectories(root); - } catch (IOException ioe) { - return null; - } - return root; - }, - (s, p) -> null - ); - - static final BundlerParamInfo VERBOSE = - new BundlerParamInfo<>( - Arguments.CLIOptions.VERBOSE.getId(), - Boolean.class, - params -> false, - // valueOf(null) is false, and we actually do want null - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? - true : Boolean.valueOf(s) - ); - - static final BundlerParamInfo RESOURCE_DIR = - new BundlerParamInfo<>( - Arguments.CLIOptions.RESOURCE_DIR.getId(), - Path.class, - params -> null, - (s, p) -> Path.of(s) - ); - - static final BundlerParamInfo INSTALL_DIR = - new BundlerParamInfo<>( - Arguments.CLIOptions.INSTALL_DIR.getId(), - String.class, - params -> null, - (s, p) -> s - ); - - static final BundlerParamInfo LAUNCHER_AS_SERVICE = - new BundlerParamInfo<>( - Arguments.CLIOptions.LAUNCHER_AS_SERVICE.getId(), - Boolean.class, - params -> false, - // valueOf(null) is false, and we actually do want null - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? - true : Boolean.valueOf(s) - ); - - - @SuppressWarnings("unchecked") - static final BundlerParamInfo>> ADD_LAUNCHERS = - new BundlerParamInfo<>( - Arguments.CLIOptions.ADD_LAUNCHER.getId(), - (Class>>) (Object) - List.class, - params -> new ArrayList<>(1), - // valueOf(null) is false, and we actually do want null - (s, p) -> null - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo - >> FILE_ASSOCIATIONS = - new BundlerParamInfo<>( - Arguments.CLIOptions.FILE_ASSOCIATIONS.getId(), - (Class>>) (Object) - List.class, - params -> new ArrayList<>(1), - // valueOf(null) is false, and we actually do want null - (s, p) -> null - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> FA_EXTENSIONS = - new BundlerParamInfo<>( - "fileAssociation.extension", - (Class>) (Object) List.class, - params -> null, // null means not matched to an extension - (s, p) -> Arrays.asList(s.split("(,|\\s)+")) - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> FA_CONTENT_TYPE = - new BundlerParamInfo<>( - "fileAssociation.contentType", - (Class>) (Object) List.class, - params -> null, - // null means not matched to a content/mime type - (s, p) -> Arrays.asList(s.split("(,|\\s)+")) - ); - - static final BundlerParamInfo FA_DESCRIPTION = - new BundlerParamInfo<>( - "fileAssociation.description", - String.class, - p -> null, - (s, p) -> s - ); - - static final BundlerParamInfo FA_ICON = - new BundlerParamInfo<>( - "fileAssociation.icon", - Path.class, - ICON::fetchFrom, - (s, p) -> Path.of(s) - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> DMG_CONTENT = - new BundlerParamInfo<>( - Arguments.CLIOptions.DMG_CONTENT.getId(), - (Class>) (Object)List.class, - p -> Collections.emptyList(), - (s, p) -> Stream.of(s.split(",")).map(Path::of).toList() - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> APP_CONTENT = - new BundlerParamInfo<>( - Arguments.CLIOptions.APP_CONTENT.getId(), - (Class>) (Object)List.class, - p->Collections.emptyList(), - (s, p) -> Stream.of(s.split(",")).map(Path::of).toList() - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> MODULE_PATH = - new BundlerParamInfo<>( - Arguments.CLIOptions.MODULE_PATH.getId(), - (Class>) (Object)List.class, - p -> JLinkRuntimeBuilder.ensureBaseModuleInModulePath(List.of()), - (s, p) -> { - List modulePath = Stream.of(s.split(File.pathSeparator)) - .map(Path::of) - .toList(); - return JLinkRuntimeBuilder.ensureBaseModuleInModulePath(modulePath); - }); - - static final BundlerParamInfo MODULE = - new BundlerParamInfo<>( - Arguments.CLIOptions.MODULE.getId(), - String.class, - p -> null, - (s, p) -> { - return String.valueOf(s); - }); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> ADD_MODULES = - new BundlerParamInfo<>( - Arguments.CLIOptions.ADD_MODULES.getId(), - (Class>) (Object) Set.class, - p -> new LinkedHashSet(), - (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) - ); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> JLINK_OPTIONS = - new BundlerParamInfo<>( - Arguments.CLIOptions.JLINK_OPTIONS.getId(), - (Class>) (Object) List.class, - p -> Arrays.asList(DEFAULT_JLINK_OPTIONS), - (s, p) -> null); - - @SuppressWarnings("unchecked") - static final BundlerParamInfo> LIMIT_MODULES = - new BundlerParamInfo<>( - "limit-modules", - (Class>) (Object) Set.class, - p -> new LinkedHashSet(), - (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) - ); - - static final BundlerParamInfo SIGN_BUNDLE = - new BundlerParamInfo<>( - Arguments.CLIOptions.MAC_SIGN.getId(), - Boolean.class, - params -> false, - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? - null : Boolean.valueOf(s) - ); - - static boolean isRuntimeInstaller(Map params) { - if (params.containsKey(MODULE.getID()) || - params.containsKey(MAIN_JAR.getID()) || - params.containsKey(PREDEFINED_APP_IMAGE.getID())) { - return false; // we are building or are given an application - } - // runtime installer requires --runtime-image, if this is false - // here then we should have thrown error validating args. - return params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID()); - } - - static boolean hasPredefinedAppImage(Map params) { - return params.containsKey(PREDEFINED_APP_IMAGE.getID()); - } - - private static String getDefaultAppVersion(Map params) { - String appVersion = DEFAULT_VERSION; - - if (isRuntimeInstaller(params)) { - return appVersion; - } - - LauncherData launcherData = null; - try { - launcherData = LAUNCHER_DATA.fetchFrom(params); - } catch (RuntimeException ex) { - if (ex.getCause() instanceof ConfigException) { - return appVersion; - } - throw ex; - } - - if (launcherData.isModular()) { - String moduleVersion = launcherData.getAppVersion(); - if (moduleVersion != null) { - Log.verbose(MessageFormat.format(I18N.getString( - "message.module-version"), - moduleVersion, - launcherData.moduleName())); - appVersion = moduleVersion; - } - } - - return appVersion; - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java similarity index 53% rename from src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java index 762b65b530e..50d1701bf0d 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 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 @@ -22,36 +22,51 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ - package jdk.jpackage.internal; +import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardOption; import jdk.jpackage.internal.util.FileUtils; +final class TempDirectory implements Closeable { -/** - * AbstractBundler - * - * This is the base class all bundlers extend from. - * It contains methods and parameters common to all bundlers. - * The concrete implementations are in the platform specific bundlers. - */ -abstract class AbstractBundler implements Bundler { + TempDirectory(Options options) throws IOException { + final var tempDir = StandardOption.TEMP_ROOT.findIn(options); + if (tempDir.isPresent()) { + this.path = tempDir.orElseThrow(); + this.options = options; + } else { + this.path = Files.createTempDirectory("jdk.jpackage"); + this.options = options.copyWithDefaultValue(StandardOption.TEMP_ROOT, path); + } - @Override - public String toString() { - return getName(); + deleteOnClose = tempDir.isEmpty(); + } + + Options options() { + return options; + } + + Path path() { + return path; + } + + boolean deleteOnClose() { + return deleteOnClose; } @Override - public void cleanup(Map params) { - try { - FileUtils.deleteRecursive( - StandardBundlerParam.TEMP_ROOT.fetchFrom(params)); - } catch (IOException e) { - Log.verbose(e.getMessage()); + public void close() throws IOException { + if (deleteOnClose) { + FileUtils.deleteRecursive(path); } } + + private final Path path; + private final Options options; + private final boolean deleteOnClose; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ValidOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ValidOptions.java deleted file mode 100644 index 89a2e4b56fe..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ValidOptions.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2018, 2023, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 java.util.EnumSet; -import java.util.HashMap; - -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.Arguments.CLIOptions; - -/** - * ValidOptions - * - * Two basic methods for validating command line options. - * - * initArgs() - * Computes the Map of valid options for each mode on this Platform. - * - * checkIfSupported(CLIOptions arg) - * Determine if the given arg is valid on this platform. - * - * checkIfImageSupported(CLIOptions arg) - * Determine if the given arg is valid for creating app image. - * - * checkIfInstallerSupported(CLIOptions arg) - * Determine if the given arg is valid for creating installer. - * - * checkIfSigningSupported(CLIOptions arg) - * Determine if the given arg is valid for signing app image. - * - */ -class ValidOptions { - - enum USE { - ALL, // valid in all cases - LAUNCHER, // valid when creating a launcher - INSTALL, // valid when creating an installer - SIGN, // valid when signing is requested - } - - private static final HashMap> options = new HashMap<>(); - - // initializing list of mandatory arguments - static { - put(CLIOptions.NAME.getId(), USE.ALL); - put(CLIOptions.VERSION.getId(), USE.ALL); - put(CLIOptions.OUTPUT.getId(), USE.ALL); - put(CLIOptions.TEMP_ROOT.getId(), USE.ALL); - put(CLIOptions.VERBOSE.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), USE.ALL); - put(CLIOptions.RESOURCE_DIR.getId(), USE.ALL); - put(CLIOptions.DESCRIPTION.getId(), USE.ALL); - put(CLIOptions.VENDOR.getId(), USE.ALL); - put(CLIOptions.COPYRIGHT.getId(), USE.ALL); - put(CLIOptions.PACKAGE_TYPE.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.ICON.getId(), USE.ALL); - - put(CLIOptions.INPUT.getId(), USE.LAUNCHER); - put(CLIOptions.MODULE.getId(), USE.LAUNCHER); - put(CLIOptions.MODULE_PATH.getId(), USE.LAUNCHER); - put(CLIOptions.ADD_MODULES.getId(), USE.LAUNCHER); - put(CLIOptions.MAIN_JAR.getId(), USE.LAUNCHER); - put(CLIOptions.APPCLASS.getId(), USE.LAUNCHER); - put(CLIOptions.ARGUMENTS.getId(), USE.LAUNCHER); - put(CLIOptions.JAVA_OPTIONS.getId(), USE.LAUNCHER); - put(CLIOptions.ADD_LAUNCHER.getId(), USE.LAUNCHER); - put(CLIOptions.JLINK_OPTIONS.getId(), USE.LAUNCHER); - put(CLIOptions.APP_CONTENT.getId(), USE.LAUNCHER); - - put(CLIOptions.LICENSE_FILE.getId(), USE.INSTALL); - put(CLIOptions.INSTALL_DIR.getId(), USE.INSTALL); - put(CLIOptions.PREDEFINED_APP_IMAGE.getId(), - (OperatingSystem.isMacOS()) ? - EnumSet.of(USE.INSTALL, USE.SIGN) : - EnumSet.of(USE.INSTALL)); - put(CLIOptions.LAUNCHER_AS_SERVICE.getId(), USE.INSTALL); - - put(CLIOptions.ABOUT_URL.getId(), USE.INSTALL); - - put(CLIOptions.FILE_ASSOCIATIONS.getId(), - (OperatingSystem.isMacOS()) ? USE.ALL : USE.INSTALL); - - if (OperatingSystem.isWindows()) { - put(CLIOptions.WIN_CONSOLE_HINT.getId(), USE.LAUNCHER); - - put(CLIOptions.WIN_HELP_URL.getId(), USE.INSTALL); - put(CLIOptions.WIN_UPDATE_URL.getId(), USE.INSTALL); - - put(CLIOptions.WIN_MENU_HINT.getId(), USE.INSTALL); - put(CLIOptions.WIN_MENU_GROUP.getId(), USE.INSTALL); - put(CLIOptions.WIN_SHORTCUT_HINT.getId(), USE.INSTALL); - put(CLIOptions.WIN_SHORTCUT_PROMPT.getId(), USE.INSTALL); - put(CLIOptions.WIN_DIR_CHOOSER.getId(), USE.INSTALL); - put(CLIOptions.WIN_UPGRADE_UUID.getId(), USE.INSTALL); - put(CLIOptions.WIN_PER_USER_INSTALLATION.getId(), - USE.INSTALL); - } - - if (OperatingSystem.isMacOS()) { - put(CLIOptions.MAC_SIGN.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.MAC_BUNDLE_NAME.getId(), USE.ALL); - put(CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), USE.ALL); - put(CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.MAC_SIGNING_KEY_NAME.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.MAC_APP_IMAGE_SIGN_IDENTITY.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.MAC_INSTALLER_SIGN_IDENTITY.getId(), - EnumSet.of(USE.INSTALL, USE.SIGN)); - put(CLIOptions.MAC_SIGNING_KEYCHAIN.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.MAC_APP_STORE.getId(), USE.ALL); - put(CLIOptions.MAC_CATEGORY.getId(), USE.ALL); - put(CLIOptions.MAC_ENTITLEMENTS.getId(), - EnumSet.of(USE.ALL, USE.SIGN)); - put(CLIOptions.DMG_CONTENT.getId(), USE.INSTALL); - } - - if (OperatingSystem.isLinux()) { - put(CLIOptions.LINUX_BUNDLE_NAME.getId(), USE.INSTALL); - put(CLIOptions.LINUX_DEB_MAINTAINER.getId(), USE.INSTALL); - put(CLIOptions.LINUX_CATEGORY.getId(), USE.INSTALL); - put(CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), USE.INSTALL); - put(CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(), - USE.INSTALL); - put(CLIOptions.LINUX_MENU_GROUP.getId(), USE.INSTALL); - put(CLIOptions.RELEASE.getId(), USE.INSTALL); - put(CLIOptions.LINUX_SHORTCUT_HINT.getId(), USE.INSTALL); - } - } - - static boolean checkIfSupported(CLIOptions arg) { - return options.containsKey(arg.getId()); - } - - static boolean checkIfImageSupported(CLIOptions arg) { - EnumSet value = options.get(arg.getId()); - return value.contains(USE.ALL) || - value.contains(USE.LAUNCHER) || - value.contains(USE.SIGN); - } - - static boolean checkIfInstallerSupported(CLIOptions arg) { - EnumSet value = options.get(arg.getId()); - return value.contains(USE.ALL) || value.contains(USE.INSTALL); - } - - static boolean checkIfSigningSupported(CLIOptions arg) { - EnumSet value = options.get(arg.getId()); - return value.contains(USE.SIGN); - } - - private static EnumSet put(String key, USE value) { - return options.put(key, EnumSet.of(value)); - } - - private static EnumSet put(String key, EnumSet value) { - return options.put(key, value); - } -} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBuildEnvFromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java similarity index 76% rename from src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBuildEnvFromParams.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java index 31759c8c529..34019ff9e81 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBuildEnvFromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java @@ -22,13 +22,11 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package jdk.jpackage.internal; -import jdk.jpackage.internal.model.MacPackage; +package jdk.jpackage.internal.cli; -final class MacBuildEnvFromParams { +import java.nio.file.Path; - static final BundlerParamInfo BUILD_ENV = BundlerParamInfo.createBundlerParam(BuildEnv.class, params -> { - return BuildEnvFromParams.create(params, MacPackagingPipeline.APPLICATION_LAYOUT::resolveAt, MacPackage::guessRuntimeLayout); - }); + +record AdditionalLauncher(String name, Path propertyFile) { } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java new file mode 100644 index 00000000000..33525f3e54d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + +/** + * Modifiers for jpackage operations. + */ +enum BundlingOperationModifier implements OptionScope { + /** + * Create runtime native bundle. + */ + BUNDLE_RUNTIME, + + /** + * Create native bundle from the predefined app image. + */ + BUNDLE_PREDEFINED_APP_IMAGE, + + ; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java new file mode 100644 index 00000000000..04af5865baa --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + + +import jdk.jpackage.internal.model.BundlingOperationDescriptor; + +/** + * Bundling operation scope. + *

+ * The scope of bundling operations. E.g., app image or native package bundling. + */ +interface BundlingOperationOptionScope extends OptionScope { + BundlingOperationDescriptor descriptor(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java new file mode 100644 index 00000000000..09ff0997c14 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + +import java.util.NoSuchElementException; +import jdk.jpackage.internal.model.BundlingEnvironment; +import jdk.jpackage.internal.model.BundlingOperationDescriptor; + +/** + * CLI bundling environment. + */ +public interface CliBundlingEnvironment extends BundlingEnvironment { + + /** + * Requests to run a bundling operation denoted with the given descriptor with + * the given values of command line options. + * + * @param op the descriptor of the requested bundling operation + * @param cmdline the validated values of the command line options + * @throws NoSuchElementException if the specified descriptor is not one of the + * items in the list returned by + * {@link #supportedOperations()} method + */ + void createBundle(BundlingOperationDescriptor op, Options cmdline); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java new file mode 100644 index 00000000000..91342500277 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + +import static java.util.stream.Collectors.toUnmodifiableMap; +import static java.util.stream.Collectors.toUnmodifiableSet; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + + +final class DefaultOptions implements Options { + + DefaultOptions(Map values) { + this(values, Optional.empty()); + } + + DefaultOptions( + Map values, + Predicate optionNamesFilter) { + + this(values, Optional.of(optionNamesFilter)); + } + + DefaultOptions( + Map values, + Optional> optionNamesFilter) { + + map = values.entrySet().stream().collect(toUnmodifiableMap(e -> { + return e.getKey().id(); + }, e -> { + return new OptionIdentifierWithValue(e.getKey(), e.getValue()); + })); + + var optionNamesStream = optionNames(values.keySet().stream()); + optionNames = optionNamesFilter.map(optionNamesStream::filter).orElse(optionNamesStream) + .collect(toUnmodifiableSet()); + } + + private DefaultOptions(Snapshot snapshot) { + map = snapshot.map(); + optionNames = snapshot.optionNames(); + } + + static DefaultOptions create(Snapshot snapshot) { + var options = new DefaultOptions(snapshot); + + var mapOptionNames = optionNames( + options.map.values().stream().map(OptionIdentifierWithValue::withId) + ).collect(toUnmodifiableSet()); + + for (var e : options.map.entrySet()) { + if (e.getKey() != e.getValue().withId().id()) { + throw new IllegalArgumentException("Corrupted options map"); + } + } + + if (!mapOptionNames.containsAll(snapshot.optionNames())) { + throw new IllegalArgumentException("Unexpected option names"); + } + return options; + } + + @Override + public Optional find(OptionIdentifier id) { + return Optional.ofNullable(map.get(Objects.requireNonNull(id))).map(OptionIdentifierWithValue::value); + } + + @Override + public boolean contains(OptionName optionName) { + return optionNames.contains(Objects.requireNonNull(optionName)); + } + + @Override + public Set ids() { + return Collections.unmodifiableSet(map.keySet()); + } + + @Override + public DefaultOptions copyWithout(Iterable ids) { + return copy(StreamSupport.stream(ids.spliterator(), false), false); + } + + @Override + public DefaultOptions copyWith(Iterable ids) { + return copy(StreamSupport.stream(ids.spliterator(), false), true); + } + + DefaultOptions add(DefaultOptions other) { + return new DefaultOptions(new Snapshot(Stream.of(this, other).flatMap(v -> { + return v.map.values().stream(); + }).collect(toUnmodifiableMap(OptionIdentifierWithValue::id, x -> x, (first, _) -> { + return first; + })), Stream.of(this, other) + .map(DefaultOptions::optionNames) + .flatMap(Collection::stream) + .collect(toUnmodifiableSet()))); + } + + Set optionNames() { + return optionNames; + } + + Set withOptionIdentifierSet() { + return map.values().stream() + .map(OptionIdentifierWithValue::withId) + .collect(toUnmodifiableSet()); + } + + record Snapshot(Map map, Set optionNames) { + Snapshot { + Objects.requireNonNull(map); + Objects.requireNonNull(optionNames); + } + } + + record OptionIdentifierWithValue(WithOptionIdentifier withId, Object value) { + OptionIdentifierWithValue { + Objects.requireNonNull(withId); + Objects.requireNonNull(value); + } + + OptionIdentifier id() { + return withId.id(); + } + + OptionIdentifierWithValue copyWithValue(Object value) { + return new OptionIdentifierWithValue(withId, value); + } + } + + private DefaultOptions copy(Stream ids, boolean includes) { + var includeIds = ids.collect(toUnmodifiableSet()); + return new DefaultOptions(map.values().stream().filter(v -> { + return includeIds.contains(v.id()) == includes; + }).collect(toUnmodifiableMap(OptionIdentifierWithValue::withId, OptionIdentifierWithValue::value))); + } + + private static Stream optionNames(Stream options) { + return options.map(v -> { + Optional> spec; + switch (v) { + case Option option -> { + spec = Optional.of(option.spec()); + } + case OptionValue optionValue -> { + spec = optionValue.asOption().map(Option::spec); + } + default -> { + spec = Optional.empty(); + } + } + return spec; + }).filter(Optional::isPresent).map(Optional::get).map(OptionSpec::names).flatMap(Collection::stream); + } + + static final DefaultOptions EMPTY = new DefaultOptions(Map.of()); + + private final Map map; + private final Set optionNames; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java new file mode 100644 index 00000000000..1d8a5eefc79 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + +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 java.util.stream.Stream; + + +/** + * Generic help formatter. + */ +final class HelpFormatter { + + private HelpFormatter(List optionGroups, OptionGroupFormatter formatter) { + this.optionGroups = Objects.requireNonNull(optionGroups); + this.formatter = Objects.requireNonNull(formatter); + } + + void format(Consumer sink) { + for (var group : optionGroups) { + formatter.format(group, sink); + } + } + + static Builder build() { + return new Builder(); + } + + + static final class Builder { + + private Builder() { + } + + HelpFormatter create() { + return new HelpFormatter(groups, validatedGroupFormatter()); + } + + Builder groups(Collection v) { + groups.addAll(v); + return this; + } + + Builder groups(OptionGroup... v) { + return groups(List.of(v)); + } + + Builder groupFormatter(OptionGroupFormatter v) { + groupFormatter = v; + return this; + } + + private OptionGroupFormatter validatedGroupFormatter() { + return Optional.ofNullable(groupFormatter).orElseGet(Builder::createConsoleFormatter); + } + + private static OptionGroupFormatter createConsoleFormatter() { + return new ConsoleOptionGroupFormatter(new ConsoleOptionFormatter(2, 10)); + } + + private final List groups = new ArrayList<>(); + private OptionGroupFormatter groupFormatter; + } + + + interface OptionFormatter { + + public default void format(OptionSpec optionSpec, Consumer sink) { + format(optionSpec.names().stream().map(OptionName::formatForCommandLine).collect(Collectors.joining(" ")), + optionSpec.valuePattern(), + optionSpec.description(), sink); + } + + void format(String optionNames, Optional valuePattern, String description, Consumer sink); + } + + interface OptionGroupFormatter { + + default void format(OptionGroup group, Consumer sink) { + formatHeader(group.name(), sink); + formatBody(group.options(), sink); + } + + void formatHeader(String gropName, Consumer sink); + + void formatBody(Iterable> optionSpecs, Consumer sink); + } + + + record ConsoleOptionFormatter(int nameOffset, int descriptionOffset) implements OptionFormatter { + + @Override + public void format(String optionNames, Optional valuePattern, String description, Consumer sink) { + sink.accept(" ".repeat(nameOffset)); + sink.accept(optionNames); + valuePattern.map(v -> " " + v).ifPresent(sink); + eol(sink); + final var descriptionOffsetStr = " ".repeat(descriptionOffset); + Stream.of(description.split("\\R")).map(line -> { + return descriptionOffsetStr + line; + }).forEach(line -> { + sink.accept(line); + eol(sink); + }); + } + } + + + record ConsoleOptionGroupFormatter(OptionFormatter optionFormatter) implements OptionGroupFormatter { + + ConsoleOptionGroupFormatter { + Objects.requireNonNull(optionFormatter); + } + + @Override + public void formatHeader(String groupName, Consumer sink) { + Objects.requireNonNull(groupName); + eol(sink); + sink.accept(groupName + ":"); + eol(sink); + } + + @Override + public void formatBody(Iterable> optionSpecs, Consumer sink) { + optionSpecs.forEach(optionSpec -> { + optionFormatter.format(optionSpec, sink); + }); + } + } + + + record OptionGroup(String name, List> options) { + + OptionGroup { + Objects.requireNonNull(name); + Objects.requireNonNull(options); + } + } + + + static Consumer eol(Consumer sink) { + sink.accept(System.lineSeparator()); + return sink; + } + + + private final List optionGroups; + private final OptionGroupFormatter formatter; +} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java similarity index 56% rename from src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java index 5c912728c32..63c2b88039f 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -22,31 +22,34 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package jdk.jpackage.internal.cli; -package jdk.jpackage.internal; - -import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; - +import java.util.List; import java.util.Map; -import jdk.jpackage.internal.model.ConfigException; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.util.MultiResourceBundle; +import jdk.jpackage.internal.util.StringBundle; -public abstract class MacBaseInstallerBundler extends AbstractBundler { +final class I18N { - public MacBaseInstallerBundler() { - appImageBundler = new MacAppBundler(); + private I18N() { } - protected void validateAppImageAndBundeler( - Map params) throws ConfigException { - if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) { - appImageBundler.validate(params); - } + static String format(String key, Object ... args) { + return BUNDLE.format(key, args); } - @Override - public String getBundleType() { - return "INSTALLER"; - } + private static final StringBundle BUNDLE; - private final Bundler appImageBundler; + static { + var prefix = "jdk.jpackage.internal.resources."; + BUNDLE = StringBundle.fromResourceBundle(MultiResourceBundle.create( + prefix + "MainResources", + Map.of( + OperatingSystem.LINUX, List.of(prefix + "LinuxResources"), + OperatingSystem.MACOS, List.of(prefix + "MacResources"), + OperatingSystem.WINDOWS, List.of(prefix + "WinResources") + ) + )); + } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java new file mode 100644 index 00000000000..57b92471e4a --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java @@ -0,0 +1,817 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.cli; + +import static java.util.stream.Collectors.toUnmodifiableMap; +import static java.util.stream.Collectors.toUnmodifiableSet; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import jdk.internal.joptsimple.ArgumentAcceptingOptionSpec; +import jdk.internal.joptsimple.OptionParser; +import jdk.internal.joptsimple.OptionSet; +import jdk.jpackage.internal.cli.DefaultOptions.OptionIdentifierWithValue; +import jdk.jpackage.internal.cli.DefaultOptions.Snapshot; +import jdk.jpackage.internal.cli.OptionSpec.MergePolicy; +import jdk.jpackage.internal.util.Result; + + +/** + * Builds an instance of {@link Options} interface backed with joptsimple command + * line parser. + * + * Two types of command line argument processing are supported: + *
    + *
  1. Parse command line. Parsed data is stored as a map of strings. + *
  2. Convert strings to objects. Parsed data is stored as a map of objects. + *
+ */ +final class JOptSimpleOptionsBuilder { + + Function> create() { + return createJOptSimpleParser()::parse; + } + + JOptSimpleOptionsBuilder options(Collection v) { + v.stream().map(u -> { + switch (u) { + case Option o -> { + return o; + } + case OptionValue ov -> { + return ov.getOption(); + } + default -> { + throw new IllegalArgumentException(); + } + } + }).forEach(options::add); + return this; + } + + JOptSimpleOptionsBuilder options(WithOptionIdentifier... v) { + return options(List.of(v)); + } + + JOptSimpleOptionsBuilder optionSpecMapper(UnaryOperator> v) { + optionSpecMapper = v; + return this; + } + + JOptSimpleOptionsBuilder jOptSimpleParserErrorHandler(Function v) { + jOptSimpleParserErrorHandler = v; + return this; + } + + private JOptSimpleParser createJOptSimpleParser() { + return JOptSimpleParser.create(options, Optional.ofNullable(optionSpecMapper), + Optional.ofNullable(jOptSimpleParserErrorHandler)); + } + + + static final class ConvertedOptionsBuilder { + + private ConvertedOptionsBuilder(TypedOptions options) { + impl = Objects.requireNonNull(options); + } + + Options create() { + return impl; + } + + ConvertedOptionsBuilder copyWithExcludes(Collection v) { + return new ConvertedOptionsBuilder(impl.copyWithout(v)); + } + + List nonOptionArguments() { + return impl.nonOptionArguments(); + } + + List detectedOptions() { + return impl.detectedOptions(); + } + + private final TypedOptions impl; + } + + + static final class OptionsBuilder { + + private OptionsBuilder(UntypedOptions options) { + impl = Objects.requireNonNull(options); + } + + Result convertedOptions() { + return impl.toTypedOptions().map(ConvertedOptionsBuilder::new); + } + + Options create() { + return impl; + } + + OptionsBuilder copyWithExcludes(Collection v) { + return new OptionsBuilder(impl.copyWithout(v)); + } + + List nonOptionArguments() { + return impl.nonOptionArguments(); + } + + List detectedOptions() { + return impl.detectedOptions(); + } + + private final UntypedOptions impl; + } + + + enum JOptSimpleErrorType { + + // jdk.internal.joptsimple.UnrecognizedOptionException + UNRECOGNIZED_OPTION(() -> { + new OptionParser(false).parse("--foo"); + }), + + // jdk.internal.joptsimple.OptionMissingRequiredArgumentException + OPTION_MISSING_REQUIRED_ARGUMENT(() -> { + var parser = new OptionParser(false); + parser.accepts("foo").withRequiredArg(); + parser.parse("--foo"); + }), + ; + + JOptSimpleErrorType(Runnable initializer) { + try { + initializer.run(); + // Should never get to this point as the above line is expected to throw + // an exception of type `jdk.internal.joptsimple.OptionException`. + throw new AssertionError(); + } catch (jdk.internal.joptsimple.OptionException ex) { + type = ex.getClass(); + } + } + + private final Class type; + } + + + record JOptSimpleError(JOptSimpleErrorType type, OptionName optionName) { + + JOptSimpleError { + Objects.requireNonNull(type); + Objects.requireNonNull(optionName); + } + + static JOptSimpleError create(jdk.internal.joptsimple.OptionException ex) { + var optionName = OptionName.of(ex.options().getFirst()); + return Stream.of(JOptSimpleErrorType.values()).filter(v -> { + return v.type.isInstance(ex); + }).findFirst().map(v -> { + return new JOptSimpleError(v, optionName); + }).orElseThrow(); + } + } + + + private record JOptSimpleParser( + OptionParser parser, + Map> optionMap, + Optional> jOptSimpleParserErrorHandler) { + + private JOptSimpleParser { + Objects.requireNonNull(parser); + Objects.requireNonNull(optionMap); + Objects.requireNonNull(jOptSimpleParserErrorHandler); + } + + Result parse(String... args) { + return applyParser(parser, args).map(optionSet -> { + final OptionSet mergerOptionSet; + if (optionMap.values().stream().allMatch(spec -> spec.names().size() == 1)) { + // No specs with multiple names, merger not needed. + mergerOptionSet = optionSet; + } else { + final var parser2 = createOptionParser(); + final var optionSpecApplier = new OptionSpecApplier(); + for (final var spec : optionMap.values()) { + optionSpecApplier.applyToParser(parser2, spec); + } + + mergerOptionSet = parser2.parse(args); + } + return new OptionsBuilder(new UntypedOptions(optionSet, mergerOptionSet, optionMap)); + }); + } + + static JOptSimpleParser create(Iterable