org.structr.util.StructrLicenseManager.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.util.StructrLicenseManager.java

Source

/**
 * Copyright (C) 2010-2018 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.security.CodeSigner;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.RandomUtils;
import org.codehaus.plexus.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.service.Feature;
import org.structr.api.service.LicenseManager;
import org.structr.common.error.FrameworkException;
import org.structr.core.graph.MaintenanceCommand;
import org.structr.core.graph.NodeServiceCommand;

/**
 */
public class StructrLicenseManager implements LicenseManager {

    private static final Logger logger = LoggerFactory.getLogger(LicenseManager.class);
    private static final String Certificate = "MIIDpzCCAo+gAwIBAgIEbgcR/TANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMC"
            + "REUxDzANBgNVBAgTBkhlc3NlbjEaMBgGA1UEBxMRRnJhbmtmdXJ0IGFtIE1haW4x"
            + "FTATBgNVBAoTDFN0cnVjdHIgR21iSDEUMBIGA1UECxMLRGV2ZWxvcG1lbnQxGjAY"
            + "BgNVBAMTEUNocmlzdGlhbiBNb3JnbmVyMB4XDTE3MDUxNzExMjExMloXDTI4MDQy"
            + "OTExMjExMlowgYMxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZIZXNzZW4xGjAYBgNV"
            + "BAcTEUZyYW5rZnVydCBhbSBNYWluMRUwEwYDVQQKEwxTdHJ1Y3RyIEdtYkgxFDAS"
            + "BgNVBAsTC0RldmVsb3BtZW50MRowGAYDVQQDExFDaHJpc3RpYW4gTW9yZ25lcjCC"
            + "ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI2i0WhHV9OauRZ6viefhuSK"
            + "OZaKlTvVYi0HLmk99/JlfIvAkkmS3Dlz+xwp9SUUPdfFMvPvMpgwXyfj9HlZGMg6"
            + "fdSGocZyJpozVSmzlM0D1xJB6MbU1k8lcvI52IYx6gUCa7jQIoPxkwb+8/NP2SXA"
            + "56RVIsNQFGpb31AwKNFViE5CqCHcF4hQ3uB/rcbxPUQ9t94R51dNLMWw37lKiqpq"
            + "iHGKYmriBOV9iivdgUFFFA0hNbclnRImXhYuUgCBaPmtjOGDywBfkRs5kbRBixR5"
            + "V41DOqaFnG5jOKTW+ycLMMmtvHnPt1dHckvYTnnM0YUS9FINbXLoxZBw2hP1mfkC"
            + "AwEAAaMhMB8wHQYDVR0OBBYEFBM6fjBbxeespRRG87mEHbqzfXCGMA0GCSqGSIb3"
            + "DQEBDQUAA4IBAQANnACQFo5r8gNK8ULZ5yvndx8Bv3YNbQEtXgHSnaQLWAgIgflG"
            + "E4GF8/0OwaxVuGxvyCd/ib74vjfCgOJvNe+XOHQQ+6o30JaILar2353QvVS5cqSU"
            + "HACOyFaBa7ndjgHSYdzabZHdhmpIIv/Tx2whClYdCCLTQi1sYjtXGMJ4FJyyJqjm"
            + "IjREcPU1KT8etnYQXaTvj2njM26lZVWc7DizsN83b+vL5h2m+z/4I5dAwVGEBMFZ"
            + "/u7r3lfqJ4h5bVTb6AMJ9gl/lOyob46gH1jNbx5ld53/ADgDFRcMW2vwLiZhay0p"
            + "Nhx1o8vT2VnQUzZxU1G4gnkdtiXN6knFTorl";

    public static final String DataEncryptionAlgorithm = "AES/CBC/PKCS5Padding";
    public static final String KeyEncryptionAlgorithm = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
    public static final String SignatureAlgorithm = "SHA512WithRSA";
    public static final String DatePattern = "yyyy-MM-dd";
    public static final String KeystoreAlias = "structr";
    public static final String KeyAlgorithm = "RSA";
    public static final String CharSet = "UTF-8";
    public static final String NameKey = "name";
    public static final String DateKey = "date";
    public static final String StartKey = "start";
    public static final String EndKey = "end";
    public static final String EditionKey = "edition";
    public static final String ModulesKey = "modules";
    public static final String MachineKey = "machine";
    public static final String SignatureKey = "key";
    public static final String ServersKey = "servers";
    public static final String HostIdKey = "hostId";
    public static final String LimitKey = "limit";
    public static final String HostIdMappingKey = "hostIdValidationAttempts";
    public static final int ServerPort = 5725;

