com.untangle.app.license.LicenseManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.untangle.app.license.LicenseManagerImpl.java

Source

/**
 * $Id: FacebookAuthenticator.java,v 1.00 2017/03/03 19:30:10 dmorris Exp $
 * 
 * Copyright (c) 2003-2017 Untangle, Inc.
 *
 *
 * This software is the confidential and proprietary information of
 * Untangle, Inc. ("Confidential Information"). You shall
 * not disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Untangle.
 */
package com.untangle.app.license;

import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Iterator;

import org.apache.log4j.Logger;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.CloseableHttpResponse;

import com.untangle.uvm.UvmContextFactory;
import com.untangle.uvm.SettingsManager;
import com.untangle.uvm.HookManager;
import com.untangle.uvm.app.AppBase;
import com.untangle.uvm.vnet.PipelineConnector;
import com.untangle.uvm.util.Pulse;
import com.untangle.uvm.util.I18nUtil;
import com.untangle.uvm.app.License;
import com.untangle.uvm.app.LicenseManager;

/**
 * License manager
 */
public class LicenseManagerImpl extends AppBase implements LicenseManager {
    private static final String LICENSE_URL_PROPERTY = "uvm.license.url";
    private static final String DEFAULT_LICENSE_URL = "https://license.untangle.com/license.php";

    private static final double LIENENCY_PERCENT = 1.25; /* the enforced seat limit is the license seat limit TIMES this value */
    private static final int LIENENCY_CONSTANT = 5; /* the enforced seat limit is the license seat limit PLUS this value */
    private static final String LIENENCY_GIFT_FILE = System.getProperty("uvm.conf.dir")
            + "/gift"; /* the file that defines the gift value */
    private static final int LIENENCY_GIFT = getLienencyGift(); /* and extra lienency constant */

    public static final String DIRECTORY_CONNECTOR_OLDNAME = "adconnector";
    public static final String BANDWIDTH_CONTROL_OLDNAME = "bandwidth";
    public static final String CONFIGURATION_BACKUP_OLDNAME = "boxbackup";
    public static final String BRANDING_MANAGER_OLDNAME = "branding";
    public static final String VIRUS_BLOCKER_OLDNAME = "virusblocker";
    public static final String SPAM_BLOCKER_OLDNAME = "spamblocker";
    public static final String WAN_FAILOVER_OLDNAME = "faild";
    public static final String IPSEC_VPN_OLDNAME = "ipsec";
    public static final String POLICY_MANAGER_OLDNAME = "policy";
    public static final String WEB_FILTER_OLDNAME = "sitefilter";
    public static final String WAN_BALANCER_OLDNAME = "splitd";
    public static final String WEB_CACHE_OLDNAME = "webcache";
    public static final String APPLICATION_CONTROL_OLDNAME = "classd";
    public static final String SSL_INSPECTOR_OLDNAME = "https";
    public static final String LIVE_SUPPORT_OLDNAME = "support";

    private static final String EXPIRED = "expired";

    /**
     * update every 4 hours, leaves an hour window
     */
    private static final long TIMER_DELAY = 1000 * 60 * 60 * 4;

    private static final Logger logger = Logger.getLogger(LicenseManagerImpl.class);

    private final PipelineConnector[] connectors = new PipelineConnector[] {};

    /**
     * Map from the product name to the latest valid license available for this product
     * This is where the fully evaluated license are stored
     * This map stores the evaluated (validated) licenses
     */
    private ConcurrentHashMap<String, License> licenseMap = new ConcurrentHashMap<String, License>();

    /**
     * A list of all known licenses
     * These are fully evaluated (validated) license
     */
    private List<License> licenseList = new LinkedList<License>();

    /**
     * The current settings
     * Contains a list of all known licenses store locally
     * Note: the licenses in the settings don't have metadata
     */
    private LicenseSettings settings;

    /**
     * Sync task
     */
    private final LicenseSyncTask task = new LicenseSyncTask();

    /**
     * Pulse that syncs the license, this is a daemon task.
     */
    private Pulse pulse = null;

    /**
     * Setup license manager application.
     * 
     * * Launch the synchronization task.
     *
     * @param appSettings       License manager application settings.
     * @param appProperties     Licese manager application properties
     */
    public LicenseManagerImpl(com.untangle.uvm.app.AppSettings appSettings,
            com.untangle.uvm.app.AppProperties appProperties) {
        super(appSettings, appProperties);

        this._readLicenses();
        this._mapLicenses();

        this.pulse = new Pulse("uvm-license", task, TIMER_DELAY);
        this.pulse.start();
    }

