org.eclipse.scada.configuration.world.lib.deployment.wix.WixDeploymentSetupBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.scada.configuration.world.lib.deployment.wix.WixDeploymentSetupBuilder.java

Source

/*******************************************************************************
 * Copyright (c) 2014, 2015 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *     Gabriel Nicolas Avellaneda - add validation for MSI version
 *******************************************************************************/
package org.eclipse.scada.configuration.world.lib.deployment.wix;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import org.eclipse.scada.configuration.generator.Profiles;
import org.eclipse.scada.configuration.world.lib.deployment.MsiHandler;
import org.eclipse.scada.configuration.world.osgi.profile.Profile;
import org.eclipse.scada.configuration.world.osgi.profile.SystemProperty;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.google.common.io.Resources;

public class WixDeploymentSetupBuilder extends XMLBase {

    public static class CommonDriverService {
        private final String name;

        private final File configuration;

        private final Properties props;

        public CommonDriverService(final String name, final File configuration, final Properties props) {
            this.name = name;
            this.configuration = configuration;
            this.props = props;
        }

        public File getConfiguration() {
            return this.configuration;
        }

        public String getName() {
            return this.name;
        }

        public Properties getProps() {
            return this.props;
        }
    }

    public static class EquinoxAppService {
        private final String name;

        private final Profile profile;

        private final File configurationBase;

        public EquinoxAppService(final String name, final Profile profile, final File configurationBase) {
            this.name = name;
            this.profile = profile;
            this.configurationBase = configurationBase;
        }

        public String getName() {
            return this.name;
        }

        public Profile getProfile() {
            return this.profile;
        }

        public File getConfigurationBase() {
            return this.configurationBase;
        }
    }

    private MsiPlatform platform = MsiPlatform.WIN32;

    private String version;

    private String manufacturer;

    private String upgradeCode;

    private String name;

    private final List<CommonDriverService> commonDrivers = new LinkedList<>();

    private final List<EquinoxAppService> apps = new LinkedList<>();

    private final Set<String> components = new HashSet<>();

    private Element caDir;

    private boolean useFirewall;

    private boolean useUserInterface;

    private File licenseFile;

    public void setUseFirewall(final boolean useFirewall) {
        this.useFirewall = useFirewall;
    }

    public void setUseUserInterface(final boolean useUserInterface) {
        this.useUserInterface = useUserInterface;
    }

    public void write(final File base) throws Exception {
        // set up document

        final Document doc = createXmlDocument();

        // create content

        createDocument(doc, base);

        // write XML

        storeXmlDocument(doc, new File(base, "Setup.wxs")); //$NON-NLS-1$
    }

    protected void createDocument(final Document doc, final File base) throws Exception {
        final Element ele = doc.createElementNS("http://schemas.microsoft.com/wix/2006/wi", "Wix"); //$NON-NLS-1$ //$NON-NLS-2$
        doc.appendChild(ele);

        createProduct(doc, ele, base);
    }

    protected static void validateProductVersion(final String version) throws IllegalStateException {
        if (!Pattern.matches("\\d+.\\d+(.\\d+)?(.\\d+)?", version)) //$NON-NLS-1$
        {
            throw new IllegalStateException(String.format(
                    "Specified Version attribute's value (%s) is not a valid version. Legal version values should look like 'x.x.x.x' where x is an integer from 0 to 65534.", //$NON-NLS-1$
                    version));
        }
    }

    private void createProduct(final Document doc, final Element root, final File base) throws Exception {
        final Element ele = createElement(root, "Product"); //$NON-NLS-1$

        validateProductVersion(this.version);

        ele.setAttribute("Version", this.version); //$NON-NLS-1$
        ele.setAttribute("Language", "1033"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Id", "*"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Manufacturer", this.manufacturer); //$NON-NLS-1$
        ele.setAttribute("UpgradeCode", this.upgradeCode); //$NON-NLS-1$
        ele.setAttribute("Name", this.name); //$NON-NLS-1$

        createPackage(ele);
        createProperties(ele);
        createMedia(ele);
        createUpgrade(ele);
        createTargetDir(ele);

        createCommonDriverServices(ele, base);
        createEquinoxApplicationServices(ele, base);

        createLicense(base);
        createUserInterface(ele);

        createFeature(ele);
    }