    private static final int CommunityMask = 0x01; // 0001
    private static final int BasicMask = 0x03; // 0011
    private static final int SmallBusinessMask = 0x07; // 0111
    private static final int EnterpriseMask = 0x0f; // 1111

    private final Set<String> modules = new LinkedHashSet<>(Arrays.asList("core", "rest", "ui"));
    private final Set<String> classes = new LinkedHashSet<>();
    private final SimpleDateFormat format = new SimpleDateFormat(DatePattern);
    private Certificate certificate = null;
    private PublicKey publicKey = null;
    private boolean allModulesLicensed = false;
    private boolean licensePresent = false;
    private String edition = "Community";
    private String licensee = null;
    private String startDateString = null;
    private String endDateString = null;
    private int editionMask = CommunityMask;

    public StructrLicenseManager(final String licenseFileName) {

        logger.info("Host ID is {}", createHash());
        logger.info("Checking Structr license..");

        // initialize certificate from static data above
        this.certificate = certFromBase64(Certificate);
        this.publicKey = certificate.getPublicKey();

        // check license
        final Map<String, String> properties = read(licenseFileName);
        if (licensePresent) {

            // read license file (if present)
            if (isValid(properties)) {

                // init edition
                edition = properties.get(EditionKey);
                if (edition != null) {

                    switch (edition) {

                    case "Enterprise":
                        editionMask = EnterpriseMask;
                        break;

                    case "Small Business":
                        editionMask = SmallBusinessMask;
                        break;

                    case "Basic":
                        editionMask = BasicMask;
                        break;
                    }
                }

                licensee = properties.get(NameKey);
                startDateString = properties.get(StartKey);
                endDateString = properties.get(EndKey);

                // init modules
                final String licensedModules = properties.get(ModulesKey);
                if (licensedModules != null) {

                    // remove default modules from Community Edition
                    modules.clear();

                    if ("*".equals(licensedModules)) {

                        allModulesLicensed = true;

                    } else if (!"none".equals(licensedModules)) {

                        for (final String module : licensedModules.split(",")) {

                            modules.add(module.trim());
                        }
                    }
                }
            }
        }

        logger.info("Running {} Edition with {}.", edition,
                allModulesLicensed ? "all modules" : "modules " + modules.toString());

        if (licensee != null) {

            logger.info("Licensed for {}", licensee);

        } else {

            logger.info("Evaluation License");
        }
    }

    @Override
    public boolean isModuleLicensed(final String module) {
        return allModulesLicensed || modules.contains(module);
    }

    @Override
    public boolean isClassLicensed(final String fqcn) {
        return allModulesLicensed || classes.contains(fqcn);
    }

    @Override
    public boolean isEdition(final int bitmask) {
        return (editionMask & bitmask) > 0;
    }

    @Override
    public String getEdition() {
        return edition;
    }

    @Override
    public String getLicensee() {
        return licensee;
    }

    @Override
    public String getHardwareFingerprint() {
        return createHash();
    }

    @Override
    public String getStartDate() {
        return startDateString;
    }

    @Override
    public String getEndDate() {
        return endDateString;
    }

    @Override
    public boolean isValid(final Feature feature) {

        if (feature != null) {

            final String moduleName = feature.getModuleName();
            return moduleName != null && isModuleLicensed(moduleName);
        }

        return false;
    }

    /**
     *
     * @param codeSigners
     * @return
     */
    @Override
    public boolean isValid(final CodeSigner[] codeSigners) {

        if (codeSigners != null && codeSigners.length > 0) {

            for (final CodeSigner codeSigner : codeSigners) {

                for (final Certificate cert : codeSigner.getSignerCertPath().getCertificates()) {

                    try {

                        cert.verify(publicKey);
                        return true;

                    } catch (Throwable ignore) {
                    }
                }
            }
        }

        // none of the code signer certificates could be verified using our key => not valid
        return false;
    }

    @Override
    public void addLicensedClass(final String fqcn) {
        classes.add(fqcn);
    }