    /**
     * Pre license manager start.
     * Reload the licenses.
     *
     * @param isPermanentTransition
     *  If true, the app is permenant
     */
    @Override
    protected void postStart(boolean isPermanentTransition) {
        logger.debug("postStart()");

        /* Reload the licenses */
        UvmContextFactory.context().licenseManager().reloadLicenses(false);
    }

    /**
     * Get the pineliene connector(???)
     *
     * @return PipelineConector
     */
    @Override
    protected PipelineConnector[] getConnectors() {
        return this.connectors;
    }

    /**
     * Reload all of the licenses from the file system.
     *
     * @param
     *     blocking If true, block the current context until we're finished.  Otherwise, launch a new non-blocking thread.
     */
    @Override
    public final void reloadLicenses(boolean blocking) {
        if (blocking) {
            try {
                _syncLicensesWithServer();
            } catch (Exception ex) {
                logger.warn("Unable to reload the licenses.", ex);
            }
        } else {
            Thread t = new Thread(new Runnable() {
                /**
                 * Launch the license synchronize routine.
                 */
                public void run() {
                    try {
                        _syncLicensesWithServer();
                    } catch (Exception ex) {
                        logger.warn("Unable to reload the licenses.", ex);
                    }
                }
            });
            t.run();
        }
    }

    /**
     * From the existing license map, return the first matching string identifier (e.g.,"virus")
     * 
     * @param  identifier Identifier to find.
     * @param  exactMatch If true, identifier must match license name excactly.  Otherwise, return the first license that begins with the identifier.
     * @return
     *     Matching license.  Return an invalid license if not found.
     */
    @Override
    public final License getLicense(String identifier, boolean exactMatch) {
        if (isGPLApp(identifier))
            return null;

        /**
         * Check the correct name first,
         * If the license exists and is valid use that one
         */
        License license = null;

        /**
         * If there is no perfect match,
         * Look for one that the prefix matches
         * example: identifer "virus-blocker" should accept "virus-blocker-cloud"
         */
        if (!exactMatch) {
            for (String name : this.licenseMap.keySet()) {
                if (name.startsWith(identifier)) {
                    logger.debug("getLicense(" + identifier + ") = " + license);
                    license = this.licenseMap.get(name);
                    if (license != null && license.getValid())
                        return license;
                }
            }
        }

        /**
         * Look for an existing perfect match
         */
        license = this.licenseMap.get(identifier);
        if (license != null)
            return license;

        /**
         * Special for development environment
         * Assume all licenses are valid
         * This should be removed if you want to test the licensing in the dev environment
         */
        if (UvmContextFactory.context().isDevel()) {
            logger.warn("Creating development license: " + identifier);
            license = new License(identifier, "0000-0000-0000-0000", identifier, "Development", 0, 9999999999l,
                    "development", 1, Boolean.TRUE, "Developer");
            this.licenseMap.put(identifier, license);
            return license;
        }

        logger.warn("No license found for: " + identifier);

        /**
         * This returns an invalid license for all other requests
         * Note: this includes the free apps, however they don't actually check the license so it won't effect behavior
         * The UI will request the license of all app (including free)
         */
        license = new License(identifier, "0000-0000-0000-0000", identifier, "Subscription", 0, 0, "invalid", 1,
                Boolean.FALSE, I18nUtil.marktr("No License Found"));
        this.licenseMap.put(identifier, license); /* add it to the map for faster response next time */
        return license;
    }

    /**
     * Return the license for the exactly matching identifier.
     * 
     * @param  identifier Application name to find.
     * @return            License of matching identifier or an invalid license if not found.
     */
    @Override
    public final License getLicense(String identifier) {
        return getLicense(identifier, true);
    }

    /**
     * For the specify identifier, determine if license is valid.
     * 
     * @param  identifier Application name to find.
     * @return            true if license is valid, false otherwise.
     */
    @Override
    public final boolean isLicenseValid(String identifier) {
        if (isGPLApp(identifier))
            return true;

        License lic = getLicense(identifier);
        if (lic == null)
            return false;
        Boolean isValid = lic.getValid();
        if (isValid == null)
            return false;
        else
            return isValid;
    }

    /**
     * Get list of all licenses.
     * 
     * @return License List.
     */
    @Override
    public final List<License> getLicenses() {
        return this.licenseList;
    }