    private void createLicense(final File base) throws Exception {
        final File targetFile = new File(new File(base, "resources"), "license.rtf");
        if (this.licenseFile != null) {
            if (this.licenseFile.isFile()) {
                Files.copy(this.licenseFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            } else {
                throw new IllegalStateException(String.format("Specified license file could not be found (%s)",
                        this.licenseFile.getAbsoluteFile()));
            }
        } else {
            writeLicense(targetFile, "Unspecified license!");
        }
    }

    private void writeLicense(final File targetFile, final String string) throws Exception {
        try (PrintWriter writer = new PrintWriter(targetFile, "US-ASCII")) //$NON-NLS-1$
        {
            writer.println("{\\rtf\\ansi");
            writer.println(string);
            writer.println("}");
        }
    }

    private void createUserInterface(final Element product) {
        if (!this.useUserInterface) {
            return;
        }

        {
            final Element ui = createElement(product, "UIRef");
            ui.setAttribute("Id", "WixUI_Minimal");
        }
        {
            final Element ui = createElement(product, "UIRef");
            ui.setAttribute("Id", "WixUI_ErrorProgressText");
        }
        {
            final Element var = createElement(product, "WixVariable");
            var.setAttribute("Id", "WixUILicenseRtf");
            var.setAttribute("Value", "resources\\license.rtf");
        }
    }

    private void createProperties(final Element product) {
        final Element p1 = createElement(product, "Property"); //$NON-NLS-1$
        p1.setAttribute("Id", "ES_JARS"); //$NON-NLS-1$ //$NON-NLS-2$
        final Element cs1 = createElement(p1, "RegistrySearch"); //$NON-NLS-1$
        cs1.setAttribute("Id", "search.jars"); //$NON-NLS-1$ //$NON-NLS-2$
        cs1.setAttribute("Root", "HKLM"); //$NON-NLS-1$ //$NON-NLS-2$
        cs1.setAttribute("Type", "raw"); //$NON-NLS-1$ //$NON-NLS-2$
        cs1.setAttribute("Key", "SOFTWARE\\Eclipse SCADA Common Drivers"); //$NON-NLS-1$ //$NON-NLS-2$
        cs1.setAttribute("Name", "JarPath"); //$NON-NLS-1$ //$NON-NLS-2$

        final Element p2 = createElement(product, "Property"); //$NON-NLS-1$
        p2.setAttribute("Id", "ES_EXTRA_JARS"); //$NON-NLS-1$ //$NON-NLS-2$
        final Element cs2 = createElement(p2, "RegistrySearch"); //$NON-NLS-1$
        cs2.setAttribute("Id", "search.extra.jars"); //$NON-NLS-1$ //$NON-NLS-2$
        cs2.setAttribute("Root", "HKLM"); //$NON-NLS-1$ //$NON-NLS-2$
        cs2.setAttribute("Type", "raw"); //$NON-NLS-1$ //$NON-NLS-2$
        cs2.setAttribute("Key", "SOFTWARE\\Eclipse SCADA Common Drivers"); //$NON-NLS-1$ //$NON-NLS-2$
        cs2.setAttribute("Name", "ExtraJarPath"); //$NON-NLS-1$ //$NON-NLS-2$

        Element cond;

        cond = createElement(product, "Condition"); //$NON-NLS-1$
        cond.setAttribute("Message", "You need to install the Eclipse SCADA Common Driver package first (jar)"); //$NON-NLS-1$ //$NON-NLS-2$
        cond.appendChild(product.getOwnerDocument().createCDATASection("ES_JARS OR Installed")); //$NON-NLS-1$

        cond = createElement(product, "Condition"); //$NON-NLS-1$
        cond.setAttribute("Message", //$NON-NLS-1$
                "You need to install the Eclipse SCADA Common Driver package first (extra-jars)"); //$NON-NLS-1$
        cond.appendChild(product.getOwnerDocument().createCDATASection("ES_EXTRA_JARS OR Installed")); //$NON-NLS-1$
    }

    private void createCommonDriverServices(final Element product, final File base) throws Exception {
        final Element dir = createElement(product, "DirectoryRef"); //$NON-NLS-1$
        dir.setAttribute("Id", "INSTALLDIR"); //$NON-NLS-1$ //$NON-NLS-2$

        final Element dir2 = createElement(dir, "Directory"); //$NON-NLS-1$
        dir2.setAttribute("Id", "drivers"); //$NON-NLS-1$ //$NON-NLS-2$
        dir2.setAttribute("Name", "drivers"); //$NON-NLS-1$ //$NON-NLS-2$

        for (final CommonDriverService cds : this.commonDrivers) {
            final File driverResourceBase = new File(new File(new File(base, "resources"), "drivers"), //$NON-NLS-1$//$NON-NLS-2$
                    cds.getName());
            driverResourceBase.mkdirs();
            createCommonDriverService(dir2, cds, driverResourceBase);
        }
    }

    private String makeServiceName(final CommonDriverService cds) {
        return String.format("eclipse_scada_common_driver_%s", cds.getName()); //$NON-NLS-1$
    }

    private String makeServiceName(final EquinoxAppService eas) {
        return String.format("eclipse_scada_app_%s", eas.getName()); //$NON-NLS-1$
    }

    /**
     * Create a component element and register it <br/>
     * Note: This method must be called <em>before</em> the method
     * {@link #createFeature(Element)} is called
     */
    private Element createComponent(final Element parent, final String id) {
        final Element comp = createElement(parent, "Component"); //$NON-NLS-1$
        comp.setAttribute("Id", id); //$NON-NLS-1$
        this.components.add(id);
        return comp;
    }

    private Element createCommonService(final Element parent, final String serviceName, final String name,
            final String description) {
        final Element dir = createElement(parent, "Directory"); //$NON-NLS-1$
        dir.setAttribute("Id", serviceName); //$NON-NLS-1$
        dir.setAttribute("Name", name); //$NON-NLS-1$

        final Element comp = createComponent(dir, serviceName);
        comp.setAttribute("Guid", "*"); //$NON-NLS-1$ //$NON-NLS-2$

        final String fileId = serviceName + ".exe";

        Element file;
        file = createElement(comp, "File"); //$NON-NLS-1$
        file.setAttribute("Id", fileId); //$NON-NLS-1$
        if (this.platform == MsiPlatform.WIN32) {
            file.setAttribute("Source", "unpack\\commons-daemon\\prunsrv.exe"); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            file.setAttribute("Source", "unpack\\commons-daemon\\amd64\\prunsrv.exe"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        Element reg;
        reg = createElement(comp, "RegistryKey"); //$NON-NLS-1$
        reg.setAttribute("Root", "HKLM"); //$NON-NLS-1$ //$NON-NLS-2$
        reg.setAttribute("Key", "SYSTEM\\CurrentControlSet\\services\\" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$

        reg = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        reg.setAttribute("Key", "Parameters"); //$NON-NLS-1$ //$NON-NLS-2$
        reg.setAttribute("ForceCreateOnInstall", "yes"); //$NON-NLS-1$ //$NON-NLS-2$

        // create firewall rule
        if (this.useFirewall) {
            final Element fw = createElement(comp, "firewall", "FirewallException", //$NON-NLS-1$//$NON-NLS-2$
                    "http://schemas.microsoft.com/wix/FirewallExtension");
            fw.setAttribute("Id", serviceName + "_fw"); //$NON-NLS-1$ //$NON-NLS-2$
            fw.setAttribute("Name", name); //$NON-NLS-1$
            fw.setAttribute("Scope", "any"); //$NON-NLS-1$ //$NON-NLS-2$
            fw.setAttribute("Description", description); //$NON-NLS-1$
            fw.setAttribute("Program", String.format("[#%s]", fileId)); //$NON-NLS-1$ //$NON-NLS-2$
        }

        return comp;
    }

    private static class ServiceConfiguration {
        String displayName;

        String description;

        String startClass;

        String startMethod;

        String stopClass;

        String stopMethod;

        String[] startArguments;

        Properties properties;

        String classpath;

        List<String> jvmArgs;
    }

    private void createProcrunService(final Element comp, final String serviceName,
            final ServiceConfiguration serviceConfiguration) {
        Element reg;
        Element regSub;
        // create prunsrv configuration

        reg = createElement(comp, "RegistryKey"); //$NON-NLS-1$
        reg.setAttribute("Root", "HKLM"); //$NON-NLS-1$ //$NON-NLS-2$
        if (this.platform == MsiPlatform.WIN32) {
            reg.setAttribute("Key", "SOFTWARE\\Apache Software Foundation\\Procrun 2.0\\" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            // this has to always go to the 32bit registry
            reg.setAttribute("Key", //$NON-NLS-1$
                    "SOFTWARE\\Wow6432Node\\Apache Software Foundation\\Procrun 2.0\\" + serviceName); //$NON-NLS-1$
        }
        reg = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        reg.setAttribute("Key", "Parameters"); //$NON-NLS-1$ //$NON-NLS-2$

        // java

        regSub = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        regSub.setAttribute("Key", "Java"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "Jvm", "auto"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "Classpath", serviceConfiguration.classpath); //$NON-NLS-1$

        final List<String> jvmProps = new LinkedList<>();

        for (final String key : serviceConfiguration.properties.stringPropertyNames()) {
            jvmProps.add(String.format("-D%s=%s", key, serviceConfiguration.properties.getProperty(key))); //$NON-NLS-1$
        }
        if (serviceConfiguration.jvmArgs != null) {
            jvmProps.addAll(serviceConfiguration.jvmArgs);
        }
        addValues(regSub, "Options", jvmProps.toArray(new String[jvmProps.size()])); //$NON-NLS-1$

        // log

        regSub = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        regSub.setAttribute("Key", "Log"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "Prefix", "common-driver-" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "StdOutput", "auto"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "StdError", "auto"); //$NON-NLS-1$ //$NON-NLS-2$

        // start

        regSub = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        regSub.setAttribute("Key", "Start"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "Class", serviceConfiguration.startClass); //$NON-NLS-1$
        addValue(regSub, "Method", serviceConfiguration.startMethod); //$NON-NLS-1$
        addValue(regSub, "Mode", "jvm"); //$NON-NLS-1$ //$NON-NLS-2$
        addValues(regSub, "Params", serviceConfiguration.startArguments); //$NON-NLS-1$

        // stop

        regSub = createElement(reg, "RegistryKey"); //$NON-NLS-1$
        regSub.setAttribute("Key", "Stop"); //$NON-NLS-1$ //$NON-NLS-2$
        addValue(regSub, "Class", serviceConfiguration.stopClass); //$NON-NLS-1$
        addValue(regSub, "Method", serviceConfiguration.stopMethod); //$NON-NLS-1$
        addValue(regSub, "Mode", "jvm"); //$NON-NLS-1$ //$NON-NLS-2$

        final Element si = createElement(comp, "ServiceInstall"); //$NON-NLS-1$
        si.setAttribute("Id", "ServiceInstall_" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        si.setAttribute("DisplayName", serviceConfiguration.displayName); //$NON-NLS-1$
        si.setAttribute("Description", serviceConfiguration.description); //$NON-NLS-1$
        si.setAttribute("Name", serviceName); //$NON-NLS-1$
        si.setAttribute("Type", "ownProcess"); //$NON-NLS-1$ //$NON-NLS-2$
        si.setAttribute("Start", "auto"); //$NON-NLS-1$ //$NON-NLS-2$
        si.setAttribute("ErrorControl", "normal"); //$NON-NLS-1$ //$NON-NLS-2$
        si.setAttribute("Vital", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
        si.setAttribute("Arguments", String.format("//RS//%s", serviceName)); //$NON-NLS-1$ //$NON-NLS-2$

        final Element sd = createElement(si, "ServiceDependency"); //$NON-NLS-1$
        sd.setAttribute("Id", "tcpip"); //$NON-NLS-1$ //$NON-NLS-2$

        final Element sc = createElement(comp, "ServiceControl"); //$NON-NLS-1$
        sc.setAttribute("Id", "ServiceControl_" + serviceName);
        sc.setAttribute("Name", serviceName); //$NON-NLS-1$
        sc.setAttribute("Stop", "both"); //$NON-NLS-1$ //$NON-NLS-2$
        sc.setAttribute("Remove", "uninstall"); //$NON-NLS-1$ //$NON-NLS-2$
        sc.setAttribute("Wait", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    private void createEquinoxApplicationService(final Element parent, final EquinoxAppService eas,
            final File resourceBase) throws Exception {
        final Profile profile = eas.getProfile();

        final String description = String.format("This is the Eclipse SCADA application instance '%s'",
                eas.getName());

        final String serviceName = makeServiceName(eas);
        final Element comp = createCommonService(parent, serviceName, eas.getName(), description);

        // cfg dir

        final String cfgDirId = "cfg_" + serviceName; //$NON-NLS-1$

        final Element cfgDir = createElement(this.caDir, "Directory"); //$NON-NLS-1$
        cfgDir.setAttribute("Id", cfgDirId); //$NON-NLS-1$
        cfgDir.setAttribute("Name", eas.getName()); //$NON-NLS-1$

        final Element cfgDirComp = createComponent(cfgDir, "comp_" + cfgDirId);
        cfgDirComp.setAttribute("Guid", UUID.nameUUIDFromBytes(cfgDirId.getBytes()).toString());

        createElement(cfgDirComp, "CreateFolder"); //$NON-NLS-1$
        final Element rmdir = createElement(cfgDirComp, "RemoveFolder"); //$NON-NLS-1$
        rmdir.setAttribute("Id", "rmdir_" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        rmdir.setAttribute("On", "uninstall"); //$NON-NLS-1$ //$NON-NLS-2$

        // create start level file

        final File startLevel = new File(resourceBase, "startLevels.properties"); //$NON-NLS-1$
        writeStartLevels(startLevel, profile);

        Element file = createElement(comp, "File"); //$NON-NLS-1$
        file.setAttribute("Id", "startLevels_" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        file.setAttribute("Source", String.format("resources\\apps\\%s\\startLevels.properties", eas.getName())); //$NON-NLS-1$ //$NON-NLS-2$

        // CA bootstrap file

        file = createElement(comp, "File"); //$NON-NLS-1$
        file.setAttribute("Id", "caBootstrap_" + serviceName); //$NON-NLS-1$
        file.setAttribute("Source", String.format("resources\\apps\\%s\\ca.bootstrap.json", eas.getName())); //$NON-NLS-1$ //$NON-NLS-2$

        Files.copy(new File(eas.getConfigurationBase(), "data.json").toPath(), //$NON-NLS-1$
                new File(resourceBase, "ca.bootstrap.json").toPath(), StandardCopyOption.REPLACE_EXISTING); //$NON-NLS-1$

        // create properties for equinox

        final Properties p = new Properties();
        p.setProperty("eclipse.ignoreApp", "true"); //$NON-NLS-1$ //$NON-NLS-2$
        p.setProperty("osgi.noShutdown", "true"); //$NON-NLS-1$ //$NON-NLS-2$
        p.setProperty("osgi.syspath", String.format("[INSTALLDIR]\\apps\\%1$s\\plugins", eas.getName())); //$NON-NLS-1$ //$NON-NLS-2$
        for (final SystemProperty prop : profile.getProperty()) {
            p.setProperty(prop.getKey(), prop.getValue());
        }
        if (p.containsKey("logback.configurationFile")) //$NON-NLS-1$
        {
            // override, since "eval" will not work here
            p.setProperty("logback.configurationFile", //$NON-NLS-1$
                    String.format("[INSTALLDIR]\\apps\\%s\\logback.xml", eas.getName())); //$NON-NLS-1$
            createLogback(comp, eas, resourceBase);
        }
        if (p.containsKey("org.eclipse.scada.ca.file.root")) //$NON-NLS-1$
        {
            p.setProperty("org.eclipse.scada.ca.file.root", String.format("[%s]\\ca", cfgDirId)); //$NON-NLS-1$ //$NON-NLS-2$
        }
        p.setProperty("org.osgi.framework.bootdelegation", "com.sun.script.javascript"); //$NON-NLS-1$ //$NON-NLS-2$
        p.setProperty("org.eclipse.scada.ca.file.provisionJsonUrl", //$NON-NLS-1$
                String.format("file:[INSTALLDIR]\\apps\\%s\\ca.bootstrap.json", eas.getName())); //$NON-NLS-1$

        final ServiceConfiguration cfg = new ServiceConfiguration();
        cfg.displayName = "Eclipse SCADA Application: " + eas.getName(); //$NON-NLS-1$
        cfg.description = description;
        cfg.startClass = "org.eclipse.scada.utils.osgi.daemon.EclipseDaemon"; //$NON-NLS-1$
        cfg.startMethod = "start"; //$NON-NLS-1$
        cfg.stopClass = "org.eclipse.scada.utils.osgi.daemon.EclipseDaemon"; //$NON-NLS-1$
        cfg.stopMethod = "stop"; //$NON-NLS-1$
        cfg.properties = p;
        cfg.jvmArgs = profile.getJvmArguments();
        cfg.startArguments = profile.getArguments() != null
                ? profile.getArguments().toArray(new String[profile.getArguments().size()])
                : null;
        cfg.classpath = String.format("[INSTALLDIR]\\apps\\%1$s\\daemon.jar;[INSTALLDIR]\\apps\\%1$s\\osgi.jar", //$NON-NLS-1$
                eas.getName());
        createProcrunService(comp, serviceName, cfg);
    }

    private void createLogback(final Element comp, final EquinoxAppService eas, final File resourceBase)
            throws Exception {
        final Element file = createElement(comp, "File"); //$NON-NLS-1$
        final String serviceName = makeServiceName(eas);
        file.setAttribute("Id", "logback.xml_" + serviceName); //$NON-NLS-1$ //$NON-NLS-2$
        file.setAttribute("Source", String.format("resources\\apps\\%s\\logback.xml", eas.getName())); //$NON-NLS-1$ //$NON-NLS-2$

        final File logback = new File(resourceBase, "logback.xml"); //$NON-NLS-1$
        try (FileOutputStream out = new FileOutputStream(logback)) {
            Resources.copy(Resources.getResource(MsiHandler.class, "templates/msi/app.logback.xml"), out); //$NON-NLS-1$
        }
    }

    private void writeStartLevels(final File startLevel, final Profile profile) throws IOException {
        try (final PrintWriter writer = new PrintWriter(startLevel)) {
            for (final Map.Entry<String, Integer> entry : Profiles.makeStartLevelMap(profile).entrySet()) {
                writer.println(String.format("%s=%s", entry.getKey(), entry.getValue())); //$NON-NLS-1$
            }
        }
    }

    private void createCommonDriverService(final Element parent, final CommonDriverService cds,
            final File resourceBase) throws Exception {
        final Path configurationFile = new File(resourceBase, "exporter.xml").toPath(); //$NON-NLS-1$

        final String description = String.format("This is the Eclipse SCADA common driver instance '%s'", //$NON-NLS-1$
                cds.getName());

        final Element comp = createCommonService(parent, makeServiceName(cds), cds.getName(), description);

        Element file;

        final String serviceName = makeServiceName(cds);

        final String fileIdConfiguration = "configuration_" + serviceName; //$NON-NLS-1$
        file = createElement(comp, "File"); //$NON-NLS-1$
        file.setAttribute("Id", fileIdConfiguration); //$NON-NLS-1$
        file.setAttribute("Source", String.format("resources\\drivers\\%s\\exporter.xml", cds.getName())); //$NON-NLS-1$ //$NON-NLS-2$

        // copy configuration file
        Files.copy(cds.getConfiguration().toPath(), configurationFile);

        // create prunsrv configuration
        final ServiceConfiguration cfg = new ServiceConfiguration();
        cfg.displayName = "Eclipse SCADA Common Driver: " + cds.getName(); //$NON-NLS-1$
        cfg.description = description;
        cfg.startClass = cfg.stopClass = "org.eclipse.scada.da.server.exporter.Application"; //$NON-NLS-1$
        cfg.startMethod = "main"; //$NON-NLS-1$
        cfg.startArguments = new String[] { "[#" + fileIdConfiguration + "]" //$NON-NLS-1$ //$NON-NLS-2$
        };
        cfg.stopMethod = "stop"; //$NON-NLS-1$
        cfg.properties = cds.getProps();
        cfg.classpath = "[ES_JARS]\\*;[ES_EXTRA_JARS]\\*"; //$NON-NLS-1$
        createProcrunService(comp, serviceName, cfg);

        // TODO: add firewall rule
    }

    private void createEquinoxApplicationServices(final Element product, final File base) throws Exception {
        if (this.apps.isEmpty()) {
            return;
        }

        createAppCleanup(product);

        final Element dir = createElement(product, "DirectoryRef"); //$NON-NLS-1$
        dir.setAttribute("Id", "apps"); //$NON-NLS-1$ //$NON-NLS-2$

        for (final EquinoxAppService eqs : this.apps) {
            final File resourceBase = new File(new File(new File(base, "resources"), "apps"), eqs.getName()); //$NON-NLS-1$ //$NON-NLS-2$
            resourceBase.mkdirs();
            createEquinoxApplicationService(dir, eqs, resourceBase);
        }
    }

    private void createAppCleanup(final Element product) {
        final Element ca = createElement(product, "CustomAction"); //$NON-NLS-1$
        ca.setAttribute("Id", "CleanupApps"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("Directory", "INSTALLDIR"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("Execute", "deferred"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("Return", "ignore"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("HideTarget", "no"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("Impersonate", "no"); //$NON-NLS-1$ //$NON-NLS-2$
        ca.setAttribute("ExeCommand", "cmd /C \"rmdir /Q /S apps\""); //$NON-NLS-1$ //$NON-NLS-2$

        final Element ies = createElement(product, "InstallExecuteSequence"); //$NON-NLS-1$
        final Element c = createElement(ies, "Custom"); //$NON-NLS-1$
        c.setAttribute("Action", "CleanupApps"); //$NON-NLS-1$ //$NON-NLS-2$
        c.setAttribute("After", "RemoveFiles"); //$NON-NLS-1$ //$NON-NLS-2$
        c.appendChild(product.getOwnerDocument().createTextNode("REMOVE=\"ALL\"")); //$NON-NLS-1$
    }

    private void addValue(final Element reg, final String key, final String value) {
        final Element ele = createElement(reg, "RegistryValue"); //$NON-NLS-1$
        ele.setAttribute("Type", "string"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Name", key); //$NON-NLS-1$
        ele.setAttribute("Value", value); //$NON-NLS-1$
    }

    private void addValues(final Element reg, final String key, final String[] values) {
        if (values == null || values.length <= 0) {
            return;
        }

        final Element ele = createElement(reg, "RegistryValue"); //$NON-NLS-1$
        ele.setAttribute("Type", "multiString"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Name", key); //$NON-NLS-1$

        for (final String value : values) {
            createElementWithText(ele, "MultiStringValue", value); //$NON-NLS-1$
        }
    }

    private void createFeature(final Element product) {
        final Element feat = createElement(product, "Feature"); //$NON-NLS-1$
        feat.setAttribute("Id", "Complete"); //$NON-NLS-1$ //$NON-NLS-2$
        feat.setAttribute("Level", "1"); //$NON-NLS-1$ //$NON-NLS-2$
        feat.setAttribute("Title", this.name); //$NON-NLS-1$
        feat.setAttribute("Display", "expand"); //$NON-NLS-1$ //$NON-NLS-2$
        feat.setAttribute("Description", "The complete package"); //$NON-NLS-1$s

        Element entry;

        entry = createElement(feat, "ComponentGroupRef"); //$NON-NLS-1$
        entry.setAttribute("Id", "ScanComponent"); //$NON-NLS-1$ //$NON-NLS-2$

        for (final String comp : this.components) {
            entry = createElement(feat, "ComponentRef"); //$NON-NLS-1$
            entry.setAttribute("Id", comp); //$NON-NLS-1$
        }
    }

    private void createTargetDir(final Element product) {
        final Element dir1 = createElement(product, "Directory"); //$NON-NLS-1$
        dir1.setAttribute("Id", "TARGETDIR"); //$NON-NLS-1$ //$NON-NLS-2$
        dir1.setAttribute("Name", "SourceDir"); //$NON-NLS-1$ //$NON-NLS-2$

        final Element dir2 = createElement(dir1, "Directory"); //$NON-NLS-1$
        dir2.setAttribute("Name", "PFiles"); //$NON-NLS-1$ //$NON-NLS-2$
        if (this.platform == MsiPlatform.WIN32) {
            dir2.setAttribute("Id", "ProgramFilesFolder"); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            dir2.setAttribute("Id", "ProgramFiles64Folder"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        final Element dir3 = createElement(dir2, "Directory"); //$NON-NLS-1$
        dir3.setAttribute("Id", "INSTALLDIR"); //$NON-NLS-1$ //$NON-NLS-2$
        dir3.setAttribute("Name", this.name); //$NON-NLS-1$

        this.caDir = createElement(dir3, "Directory"); //$NON-NLS-1$
        this.caDir.setAttribute("Id", "CA_DIR"); //$NON-NLS-1$ //$NON-NLS-2$
        this.caDir.setAttribute("Name", "configuration"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    private void createUpgrade(final Element product) {
        final Element ele = createElement(product, "MajorUpgrade"); //$NON-NLS-1$
        ele.setAttribute("AllowDowngrades", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    private void createMedia(final Element product) {
        final Element ele = createElement(product, "Media"); //$NON-NLS-1$
        ele.setAttribute("Id", "1"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Cabinet", "contents.cab"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("EmbedCab", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("CompressionLevel", "none"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    private void createPackage(final Element product) {
        final Element ele = createElement(product, "Package"); //$NON-NLS-1$
        ele.setAttribute("InstallScope", "perMachine"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("InstallPrivileges", "elevated"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("InstallerVersion", "200"); //$NON-NLS-1$ //$NON-NLS-2$
        ele.setAttribute("Compressed", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    public String getManufacturer() {
        return this.manufacturer;
    }

    public void setManufacturer(final String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public String getUpgradeCode() {
        return this.upgradeCode;
    }

    public void setUpgradeCode(final String upgradeCode) {
        this.upgradeCode = upgradeCode;
    }

    public String getName() {
        return this.name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public String getVersion() {
        return this.version;
    }

    public void setVersion(final String version) {
        this.version = version;
    }

    public void addCommonDriver(final CommonDriverService driver) {
        this.commonDrivers.add(driver);
    }

    public void addApplication(final EquinoxAppService equinoxAppService) {
        this.apps.add(equinoxAppService);
    }

    public MsiPlatform getPlatform() {
        return this.platform;
    }

    public void setPlatform(final MsiPlatform platform) {
        this.platform = platform;
    }

    public void setLicenseFile(final File licenseFile) {
        this.licenseFile = licenseFile;
    }

}