    // ----- private methods -----
    private boolean isValid(final Map<String, String> properties) {

        if (!licensePresent) {
            return false;
        }

        final String src = collectLicenseFieldsForSignature(properties);
        final String name = properties.get(NameKey);
        final String key = properties.get(SignatureKey);
        final String hostId = properties.get(MachineKey);
        final String thisHostId = createHash();
        final String edition = properties.get(EditionKey);
        final String modules = properties.get(ModulesKey);
        final String dateString = properties.get(DateKey);
        final String startDateString = properties.get(StartKey);
        final String endDateString = properties.get(EndKey);
        final String serversString = properties.get(ServersKey);
        final Date now = new Date();

        if (StringUtils.isEmpty(key)) {

            logger.error("Unable to read key from license file.");
            return false;
        }

        try {

            final byte[] data = src.getBytes(CharSet);
            final Signature signer = Signature.getInstance(SignatureAlgorithm);
            final byte[] signature = Hex.decodeHex(key.toCharArray());

            signer.initVerify(certificate);
            signer.update(data);

            if (!signer.verify(signature)) {

                logger.error("License signature verification failed, license is not valid.");
                return false;
            }

        } catch (Throwable t) {

            logger.error("Unable to verify license.", t);
            return false;
        }

        if (StringUtils.isEmpty(name)) {

            logger.error("License file doesn't contain licensee name.");
            return false;
        }

        if (StringUtils.isEmpty(edition)) {

            logger.error("License file doesn't contain edition.");
            return false;
        }

        if (StringUtils.isEmpty(modules)) {

            logger.error("License file doesn't contain modules.");
            return false;
        }

        if (StringUtils.isEmpty(hostId)) {

            logger.error("License file doesn't contain host ID.");
            return false;
        }

        if (StringUtils.isEmpty(dateString)) {

            logger.error("License file doesn't contain license date.");
            return false;
        }

        if (StringUtils.isEmpty(startDateString)) {

            logger.error("License file doesn't contain start date.");
            return false;
        }

        if (StringUtils.isEmpty(endDateString)) {

            logger.error("License file doesn't contain end date.");
            return false;
        }

        // verify host ID
        if (!thisHostId.equals(hostId) && !"*".equals(hostId)) {

            logger.error("Host ID found in license ({}) file does not match current host ID.", hostId);
            return false;
        }

        if ("*".equals(hostId)) {

            // check volume license against server addresses
            if (StringUtils.isNotBlank(serversString)) {

                // send HostID to server
                properties.put(HostIdKey, thisHostId);

                return checkVolumeLicense(properties, serversString);
            }

            final Calendar issuedAtPlusOneMonth = GregorianCalendar.getInstance();
            final Calendar cal = GregorianCalendar.getInstance();

            // set issuedAt to license date plus one month
            issuedAtPlusOneMonth.setTime(parseDate(dateString));
            issuedAtPlusOneMonth.add(Calendar.MONTH, 1);

            // check that the license file was issued not more than one month ago
            if (cal.after(issuedAtPlusOneMonth)) {

                logger.error(
                        "Development license found in license file is not valid any more, license period ended {}.",
                        format.format(issuedAtPlusOneMonth.getTime()));
                return false;
            }
        }

        // verify that the license is valid for the current date
        final Date startDate = parseDate(startDateString);
        if (startDate != null && now.before(startDate) && !now.equals(startDate)) {

            logger.error("License found in license file is not yet valid, license period starts {}.",
                    format.format(startDate.getTime()));
            return false;
        }

        // verify that the license is valid for the current date
        final Date endDate = parseDate(endDateString);
        if (endDate != null && now.after(endDate) && !now.equals(endDate)) {

            logger.error("License found in license file is not valid any more, license period ended {}.",
                    format.format(endDate.getTime()));
            return false;
        }

        return true;
    }

    private Map<String, String> read(final String fileName) {

        final Decoder base64Decoder = java.util.Base64.getMimeDecoder();
        final Map<String, String> properties = new LinkedHashMap<>();

        try (final BufferedReader reader = new BufferedReader(
                new InputStreamReader(base64Decoder.wrap(new FileInputStream(fileName))))) {

            licensePresent = true;

            String line = reader.readLine();
            while (line != null) {

                final int pos = line.indexOf("=");
                if (pos >= 0) {

                    final String key = line.substring(0, pos).trim();
                    final String value = line.substring(pos + 1).trim();

                    properties.put(key, value);
                }

                line = reader.readLine();
            }

        } catch (FileNotFoundException fnex) {
            licensePresent = false;
        } catch (IOException ioex) {
            logger.warn("Error reading license file: {}", ioex.getMessage());
        }

        return properties;
    }

    private Certificate certFromBase64(final String src) {

        try {

            final byte[] byteKey = Base64.decode(src.getBytes());

            return CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(byteKey));

        } catch (Throwable t) {
            logger.warn("Unable to decode public key.", t);
        }