    /**
     * Determine if product has premium license.
     * 
     * @return true if at least one license is valid.
     */
    @Override
    public final boolean hasPremiumLicense() {
        return validLicenseCount() > 0;
    }

    /**
     * Determine number of valid licenses.
     * 
     * @return count of valid licenses.
     */
    @Override
    public int validLicenseCount() {
        int validCount = 0;
        for (License lic : this.settings.getLicenses()) {
            try {
                if (lic.getValid())
                    validCount++;
            } catch (Exception e) {
                logger.warn("Exception", e);
            }
        }
        return validCount;
    }

    /**
     * Return the lowest valid license seat.
     * 
     * @return Number of seats.
     */
    @Override
    public int getSeatLimit() {
        return getSeatLimit(true);
    }

    /**
     * Calculate the lowest license seat for valid subscriptions.
     * 
     * @param  lienency If non-zero seats, calculate seats based on a value higher than actual.  Otherwise, use strict seat value. 
     * @return          Calculated seat number.
     */
    @Override
    public int getSeatLimit(boolean lienency) {
        if (UvmContextFactory.context().isDevel())
            return -1;

        int seats = -1;
        for (License lic : this.settings.getLicenses()) {
            if (!lic.getValid() || lic.getTrial()) // only count valid non-trials
                continue;
            if (lic.getSeats() == null)
                continue;
            if (lic.getSeats() <= 0) //ignore invalid seat ranges
                continue;
            if (lic.getSeats() > seats && seats > 0) //if there is already a lower count limit, ignore this one
                continue;
            seats = lic.getSeats();
        }

        if (seats > 0 && lienency)
            seats = ((int) Math.round(((double) seats) * LIENENCY_PERCENT)) + LIENENCY_CONSTANT + LIENENCY_GIFT;

        return seats;
    }

