blob: a00eb16a4a9af78e87cb0cb70abce69b5e953b49 [file]
/*
* Copyright (c) 2021, 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.WinMsiPackage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.WixAppImageFragmentBuilder.ShortcutsFolder;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import jdk.jpackage.internal.resources.ResourceLocator;
import jdk.jpackage.internal.util.XmlConsumer;
/**
* Creates UI WiX fragment.
*/
final class WixUiFragmentBuilder extends WixFragmentBuilder {
@Override
void initFromParams(BuildEnv env, WinMsiPackage pkg) {
super.initFromParams(env, pkg);
withLicenseDlg = pkg.licenseFile().isPresent();
if (withLicenseDlg) {
Path licenseFileName = pkg.licenseFile().orElseThrow().getFileName();
Path destFile = getConfigRoot().resolve(licenseFileName);
setWixVariable("JpLicenseRtf", destFile.toAbsolutePath().toString());
}
withInstallDirChooserDlg = pkg.withInstallDirChooser();
final var shortcutFolders = ShortcutsFolder.getForPackage(pkg);
withShortcutPromptDlg = !shortcutFolders.isEmpty() && pkg.withShortcutPrompt();
customDialogs = new ArrayList<>();
if (withShortcutPromptDlg) {
CustomDialog dialog = new CustomDialog(env::createResource, I18N.getString(
"resource.shortcutpromptdlg-wix-file"),
"ShortcutPromptDlg.wxs");
for (var shortcutFolder : shortcutFolders) {
dialog.wixVariables.defineWixVariable(
shortcutFolder.getWixVariableName());
}
customDialogs.add(dialog);
}
if (withInstallDirChooserDlg) {
CustomDialog dialog = new CustomDialog(env::createResource, I18N.getString(
"resource.installdirnotemptydlg-wix-file"),
"InstallDirNotEmptyDlg.wxs");
List<Dialog> dialogIds = getUI().dialogIdsSupplier.apply(this);
dialog.wixVariables.setWixVariable("JpAfterInstallDirDlg",
dialogIds.get(dialogIds.indexOf(Dialog.InstallDirDlg) + 1).id);
customDialogs.add(dialog);
}
}
@Override
void configureWixPipeline(WixPipeline.Builder wixPipeline) {
super.configureWixPipeline(wixPipeline);
if (withShortcutPromptDlg || withInstallDirChooserDlg || withLicenseDlg) {
final String extName;
switch (getWixType()) {
case Wix3 -> extName = "WixUIExtension";
case Wix4 -> extName = "WixToolset.UI.wixext";
default -> throw new IllegalArgumentException();
}
wixPipeline.addLightOptions("-ext", extName);
}
// Only needed if we using CA dll, so Wix can find it
if (withCustomActionsDll) {
wixPipeline.addLightOptions("-b",
getConfigRoot().toAbsolutePath().toString());
}
for (var customDialog : customDialogs) {
customDialog.addToWixPipeline(wixPipeline);
}
}
@Override
void addFilesToConfigRoot() throws IOException {
super.addFilesToConfigRoot();
if (withCustomActionsDll) {
String fname = "wixhelper.dll"; // CA dll
try (InputStream is = ResourceLocator.class.getResourceAsStream(fname)) {
Files.copy(is, getConfigRoot().resolve(fname));
}
}
}
@Override
protected Collection<XmlConsumer> getFragmentWriters() {
return List.of(this::addUI);
}
private void addUI(XMLStreamWriter xml) throws XMLStreamException,
IOException {
if (withInstallDirChooserDlg) {
xml.writeStartElement("Property");
xml.writeAttribute("Id", "WIXUI_INSTALLDIR");
xml.writeAttribute("Value", "INSTALLDIR");
xml.writeEndElement(); // Property
}
if (withLicenseDlg) {
xml.writeStartElement("WixVariable");
xml.writeAttribute("Id", "WixUILicenseRtf");
xml.writeAttribute("Value", "$(var.JpLicenseRtf)");
xml.writeEndElement(); // WixVariable
}
var ui = getUI();
if (ui != null) {
ui.write(getWixType(), this, xml);
} else {
xml.writeStartElement("UI");
xml.writeAttribute("Id", "JpUI");
xml.writeEndElement();
}
}
private UI getUI() {
if (withInstallDirChooserDlg || withShortcutPromptDlg) {
// WixUI_InstallDir for shortcut prompt dialog too because in
// WixUI_Minimal UI sequence WelcomeEulaDlg dialog doesn't have "Next"
// button, but has "Install" button. So inserting shortcut prompt dialog
// after welcome dialog in WixUI_Minimal UI sequence would be confusing
return UI.InstallDir;
} else if (withLicenseDlg) {
return UI.Minimal;
} else {
return null;
}
}
private enum UI {
InstallDir("WixUI_InstallDir",
WixUiFragmentBuilder::dialogSequenceForWixUI_InstallDir,
Dialog::createPairsForWixUI_InstallDir),
Minimal("WixUI_Minimal", null, null);
UI(String wixUIRef,
Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier,
Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier) {
this.wixUIRef = wixUIRef;
this.dialogIdsSupplier = dialogIdsSupplier;
this.dialogPairsSupplier = dialogPairsSupplier;
}
void write(WixToolsetType wixType, WixUiFragmentBuilder outer, XMLStreamWriter xml) throws XMLStreamException, IOException {
switch (wixType) {
case Wix3 -> {}
case Wix4 -> {
// https://wixtoolset.org/docs/fourthree/faqs/#converting-custom-wixui-dialog-sets
xml.writeProcessingInstruction("foreach WIXUIARCH in X86;X64;A64");
writeWix4UIRef(xml, wixUIRef, "JpUIInternal_$(WIXUIARCH)");
xml.writeProcessingInstruction("endforeach");
writeWix4UIRef(xml, "JpUIInternal", "JpUI");
}
default -> {
throw new IllegalArgumentException();
}
}
xml.writeStartElement("UI");
switch (wixType) {
case Wix3 -> {
xml.writeAttribute("Id", "JpUI");
xml.writeStartElement("UIRef");
xml.writeAttribute("Id", wixUIRef);
xml.writeEndElement(); // UIRef
}
case Wix4 -> {
xml.writeAttribute("Id", "JpUIInternal");
}
default -> {
throw new IllegalArgumentException();
}
}
writeContents(wixType, outer, xml);
xml.writeEndElement(); // UI
}
private void writeContents(WixToolsetType wixType, WixUiFragmentBuilder outer,
XMLStreamWriter xml) throws XMLStreamException, IOException {
if (dialogIdsSupplier != null) {
List<Dialog> dialogIds = dialogIdsSupplier.apply(outer);
Map<DialogPair, List<Publish>> dialogPairs = dialogPairsSupplier.get();
if (dialogIds.contains(Dialog.InstallDirDlg)) {
xml.writeStartElement("DialogRef");
xml.writeAttribute("Id", "InstallDirNotEmptyDlg");
xml.writeEndElement(); // DialogRef
}
var it = dialogIds.iterator();
Dialog firstId = it.next();
while (it.hasNext()) {
Dialog secondId = it.next();
DialogPair pair = new DialogPair(firstId, secondId);
for (var curPair : List.of(pair, pair.flip())) {
for (var publish : dialogPairs.get(curPair)) {
writePublishDialogPair(wixType, xml, publish, curPair);
}
}
firstId = secondId;
}
}
}
private static void writeWix4UIRef(XMLStreamWriter xml, String uiRef, String id) throws XMLStreamException, IOException {
// https://wixtoolset.org/docs/fourthree/faqs/#referencing-the-standard-wixui-dialog-sets
xml.writeStartElement("UI");
xml.writeAttribute("Id", id);
xml.writeStartElement("ui:WixUI");
xml.writeAttribute("Id", uiRef);
xml.writeNamespace("ui", "http://wixtoolset.org/schemas/v4/wxs/ui");
xml.writeEndElement(); // UIRef
xml.writeEndElement(); // UI
}
private final String wixUIRef;
private final Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier;
private final Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier;
}
private List<Dialog> dialogSequenceForWixUI_InstallDir() {
List<Dialog> dialogIds = new ArrayList<>(
List.of(Dialog.WixUI_WelcomeDlg));
if (withLicenseDlg) {
dialogIds.add(Dialog.WixUI_LicenseAgreementDlg);
}
if (withInstallDirChooserDlg) {
dialogIds.add(Dialog.InstallDirDlg);
}
if (withShortcutPromptDlg) {
dialogIds.add(Dialog.ShortcutPromptDlg);
}
dialogIds.add(Dialog.WixUI_VerifyReadyDlg);
return dialogIds;
}
private enum Dialog {
WixUI_WelcomeDlg,
WixUI_LicenseAgreementDlg,
InstallDirDlg,
ShortcutPromptDlg,
WixUI_VerifyReadyDlg;
Dialog() {
if (name().startsWith("WixUI_")) {
id = name().substring("WixUI_".length());
} else {
id = name();
}
}
static Map<DialogPair, List<Publish>> createPair(Dialog firstId,
Dialog secondId, List<PublishBuilder> nextBuilders,
List<PublishBuilder> prevBuilders) {
var pair = new DialogPair(firstId, secondId);
return Map.of(pair, nextBuilders.stream().map(b -> {
return buildPublish(b.create()).next().create();
}).toList(), pair.flip(),
prevBuilders.stream().map(b -> {
return buildPublish(b.create()).back().create();
}).toList());
}
static Map<DialogPair, List<Publish>> createPair(Dialog firstId,
Dialog secondId, List<PublishBuilder> builders) {
return createPair(firstId, secondId, builders, builders);
}
static Map<DialogPair, List<Publish>> createPairsForWixUI_InstallDir() {
Map<DialogPair, List<Publish>> map = new HashMap<>();
// Order is a "weight" of action. If there are multiple
// "NewDialog" action for the same dialog Id, MSI would pick the one
// with higher order value. In WixUI_InstallDir dialog sequence the
// highest order value is 4. InstallDirNotEmptyDlg adds NewDialog
// action with order 5. Setting order to 6 for all
// actions configured in this function would make them executed
// instead of corresponding default actions defined in
// WixUI_InstallDir dialog sequence.
var order = 6;
// Based on WixUI_InstallDir.wxs
var backFromVerifyReadyDlg = List.of(buildPublish().condition(
"NOT Installed").order(order));
var uncondinal = List.of(buildPublish().condition("1"));
var ifNotIstalled = List.of(
buildPublish().condition("NOT Installed").order(order));
var ifLicenseAccepted = List.of(buildPublish().condition(
"LicenseAccepted = \"1\"").order(order));
// Empty condition list for the default dialog sequence
map.putAll(createPair(WixUI_WelcomeDlg, WixUI_LicenseAgreementDlg,
List.of()));
map.putAll(
createPair(WixUI_WelcomeDlg, InstallDirDlg, ifNotIstalled));
map.putAll(createPair(WixUI_WelcomeDlg, ShortcutPromptDlg,
ifNotIstalled));
map.putAll(createPair(WixUI_LicenseAgreementDlg, InstallDirDlg,
List.of()));
map.putAll(createPair(WixUI_LicenseAgreementDlg, ShortcutPromptDlg,
ifLicenseAccepted, uncondinal));
map.putAll(createPair(WixUI_LicenseAgreementDlg,
WixUI_VerifyReadyDlg, ifLicenseAccepted,
backFromVerifyReadyDlg));
map.putAll(createPair(InstallDirDlg, ShortcutPromptDlg, List.of(),
uncondinal));
map.putAll(createPair(InstallDirDlg, WixUI_VerifyReadyDlg, List.of()));
map.putAll(createPair(ShortcutPromptDlg, WixUI_VerifyReadyDlg,
uncondinal, backFromVerifyReadyDlg));
return map;
}
private final String id;
}
private static final class DialogPair {
DialogPair(Dialog first, Dialog second) {
this(first.id, second.id);
}
DialogPair(String firstId, String secondId) {
this.firstId = firstId;
this.secondId = secondId;
}
DialogPair flip() {
return new DialogPair(secondId, firstId);
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + Objects.hashCode(this.firstId);
hash = 97 * hash + Objects.hashCode(this.secondId);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DialogPair other = (DialogPair) obj;
if (!Objects.equals(this.firstId, other.firstId)) {
return false;
}
if (!Objects.equals(this.secondId, other.secondId)) {
return false;
}
return true;
}
private final String firstId;
private final String secondId;
}
private static final class Publish {
Publish(String control, String condition, int order) {
this.control = control;
this.condition = condition;
this.order = order;
}
private final String control;
private final String condition;
private final int order;
}
private static final class PublishBuilder {
PublishBuilder() {
order(0);
next();
condition("1");
}
PublishBuilder(Publish publish) {
order(publish.order);
control(publish.control);
condition(publish.condition);
}
public PublishBuilder control(String v) {
control = v;
return this;
}
public PublishBuilder next() {
return control("Next");
}
public PublishBuilder back() {
return control("Back");
}
public PublishBuilder condition(String v) {
condition = v;
return this;
}
public PublishBuilder order(int v) {
order = v;
return this;
}
Publish create() {
return new Publish(control, condition, order);
}
private String control;
private String condition;
private int order;
}
private static PublishBuilder buildPublish() {
return new PublishBuilder();
}
private static PublishBuilder buildPublish(Publish publish) {
return new PublishBuilder(publish);
}
private static void writePublishDialogPair(WixToolsetType wixType, XMLStreamWriter xml,
Publish publish, DialogPair dialogPair) throws IOException, XMLStreamException {
xml.writeStartElement("Publish");
xml.writeAttribute("Dialog", dialogPair.firstId);
xml.writeAttribute("Control", publish.control);
xml.writeAttribute("Event", "NewDialog");
xml.writeAttribute("Value", dialogPair.secondId);
if (publish.order != 0) {
xml.writeAttribute("Order", String.valueOf(publish.order));
}
switch (wixType) {
case Wix3 -> xml.writeCharacters(publish.condition);
case Wix4 -> xml.writeAttribute("Condition", publish.condition);
default -> throw new IllegalArgumentException();
}
xml.writeEndElement();
}
private final class CustomDialog {
CustomDialog(Function<String, OverridableResource> createResource, String category,
String wxsFileName) {
this.wxsFileName = wxsFileName;
this.wixVariables = new WixVariables();
addResource(createResource.apply(wxsFileName).setCategory(category).setPublicName(
wxsFileName), wxsFileName);
}
void addToWixPipeline(WixPipeline.Builder wixPipeline) {
wixPipeline.addSource(getConfigRoot().toAbsolutePath().resolve(
wxsFileName), wixVariables.getValues());
}
private final WixVariables wixVariables;
private final String wxsFileName;
}
private boolean withInstallDirChooserDlg;
private boolean withShortcutPromptDlg;
private boolean withLicenseDlg;
private boolean withCustomActionsDll = true;
private List<CustomDialog> customDialogs;
}