        return null;
    }

    private String createHash() {

        try {

            final MessageDigest digest = MessageDigest.getInstance("MD5");

            // use network interface hardware addresses for host identification
            for (final NetworkInterface iface : getNetworkInterfaces()) {

                try {
                    final byte[] hardwareAddress = iface.getHardwareAddress();
                    if (hardwareAddress != null) {

                        digest.update(hardwareAddress);
                    }

                } catch (SocketException ex) {
                }
            }

            return Hex.encodeHexString(digest.digest());

        } catch (NoSuchAlgorithmException ex) {
            logger.warn("Unable to create hardware hash.", ex);
        }

        return null;
    }

    private Iterable<NetworkInterface> getNetworkInterfaces() {

        final List<NetworkInterface> interfaces = new LinkedList<>();

        try {
            for (final Enumeration<NetworkInterface> enumeration = NetworkInterface
                    .getNetworkInterfaces(); enumeration.hasMoreElements();) {

                interfaces.add(enumeration.nextElement());
            }

        } catch (SocketException ex) {
        }

        return interfaces;
    }

    private Date parseDate(final String dateOrNull) {

        if (dateOrNull != null) {

            try {
                return alignToDay(format.parse(dateOrNull));

            } catch (Throwable ignore) {
            }
        }

        return new Date(0L);
    }

    private Date alignToDay(final Date date) {

        try {
            return format.parse(format.format(date));

        } catch (Throwable ignore) {
        }

        return null;
    }

    // ----- private static methods -----
    public static String collectLicenseFieldsForSignature(final Map<String, String> properties) {

        final StringBuilder buf = new StringBuilder();

        buf.append(properties.get(NameKey));
        buf.append(properties.get(DateKey));
        buf.append(properties.get(StartKey));
        buf.append(properties.get(EndKey));
        buf.append(properties.get(EditionKey));
        buf.append(properties.get(ModulesKey));
        buf.append(properties.get(MachineKey));

        // optional value
        final String servers = properties.get(ServersKey);
        if (StringUtils.isNotBlank(servers)) {

            buf.append(servers);
        }

        return buf.toString();
    }

    private static void create(final String name, final String start, final String end, final String edition,
            final String modules, final String hostId, final String servers, final String keystoreFileName,
            final String password, final String outputFileName) {

        final Map<String, String> properties = new LinkedHashMap<>();
        final SimpleDateFormat format = new SimpleDateFormat(DatePattern);

        properties.put(NameKey, name);
        properties.put(DateKey, format.format(System.currentTimeMillis()));
        properties.put(StartKey, start);
        properties.put(EndKey, end);
        properties.put(EditionKey, edition);
        properties.put(ModulesKey, modules);
        properties.put(MachineKey, hostId);

        if (StringUtils.isNotBlank(servers)) {
            properties.put(ServersKey, servers);
        }

        sign(properties, keystoreFileName, password);
        write(properties, outputFileName);
    }

    private static void sign(final Map<String, String> properties, final String keystoreFileName,
            final String password) {

        final String src = collectLicenseFieldsForSignature(properties);

        try {

            final byte[] data = src.getBytes(CharSet);
            final Signature signer = Signature.getInstance(SignatureAlgorithm);
            final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

            try (final InputStream is = new FileInputStream(keystoreFileName)) {

                keyStore.load(is, password.toCharArray());

                final Key key = keyStore.getKey(KeystoreAlias, password.toCharArray());

                signer.initSign((PrivateKey) key);
                signer.update(data);

                properties.put(SignatureKey, Hex.encodeHexString(signer.sign()));
            }

        } catch (Throwable t) {
            logger.warn("Unable to sign license.", t);
        }
    }

    private static void write(final Map<String, String> properties, final String fileName) {

        final Encoder base64Encoder = java.util.Base64.getEncoder();

        try (final Writer writer = new OutputStreamWriter(base64Encoder.wrap(new FileOutputStream(fileName)))) {

            writer.write(write(properties));

        } catch (IOException ioex) {
            logger.warn("Unable to write file.", ioex);
        }
    }

    private static String write(final Map<String, String> properties) {

        final StringBuilder buf = new StringBuilder();

        properties.forEach((key, value) -> {

            buf.append(key);
            buf.append(" = ");
            buf.append(value);
            buf.append("\n");
        });

        return buf.toString();

    }

    private boolean checkVolumeLicense(final Map<String, String> properties, final String serversString) {

        try {

            final KeyGenerator kgen = KeyGenerator.getInstance("AES");
            final byte[] data = write(properties).getBytes("utf-8");
            final String name = properties.get(NameKey);
            final byte[] expected = name.getBytes("utf-8");

            kgen.init(128);

            for (final String part : serversString.split("[, ]+")) {

                final String address = part.trim();

                if (StringUtils.isNotBlank(address)) {

                    try {

                        logger.info("Trying to verify volume license with server {}", address);

                        final long t0 = System.currentTimeMillis();
                        final SecretKey aesKey = kgen.generateKey(); // symmetric stream key
                        final byte[] ivspec = RandomUtils.nextBytes(16); // initialization vector for stream cipher
                        final byte[] key = encryptSessionKey(aesKey.getEncoded());
                        final byte[] encryptedIV = encryptSessionKey(ivspec);
                        final byte[] encryptedData = encryptData(data, aesKey, ivspec);
                        final byte[] response = sendAndReceive(address, key, encryptedIV, encryptedData);
                        final boolean result = verify(expected, response);

                        if (result == true) {
                            logger.info("License verified in {} ms", System.currentTimeMillis() - t0);
                        }

                        return result;

                    } catch (Throwable t) {
                        logger.warn("Unable to verify volume license: {}", t.getMessage());
                    }
                }
            }

        } catch (Throwable t) {
            t.printStackTrace();
        }

        return false;
    }

    private byte[] encryptData(final byte[] data, final SecretKey sessionKey, final byte[] ivSpec)
            throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException,
            BadPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException {

        // setup
        final Cipher cipher = Cipher.getInstance(DataEncryptionAlgorithm);

        cipher.init(Cipher.ENCRYPT_MODE, sessionKey, new IvParameterSpec(ivSpec));

        return cipher.doFinal(data);
    }

    private byte[] encryptSessionKey(final byte[] sessionKey)
            throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException,
            BadPaddingException, UnsupportedEncodingException {

        // setup
        final Cipher cipher = Cipher.getInstance(KeyEncryptionAlgorithm);

        cipher.init(Cipher.ENCRYPT_MODE, certificate);

        return cipher.doFinal(sessionKey);
    }

    private byte[] sendAndReceive(final String address, final byte[] key, final byte[] ivspec, final byte[] data) {

        final byte[] result = new byte[256];

        try (final Socket socket = new java.net.Socket(address, ServerPort)) {

            socket.getOutputStream().write(key);
            socket.getOutputStream().flush();

            socket.getOutputStream().write(ivspec);
            socket.getOutputStream().flush();

            socket.getOutputStream().write(data);
            socket.getOutputStream().flush();

            // don't waste much time to wait for an answer
            socket.setSoTimeout(2000);

            // read exactly 256 bytes (size of expected signature response)
            socket.getInputStream().read(result, 0, 256);

        } catch (Throwable t) {
            logger.warn("Unable to verify volume license: {}", t.getMessage());
        }

        return result;
    }

    private boolean verify(final byte[] data, final byte[] signatureData) {

        try {

            final Signature verifier = Signature.getInstance(SignatureAlgorithm);

            verifier.initVerify(certificate);
            verifier.update(data);

            if (verifier.verify(signatureData)) {

                return true;
            }

        } catch (Throwable t) {
            logger.warn("Unable to verify volume license: {}", t.getMessage());
        }

        logger.error("License verification failed, license is not valid.");

        return false;
    }

    // ----- nested classes ------
    public static class CreateLicenseCommand extends NodeServiceCommand implements MaintenanceCommand {

        @Override
        public void execute(final Map<String, Object> attributes) throws FrameworkException {

            final String name = (String) attributes.get(NameKey);
            final String start = (String) attributes.get(StartKey);
            final String end = (String) attributes.get(EndKey);
            final String edition = (String) attributes.get(EditionKey);
            final String modules = (String) attributes.get(ModulesKey);
            final String hostId = (String) attributes.get(MachineKey);
            final String servers = (String) attributes.get(ServersKey);
            final String keystore = (String) attributes.get("keystore");
            final String password = (String) attributes.get("password");

            // outfile, fallback to license.key
            String outFile = (String) attributes.get("outFile");
            if (outFile == null) {
                outFile = "license.key";
            }

            if (name == null || start == null || end == null || edition == null || modules == null || hostId == null
                    || keystore == null || password == null) {

                logger.warn(
                        "Cannot create license file, missing parameter. Parameters are: name, start, end, edition, modules, machine, keystore, password, outFile (optional).");

            } else {

                StructrLicenseManager.create(name, start, end, edition, modules, hostId, servers, keystore,
                        password, outFile);
                logger.info("Successfully created license file {}.", outFile);
            }
        }

        @Override
        public boolean requiresEnclosingTransaction() {
            return false;
        }

        @Override
        public boolean requiresFlushingOfCaches() {
            return false;
        }
    }
}