    /**
     * For the specified applicaton, attempt to get a trial license.
     * 
     * @param  appName   Application name to request.
     * @throws Exception Throw excepton based on inability or general errors conecting license server.
     */
    @Override
    public void requestTrialLicense(String appName) throws Exception {
        if (appName == null) {
            logger.warn("Invalid name: " + appName);
            return;
        }
        // if already have a valid license, just return
        if (UvmContextFactory.context().licenseManager().isLicenseValid(appName)) {
            logger.warn("Already have a valid license for: " + appName);
            return;
        }

        String licenseUrl = System.getProperty("uvm.license.url");
        if (licenseUrl == null)
            licenseUrl = "https://license.untangle.com/license.php";

        /**
         * The API specifies libitem, however libitems no longer exist
         * First we try with the actual name, then the libitem time
         * Then we try with the old app name (if it has an old name), then we try with the old libitem name
         * We do all these different calls so that the product supports any version of the license server
         */
        String libitemName = "untangle-libitem-" + appName;
        String urlStr = licenseUrl + "?action=startTrial" + "&node=" + appName + "&" + getServerParams();
        String urlStr2 = licenseUrl + "?action=startTrial" + "&libitem=" + libitemName + "&" + getServerParams();

        String oldName = null;
        String urlStr3 = null;
        String urlStr4 = null;

        switch (appName) {
        case License.DIRECTORY_CONNECTOR:
            oldName = DIRECTORY_CONNECTOR_OLDNAME;
            break;
        case License.BANDWIDTH_CONTROL:
            oldName = BANDWIDTH_CONTROL_OLDNAME;
            break;
        case License.CONFIGURATION_BACKUP:
            oldName = CONFIGURATION_BACKUP_OLDNAME;
            break;
        case License.BRANDING_MANAGER:
            oldName = BRANDING_MANAGER_OLDNAME;
            break;
        case License.VIRUS_BLOCKER:
            oldName = VIRUS_BLOCKER_OLDNAME;
            break;
        case License.SPAM_BLOCKER:
            oldName = SPAM_BLOCKER_OLDNAME;
            break;
        case License.WAN_FAILOVER:
            oldName = WAN_FAILOVER_OLDNAME;
            break;
        case License.IPSEC_VPN:
            oldName = IPSEC_VPN_OLDNAME;
            break;
        case License.POLICY_MANAGER:
            oldName = POLICY_MANAGER_OLDNAME;
            break;
        case License.WEB_FILTER:
            oldName = WEB_FILTER_OLDNAME;
            break;
        case License.WAN_BALANCER:
            oldName = WAN_BALANCER_OLDNAME;
            break;
        case License.WEB_CACHE:
            oldName = WEB_CACHE_OLDNAME;
            break;
        case License.APPLICATION_CONTROL:
            oldName = APPLICATION_CONTROL_OLDNAME;
            break;
        case License.SSL_INSPECTOR:
            oldName = SSL_INSPECTOR_OLDNAME;
            break;
        case License.LIVE_SUPPORT:
            oldName = LIVE_SUPPORT_OLDNAME;
            break;
        }
        if (oldName != null) {
            String oldLibitemName = "untangle-libitem-" + oldName;
            urlStr3 = licenseUrl + "?action=startTrial" + "&node=" + oldName + "&" + getServerParams();
            urlStr4 = licenseUrl + "?action=startTrial" + "&libitem=" + oldLibitemName + "&" + getServerParams();
        }

        CloseableHttpClient httpClient = HttpClients.custom().build();
        CloseableHttpResponse response = null;
        HttpGet get;
        URL url;

        try {
            logger.info("Requesting Trial: " + urlStr);
            url = new URL(urlStr);
            get = new HttpGet(url.toString());
            response = httpClient.execute(get);
            if (response != null) {
                response.close();
                response = null;
            }

            if (urlStr2 != null) {
                logger.info("Requesting Trial: " + urlStr2);
                url = new URL(urlStr2);
                get = new HttpGet(url.toString());
                response = httpClient.execute(get);
                if (response != null) {
                    response.close();
                    response = null;
                }
            }

            if (urlStr3 != null) {
                logger.info("Requesting Trial: " + urlStr3);
                url = new URL(urlStr3);
                get = new HttpGet(url.toString());
                response = httpClient.execute(get);
                if (response != null) {
                    response.close();
                    response = null;
                }
            }

            if (urlStr4 != null) {
                logger.info("Requesting Trial: " + urlStr4);
                url = new URL(urlStr4);
                get = new HttpGet(url.toString());
                response = httpClient.execute(get);
                if (response != null) {
                    response.close();
                    response = null;
                }
            }
        } catch (java.net.UnknownHostException e) {
            logger.warn("Exception requesting trial license:" + e.toString());
            throw (new Exception("Unable to fetch trial license: DNS lookup failed.", e));
        } catch (java.net.ConnectException e) {
            logger.warn("Exception requesting trial license:" + e.toString());
            throw (new Exception("Unable to fetch trial license: Connection timeout.", e));
        } catch (Exception e) {
            logger.warn("Exception requesting trial license:" + e.toString());
            throw (new Exception("Unable to fetch trial license: " + e.toString(), e));
        } finally {
            try {
                if (response != null)
                    response.close();
            } catch (Exception e) {
                logger.warn("close", e);
            }
            try {
                httpClient.close();
            } catch (Exception e) {
                logger.warn("close", e);
            }
        }

        // blocking call because we need the new trial license
        UvmContextFactory.context().licenseManager().reloadLicenses(true);
    }

    /**
     * Not used.  Use methods in uvm class.
     * 
     * @return null
     */
    public Object getSettings() {
        /* These are controlled using the methods in the uvm class */
        return null;
    }

    /**
     * Not used.  Use methods in uvm class.
     * @param settings null
     */
    public void setSettings(Object settings) {
        /* These are controlled using the methods in the uvm class */
    }

    /**
     * Initialize the settings
     * (By default there are no liceneses)
     */
    private void _initializeSettings() {
        logger.info("Initializing Settings...");

        List<License> licenses = new LinkedList<License>();
        this.settings = new LicenseSettings(licenses);

        this._saveSettings(this.settings);
    }

    /**
     * Read the licenses and load them into the current settings object
     */
    private synchronized void _readLicenses() {
        SettingsManager settingsManager = UvmContextFactory.context().settingsManager();

        try {
            this.settings = settingsManager.load(LicenseSettings.class,
                    System.getProperty("uvm.conf.dir") + "/licenses/licenses.js");
        } catch (SettingsManager.SettingsException e) {
            logger.error("Unable to read license file: ", e);
        }

        if (this.settings == null)
            _initializeSettings();

        if (this.settings.getLicenses() != null) {
            Iterator<License> iterator = this.settings.getLicenses().iterator();
            while (iterator.hasNext()) {
                License license = iterator.next();

                // remove obsolete names
                if (isObsoleteApp(license.getName()))
                    iterator.remove();

                // recompute metadata - we don't want to use value in file (could have been changed)
                _setValidAndStatus(license);
            }
        }

        return;
    }

    /**
     * This gets all the current revocations from the license server for this UID
     * and removes any licenses that have been revoked
     */
    @SuppressWarnings("unchecked") //LinkedList<LicenseRevocation> <-> LinkedList
    private synchronized void _checkRevocations() {
        SettingsManager settingsManager = UvmContextFactory.context().settingsManager();
        LinkedList<LicenseRevocation> revocations;
        boolean changed = false;

        logger.info("REFRESH: Checking Revocations...");

        try {
            String urlStr = _getLicenseUrl() + "?" + "action=getRevocations" + "&" + getServerParams();
            logger.info("Downloading: \"" + urlStr + "\"");

            Object o = settingsManager.loadUrl(LinkedList.class, urlStr);
            revocations = (LinkedList<LicenseRevocation>) o;
        } catch (SettingsManager.SettingsException e) {
            logger.error("Unable to read license file: ", e);
            return;
        } catch (ClassCastException e) {
            logger.error("getRevocations returned unexpected response", e);
            return;
        }

        for (LicenseRevocation revoke : revocations) {
            changed |= _revokeLicense(revoke);
        }

        if (changed)
            _saveSettings(settings);

        logger.info("REFRESH: Checking Revocations... done (modified: " + changed + ")");

        return;
    }

    /** 
     * This remove a license from the list of current licenses
     *
     * @param revoke LicenseRevocation object to revoke in licenses.
     * @return true if a license was removed, false otherwise
     */
    private synchronized boolean _revokeLicense(LicenseRevocation revoke) {
        if (this.settings == null || this.settings.getLicenses() == null) {
            logger.error("Invalid settings:" + this.settings);
            return false;
        }
        if (revoke == null) {
            logger.error("Invalid argument:" + revoke);
            return false;
        }
        if (revoke.getName() == null) {
            logger.error("Invalid name:" + revoke.getName());
            return false;
        }

        /**
         * See if you find a match in the current licenses
         * If so, remove it
         */
        Iterator<License> itr = this.settings.getLicenses().iterator();
        while (itr.hasNext()) {
            License existingLicense = itr.next();

            if (revoke.getName().equals(existingLicense.getName())) {
                logger.warn("Revoking License: " + revoke.getName());
                itr.remove();
                return true;
            }
        }

        return false;
    }

    /**
     * This downloads a list of current licenese from the license server
     * Any new licenses are added. Duplicate licenses are updated if the new one grants better privleges
     */
    @SuppressWarnings("unchecked") //LinkedList<License> <-> LinkedList
    private synchronized void _downloadLicenses() {
        SettingsManager settingsManager = UvmContextFactory.context().settingsManager();
        LinkedList<License> licenses;
        boolean changed = false;

        logger.info("REFRESH: Downloading new Licenses...");

        try {
            String urlStr = _getLicenseUrl() + "?" + "action=getLicenses" + "&" + getServerParams();
            logger.info("Downloading: \"" + urlStr + "\"");

            Object o = settingsManager.loadUrl(LinkedList.class, urlStr);
            licenses = (LinkedList<License>) o;
        } catch (SettingsManager.SettingsException e) {
            logger.error("Unable to read license file: ", e);
            return;
        } catch (ClassCastException e) {
            logger.error("getRevocations returned unexpected response", e);
            return;
        }

        for (License lic : licenses) {
            if (!isObsoleteApp(lic.getName())) {
                changed |= _insertOrUpdate(lic);
            }
        }

        if (changed)
            _saveSettings(settings);

        logger.info("REFRESH: Downloading new Licenses... done (changed: " + changed + ")");

        return;
    }

    /**
     * This takes the passed argument and inserts it into the current licenses
     * If there is currently an existing license for that product it will be removed
     *
     * @param license License object to add.
     * @return true if a license was added or modified, false otherwise
     */
    private synchronized boolean _insertOrUpdate(License license) {
        boolean insertNewLicense = true;

        if (this.settings == null || this.settings.getLicenses() == null) {
            logger.error("Invalid settings:" + this.settings);
            return false;
        }
        if (license == null) {
            logger.error("Invalid argument:" + license);
            return false;
        }

        /**
         * See if you find a match in the current licenses
         * If so, the new one replaces it so remove the existing one
         */
        Iterator<License> itr = this.settings.getLicenses().iterator();
        while (itr.hasNext()) {
            try {
                License existingLicense = itr.next();
                if (existingLicense.getName().equals(license.getName())) {

                    /**
                     * As a measure of safety we only replace an existing license under certain circumstances
                     * This is so we are careful to only increase entitlements during this phase
                     */
                    boolean replaceLicense = false;
                    insertNewLicense = false;

                    /**
                     * Check the validity of the current license
                     * If it isn't valid, we might as well try the new one
                     * Note: we have to use getLicenses to do this because the settings don't store validity
                     */
                    if ((getLicense(existingLicense.getName()) != null)
                            && !(getLicense(existingLicense.getName()).getValid())) {
                        logger.info("REFRESH: Replacing license " + license + " - old one is invalid");
                        replaceLicense = true;
                    }

                    /**
                     * If the current one is a trial, and the new one is not, use the new one
                     */
                    if (!(License.LICENSE_TYPE_TRIAL.equals(license.getType()))
                            && License.LICENSE_TYPE_TRIAL.equals(existingLicense.getType())) {
                        logger.info("REFRESH: Replacing license " + license + " - old one is trial");
                        replaceLicense = true;
                    }

                    /**
                     * If the new one has a later end date, use the new one
                     */
                    if (license.getEnd() > existingLicense.getEnd()) {
                        logger.info("REFRESH: Replacing license " + license + " - new one has later end date");
                        replaceLicense = true;
                    }

                    /**
                     * If the new one has a different seat amount
                     */
                    if (license.getSeats() != null && existingLicense.getSeats() == null) {
                        logger.info("REFRESH: Replacing license " + license + " - number of seats now specified");
                        replaceLicense = true;
                    }
                    if (license.getSeats() != null && existingLicense.getSeats() != null
                            && license.getSeats() > existingLicense.getSeats()) {
                        logger.info("REFRESH: Replacing license " + license + " - new one has more seats");
                        replaceLicense = true;
                    }

                    if (replaceLicense) {
                        itr.remove();
                        insertNewLicense = true;
                    } else {
                        logger.info("REFRESH: Keeping current license: " + license);
                    }
                }
            } catch (Exception e) {
                logger.warn("Exception processing existing license.", e);
            }
        }

        /**
         * if a match hasnt been found it needs to be added
         */
        if (insertNewLicense) {
            logger.info("REFRESH: Inserting new license   : " + license);
            List<License> licenses = this.settings.getLicenses();
            licenses.add(license);
            return true;
        }

        return false;
    }

    /**
     * update the app to License Map
     */
    private synchronized void _mapLicenses() {
        /* Create a new map of all of the valid licenses */
        ConcurrentHashMap<String, License> newMap = new ConcurrentHashMap<String, License>();
        LinkedList<License> newList = new LinkedList<License>();
        License license = null;

        if (this.settings != null) {
            for (License lic : this.settings.getLicenses()) {
                try {
                    /**
                     * Create a duplicate - we're about to fill in metadata
                     * But we don't want to mess with the original
                     */
                    license = new License(lic);

                    /**
                     * Complete Meta-data
                     */
                    _setValidAndStatus(license);

                    String identifier = license.getCurrentName();
                    if (identifier == null) {
                        logger.warn("Ignoring license with no name: " + license);
                        continue;
                    }

                    License current = newMap.get(identifier);

                    /* current license is newer and better */
                    if ((current != null) && (current.getEnd() > license.getEnd()))
                        continue;

                    logger.info("Adding License: " + license.getCurrentName() + " to Map. (valid: "
                            + license.getValid() + ")");

                    newMap.put(identifier, license);
                    newList.add(license);
                } catch (Exception e) {
                    logger.warn("Failed to load license: " + license, e);
                }
            }
        }

        this.licenseMap = newMap;
        this.licenseList = newList;
    }

    /**
     * Verify the validity of a license
     *
     * @param license License object for a subscription.
     * @return true if license is valid, false otherwise.
     */
    private boolean _isLicenseValid(License license) {
        long now = (System.currentTimeMillis() / 1000);

        /* check if the license hasn't started yet (start date in future) */
        if (license.getStart() > now) {
            logger.warn("The license: " + license + " isn't valid yet (" + license.getStart() + " > " + now + ")");
            license.setStatus("Invalid (Start Date in Future)"); /* XXX i18n */
            return false;
        }

        /* check if it is already expired */
        if ((license.getEnd() < now)) {
            logger.warn("The license: " + license + " has expired (" + license.getEnd() + " < " + now + ")");
            license.setStatus("Invalid (Expired)"); /* XXX i18n */
            return false;
        }

        /* check the UID */
        if (license.getUID() == null || !license.getUID().equals(UvmContextFactory.context().getServerUID())) {
            logger.warn("The license: " + license + " does not match this server's UID (" + license.getUID()
                    + " != " + UvmContextFactory.context().getServerUID() + ")");
            license.setStatus("Invalid (UID Mismatch)"); /* XXX i18n */
            return false;
        }

        String input = null;
        if (license.getKeyVersion() == 1) {
            input = license.getKeyVersion() + license.getUID() + license.getName() + license.getType()
                    + license.getStart() + license.getEnd() + "the meaning of life is 42";
        } else if (license.getKeyVersion() == 3) {
            input = license.getKeyVersion() + license.getUID() + license.getName() + license.getType()
                    + license.getStart() + license.getEnd() + nullToEmptyStr(license.getSeats())
                    + "the meaning of life is 42";
        } else {
            // versions v1,v3 are supported. v2 is for ICC
            // any other version is unknown
            license.setStatus("Invalid (Invalid Key Version)");
            return false;
        }
        //logger.info("KEY Input: " + input);

        MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (java.security.NoSuchAlgorithmException e) {
            logger.warn("Unknown Algorith MD5", e);
            license.setStatus("Invalid (Invalid Algorithm)");
            return false;
        }
        byte[] digest = md.digest(input.getBytes());
        String output = _toHex(digest);
        //logger.info("KEY Output: " + output);
        //logger.info("KEY Expect: " + license.getKey());

        if (!license.getKey().equals(output)) {
            logger.warn("Invalid key: " + output);
            license.setStatus("Invalid (Invalid Key)");
            return false;
        }

        logger.debug("License " + license + " is valid.");
        return true;
    }

    /**
     * Convert the bytes to a hex string
     *
     * @param data Array of bytes to convert to a hext string.
     * @return String of hex values for the passed byte array.
     */
    private String _toHex(byte data[]) {
        String response = "";
        for (byte b : data) {
            int c = b;
            if (c < 0)
                c = c + 0x100;
            response += String.format("%02x", c);
        }

        return response;
    }

    /**
     * Set the current settings to new Settings
     * Also save the settings to disk if save is true
     *
     * @param newSettings LicenseSetttings to save.
     */
    private void _saveSettings(LicenseSettings newSettings) {
        /**
         * Compute metadata before saving
         */
        Iterator<License> itr = this.settings.getLicenses().iterator();
        while (itr.hasNext()) {
            License license = itr.next();
            _setValidAndStatus(license);
            if (license.getValid() != null && !license.getValid()) {
                logger.warn("Removing invalid license from list: " + license);
                itr.remove();
            }
        }

        /**
         * Save the settings
         */
        SettingsManager settingsManager = UvmContextFactory.context().settingsManager();
        try {
            settingsManager.save(System.getProperty("uvm.conf.dir") + "/licenses/licenses.js", newSettings);
        } catch (SettingsManager.SettingsException e) {
            logger.warn("Failed to save settings.", e);
            return;
        }

        /**
         * Change current settings
         */
        this.settings = newSettings;

        /**
         * Licenses are only saved when changed - call license changed hook
         */
        UvmContextFactory.context().hookManager().callCallbacks(HookManager.LICENSE_CHANGE, 1);
    }

    /**
     * Returns an estimate of # devices on the network
     * This is not meant to be very accurate - it is just an estimate
     *
     * @return Number of estimated devices on the network.
     */
    private int _getEstimatedNumDevices() {
        return UvmContextFactory.context().hostTable().getCurrentActiveSize();
    }

    /**
     * Returns the url for the license server API
     *
     * @return license agreement.
     */
    private String _getLicenseUrl() {
        String urlStr = System.getProperty(LICENSE_URL_PROPERTY);

        if (urlStr == null)
            urlStr = DEFAULT_LICENSE_URL;

        return urlStr;
    }

    /**
     * syncs the license server state with local state
     */
    private void _syncLicensesWithServer() {
        logger.info("Reloading licenses...");

        synchronized (LicenseManagerImpl.this) {
            _readLicenses();

            if (!UvmContextFactory.context().isDevel()) {
                _downloadLicenses();
                _checkRevocations();
            }

            _mapLicenses();
        }

        logger.info("Reloading licenses... done");
    }

    /**
     * Task to run the synchronoization routine. 
     */
    private class LicenseSyncTask implements Runnable {
        /**
         * Launch the license synchronize routine.
         */
        public void run() {
            _syncLicensesWithServer();
        }
    }

    /**
     * Determine if application is GPL-based.
     * 
     * @param  identifier Application name.
     * @return            true if GPL, false otherwise.
     */
    private boolean isGPLApp(String identifier) {
        switch (identifier) {
        case "untangle-node-ad-blocker":
            return true;
        case "ad-blocker":
            return true;
        case "untangle-node-virus-blocker-lite":
            return true;
        case "virus-blocker-lite":
            return true;
        case "untangle-node-captive-portal":
            return true;
        case "captive-portal":
            return true;
        case "untangle-node-firewall":
            return true;
        case "firewall":
            return true;
        case "untangle-node-intrusion-prevention":
            return true;
        case "intrusion-prevention":
            return true;
        case "untangle-node-openvpn":
            return true;
        case "openvpn":
            return true;
        case "untangle-node-phish-blocker":
            return true;
        case "phish-blocker":
            return true;
        case "untangle-node-application-control-lite":
            return true;
        case "application-control-lite":
            return true;
        case "untangle-node-router":
            return true;
        case "router":
            return true;
        case "untangle-node-reports":
            return true;
        case "reports":
            return true;
        case "untangle-node-shield":
            return true;
        case "shield":
            return true;
        case "untangle-node-spam-blocker-lite":
            return true;
        case "spam-blocker-lite":
            return true;
        case "untangle-node-web-monitor":
            return true;
        case "web-monitor":
            return true;
        case "untangle-node-license":
            return true;
        case "license":
            return true;
        case "untangle-casing-http":
            return true;
        case "http":
            return true;
        case "untangle-casing-ftp":
            return true;
        case "ftp":
            return true;
        case "untangle-casing-smtp":
            return true;
        case "smtp":
            return true;
        case "tunnel-vpn":
            return true;
        default:
            return false;
        }
    }

    /**
     * Determine if application is obsolete.
     * 
     * @param  identifier Application name.
     * @return            true if applicatio is obsolete, false otherwise.
     */
    private boolean isObsoleteApp(String identifier) {
        if ("untangle-node-kav".equals(identifier))
            return true;
        if ("kav".equals(identifier))
            return true;
        if ("untangle-node-commtouch".equals(identifier))
            return true;
        if ("commtouch".equals(identifier))
            return true;
        if ("untangle-node-commtouchav".equals(identifier))
            return true;
        if ("commtouchav".equals(identifier))
            return true;
        if ("untangle-node-commtouchas".equals(identifier))
            return true;
        if ("commtouchas".equals(identifier))
            return true;
        return false;
    }

    /**
     * Set a license to valid.
     * 
     * @param license License object to set.
     */
    private void _setValidAndStatus(License license) {
        if (_isLicenseValid(license)) {
            license.setValid(Boolean.TRUE);
            license.setStatus("Valid");
        } else {
            license.setValid(Boolean.FALSE);
        }
    }

    /**
     * If passed value is null, return an empty string.  Otherwise return the object's string value.
     * 
     * @param  foo Passed value.
     * @return     String of the value.
     */
    private static String nullToEmptyStr(Object foo) {
        if (foo == null)
            return "";
        else
            return foo.toString();
    }

    /**
     * Get the lienency gift value.
     * 
     * @return Number of lienency seats.
     */
    private static int getLienencyGift() {
        BufferedReader reader = null;
        int returnValue = 0;
        try {
            File giftFile = new File(LIENENCY_GIFT_FILE);
            if (!giftFile.exists())
                return 0;

            reader = new BufferedReader(new FileReader(giftFile));
            Integer i = Integer.parseInt(reader.readLine());
            if (i != null)
                returnValue = 0;
            else
                returnValue = i;

        } catch (Exception x) {
            logger.warn("Exception", x);
            returnValue = 0;
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception x) {
                    logger.warn("Exception", x);
                }
            }
        }

        return returnValue;
    }

    /**
     * Get the URL encoded parameters to describe this server
     * @return string
     */
    private String getServerParams() {
        int numDevices = _getEstimatedNumDevices();
        String model = UvmContextFactory.context().getApplianceModel();
        String uvmVersion = UvmContextFactory.context().version();
        if (model != null) {
            try {
                model = URLEncoder.encode(model, "UTF-8");
            } catch (Exception e) {
                logger.warn("Failed to encode", e);
                model = null;
            }
        }
        return "uid=" + UvmContextFactory.context().getServerUID() + "&appliance="
                + UvmContextFactory.context().isAppliance() + (model != null ? "&appliance-model=" + model : "")
                + "&numDevices=" + numDevices + "&version=" + uvmVersion;
    }
}