DirectoryEntry.java

package emissary.directory;

import emissary.core.Namespace;
import emissary.core.NamespaceException;
import emissary.place.IServiceProviderPlace;
import emissary.util.xml.JDOMUtil;

import org.jdom2.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import javax.annotation.Nullable;

import static emissary.directory.KeyManipulator.CLASSSEPARATOR;
import static emissary.directory.KeyManipulator.DOLLAR;

/**
 * This class is container object for storing directory entry information such as the service name and type, the
 * location, the cost and quality and description of a place.
 *
 * The implementation is not synchronized and is not suitable for multiple threads that will be changing the contents
 * unless external synchronization is used.
 */
public class DirectoryEntry implements Serializable {

    // Serializable
    static final long serialVersionUID = 2629953887545857011L;

    /** The key for this entry as a string */
    protected String theKey;

    /** The cost of this entry */
    protected int theCost = 0;

    /** The quality of this entry */
    protected int theQuality = 100;

    /**
     * The expense is computed from the cost and quality by {@link #calculateExpense}
     */
    protected int theExpense = 0;

    /** Protection against repeated namespace lookups for this entry */
    protected transient boolean lookupAttempted = false;

    /** Cached reference to the place instance if local */
    @Nullable
    protected transient IServiceProviderPlace localPlace = null;

    /** The data type from the key */
    protected String dataType;

    /** The DataType::ServiceType from the key */
    protected String dataId;

    /** THe service name from the key */
    protected String serviceName;

    /** The service type from the key */
    protected String serviceType;

    /** THe service location from the key */
    protected String serviceLocation;

    /** The service URL from the key */
    protected String serviceHostUrl;

    /** Logger instance */
    protected static final Logger logger = LoggerFactory.getLogger(DirectoryEntry.class);

    /** The description field for this entry */
    @Nullable
    protected String description;

    /** Age of this entry */
    protected transient long age = System.currentTimeMillis();

    /** Xml name of entry element value is {@value} */
    public static final String ENTRY = "entry";

    /** Xml name of key element value is {@value} */
    public static final String KEY = "key";

    /** Xml name of description element value is {@value} */
    public static final String DESC = "description";

    /** Xml name of cost element value is {@value} */
    public static final String COST = "cost";

    /** Xml name of quality element value is {@value} */
    public static final String QUALITY = "quality";

    /** Xml name of expense element value is {@value} */
    public static final String EXPENSE = "expense";

    /** Value of PRESERVE_TIME flag */
    public static final boolean PRESERVE_TIME = true;

    /**
     * Constructor which create a new directory entry and new directoryInfo object.
     * 
     * @param key four-tuple key of the place
     * @param description from config file
     * @param cost from config file
     * @param quality from config file
     */
    public DirectoryEntry(final String key, final String description, final int cost, final int quality) {
        if (logger.isDebugEnabled()) {
            logger.debug("Directory entry: {},{},{},\"{}\"", key, cost, quality, description);
        }
        setKey(key);
        this.theQuality = quality;
        this.theCost = cost;
        calculateExpense();

        this.description = description;
    }

    /**
     * Create an entry from nothing but a key
     * 
     * @param key the key to use
     */
    public DirectoryEntry(final String key) {
        setKey(key);
    }

    /**
     * Make an entry from parts, specifying expense
     * 
     * @param dataType the first part of the key
     * @param serviceName the second part of the key
     * @param serviceType the third part of the key
     * @param serviceLocation the fourth part of the key
     * @param description the description
     * @param expense the expense
     */
    public DirectoryEntry(final String dataType, final String serviceName, final String serviceType, final String serviceLocation,
            final String description, final int expense) {
        this.dataType = dataType;
        this.serviceName = serviceName;
        this.serviceType = serviceType;
        this.serviceLocation = serviceLocation;
        this.description = description;
        setCqeFromExp(expense);
        buildKey();
    }

    /**
     * Make an entry from parts, specifing cost and quality
     * 
     * @param dataType the first part of the key
     * @param serviceName the second part of the key
     * @param serviceType the third part of the key
     * @param serviceLocation the fourth part of the key
     * @param description the description
     * @param cost the cost of the place
     * @param quality the quality of the place
     */
    public DirectoryEntry(final String dataType, final String serviceName, final String serviceType, final String serviceLocation,
            final String description, final int cost, final int quality) {
        this.dataType = dataType;
        this.serviceName = serviceName;
        this.serviceType = serviceType;
        this.serviceLocation = serviceLocation;
        this.description = description;
        this.theCost = cost;
        this.theQuality = quality;
        calculateExpense();
        buildKey();
    }

    /**
     * Make an entry from parts, default expense
     * 
     * @param dataType the first part of the key
     * @param serviceName the second part of the key
     * @param serviceType the third part of the key
     * @param serviceLocation the fourth part of the key
     * @param description the description
     */
    public DirectoryEntry(final String dataType, final String serviceName, final String serviceType, final String serviceLocation,
            final String description) {
        this(dataType, serviceName, serviceType, serviceLocation, description, 0, 0);
    }

    /**
     * Copy constructor
     * 
     * @param that the entry to copy
     */
    public DirectoryEntry(final DirectoryEntry that) {
        this(that, !PRESERVE_TIME);
    }

    /**
     * Copy constructor with time copy
     * 
     * @param that the entry to copy
     * @param preserveTime copy the time value also when true
     */
    DirectoryEntry(final DirectoryEntry that, final boolean preserveTime) {
        this.theKey = that.theKey;
        this.serviceType = that.serviceType;
        this.serviceName = that.serviceName;
        this.dataType = that.dataType;
        this.dataId = that.dataId;
        this.serviceLocation = that.serviceLocation;
        this.serviceHostUrl = that.serviceHostUrl;
        this.theQuality = that.theQuality;
        this.theCost = that.theCost;
        this.calculateExpense();
        this.description = that.description;
        if (preserveTime) {
            this.age = that.age;
        }
    }

    /**
     * Ensure this directory entry contains a valid key
     */
    public boolean isValid() {
        return KeyManipulator.isValid(this.theKey);
    }

    /**
     * Set the expense, cost and quality from the expense
     * 
     * @param expense the expense to use
     */
    protected void setCqeFromExp(final int expense) {
        if (expense >= 100) {
            final int invQual = expense % 100;
            this.theQuality = 100 - invQual;
            this.theCost = (expense - invQual) / 100;
        } else {
            // Should give expense of 0
            this.theCost = 0;
            this.theQuality = 100;
        }

        this.theExpense = expense;

    }

    /**
     * Get place description
     */
    public String getDescription() {
        return this.description;
    }

    /**
     * Returns the key
     */
    public String getKey() {
        return this.theKey;
    }

    /**
     * Return the full key with expense
     */
    public String getFullKey() {
        return this.theKey + DOLLAR + this.theExpense;
    }

    /**
     * Get quality of how well a place does its function
     */
    public int getQuality() {
        return this.theQuality;
    }

    /**
     * Set the quality associated with the entry and force the expese to be recalculated
     */
    public void setQuality(final int quality) {
        this.theQuality = quality;
        calculateExpense();
    }

    /**
     * Get and set cost of using system, ie. how fast the place processes data
     */
    public int getCost() {
        return this.theCost;
    }

    /**
     * Set the cost of processing at a place.
     * 
     * @param cost the new cost
     */
    public void setCost(final int cost) {
        this.theCost = cost;
        calculateExpense();
    }

    /**
     * Increment the cost of a place's processing
     * 
     * @param costIncrement the increment to add
     */
    public void addCost(final int costIncrement) {
        this.theCost += costIncrement;
        calculateExpense();
    }

    /**
     * Set key and precompute stuff we need
     * 
     * @see #buildKey
     * @param key the key
     */
    protected void setKey(final String key) {
        this.theKey = KeyManipulator.removeExpense(key);
        this.serviceType = KeyManipulator.getServiceType(this.theKey);
        this.serviceName = KeyManipulator.getServiceName(this.theKey);
        this.dataType = KeyManipulator.getDataType(key);
        this.dataId = this.dataType + KeyManipulator.DATAIDSEPARATOR + this.serviceType;
        this.serviceLocation = KeyManipulator.getServiceLocation(key);
        this.serviceHostUrl = KeyManipulator.getServiceHostUrl(key);
        final int exp = KeyManipulator.getExpense(key, -1);
        if (exp > -1) {
            setCqeFromExp(exp);
        }
    }

    /**
     * Set a new data type
     * 
     * @param dataType the new data type
     */
    public void setDataType(final String dataType) {
        this.dataType = dataType;
        buildKey();
    }

    /**
     * Set a new servicelocation
     * 
     * @param serviceLocation the new value
     */
    public void setServiceLocation(final String serviceLocation) {
        this.serviceLocation = serviceLocation;
        this.serviceHostUrl = serviceLocation.substring(0, serviceLocation.lastIndexOf(CLASSSEPARATOR) + 1);
        buildKey();
    }

    /**
     * Set a reference to the local place object
     * 
     * @param place the local reference
     */
    protected void setLocalPlace(final IServiceProviderPlace place) {
        this.localPlace = place;
    }

    /**
     * Get service name
     * 
     * @return service name from key
     */
    public String getDataType() {
        return this.dataType;
    }

    /**
     * Get dataId
     * 
     * @return dataId from key
     */
    public String getDataId() {
        return this.dataId;
    }

    /**
     * Get service location
     * 
     * @return service location from key
     */
    public String getServiceLocation() {
        return this.serviceLocation;
    }

    /**
     * Get service host url
     * 
     * @return service host url from key
     */
    public String getServiceHostUrl() {
        return this.serviceHostUrl;
    }

    /**
     * Get the reference to the local place
     * 
     * @return local place reference or null if not local
     */
    public IServiceProviderPlace getLocalPlace() {
        isLocal(); // force lookup if needed
        return this.localPlace;
    }

    /**
     * String rep for debug
     */
    @Override
    public String toString() {
        return getFullKey() + (this.description != null ? (" (" + this.description + ")") : "");
    }

    /**
     * Used to order directory entries. Returns true if this entry is better than the argument entry
     * 
     * @param that the entry to test
     * @return true if this is better than that
     */
    public boolean isBetterThan(final DirectoryEntry that) {
        return this.theExpense < that.getExpense();
    }

    /**
     * test if the current dataEntry matches the passed key pattern.
     */
    public boolean matches(final String pattern) {
        return matches(pattern.toCharArray());
    }

    /**
     * test if the current dataEntry matches the passed key pattern
     */
    public boolean matches(final char[] pattern) {
        return KeyManipulator.gmatch(this.theKey.toCharArray(), pattern);
    }


    /**
     * test if the current dataEntry matches the passed key pattern specifically ignoring cost in the incoming pattern (if
     * any)
     */
    public boolean matchesIgnoreCost(final String pattern) {
        return matches(KeyManipulator.removeExpense(pattern));
    }

    /**
     * Return the service type from the key of this entry
     */
    public String getServiceType() {
        return this.serviceType;
    }

    /**
     * Set a new service type
     */
    public void setServiceType(final String serviceType) {
        this.serviceType = serviceType;
        buildKey();
    }

    /**
     * Return the service name from the key of this entry
     */
    public String getServiceName() {
        return this.serviceName;
    }


    /**
     * Set a new service name
     */
    public void setServiceName(final String serviceName) {
        this.serviceName = serviceName;
        buildKey();
    }

    /**
     * Return the expense associated with this entry
     */
    public int getExpense() {
        return this.theExpense;
    }

    /**
     * Expense includes the cost and quality, cost is primary
     */
    protected void calculateExpense() {
        this.theExpense = calculateExpense(this.theCost, this.theQuality);
    }

    /**
     * Calculate an expense value from the cost and quality, cost is primary
     * 
     * @param cost the cost
     * @param quality the quality
     */
    public static int calculateExpense(final int cost, final int quality) {
        return (cost * 100) + (100 - quality);
    }

    /**
     * Keep the internal key parameters in sync when one of the setters is called
     * 
     * @see #setKey(String)
     */
    protected void buildKey() {
        this.theKey = KeyManipulator.makeKey(this.dataType, this.serviceName, this.serviceType, this.serviceLocation);
        this.dataId = this.dataType + KeyManipulator.DATAIDSEPARATOR + this.serviceType;
    }

    /**
     * Determine if local by looking up in namespace
     * 
     * @return true if local
     */
    public boolean isLocal() {
        if (!this.lookupAttempted) {
            try {
                setLocalPlace((IServiceProviderPlace) Namespace.lookup(this.serviceLocation));
            } catch (NamespaceException e) {
                // empty catch block
            }
            logger.debug("NS Lookup for locality on {}{}", this.serviceLocation, this.localPlace == null ? " failed" : " passed");

            this.lookupAttempted = true;
        }
        return this.localPlace != null;
    }

    /**
     * Change the key such that the place specified by proxyKey acts as a proxy for the current key. We keep the same data
     * type, service type, service name and expense but change the place to the proxy
     * 
     * @param proxyKey the replacement key
     */
    public void proxyFor(final String proxyKey) {
        final String newKey = KeyManipulator.makeProxyKey(this.theKey, proxyKey, this.theExpense);
        setKey(newKey);
    }

    /**
     * Show the creation date of this entry
     */
    public long getAge() {
        return this.age;
    }

    /**
     * Package access to the age value so that creation time can be preserved in some cases
     * 
     * @param age the age to be preserved
     */
    void preserveCopyAge(final long age) {
        this.age = age;
    }

    /**
     * Build an entry from the supplied xml fragment
     * 
     * @param e a JDOM Element
     */
    public static DirectoryEntry fromXml(final Element e) {
        final String key = e.getChildTextTrim(KEY);
        final String desc = e.getChildTextTrim(DESC);
        final int cost = JDOMUtil.getChildIntValue(e, COST);
        final int quality = JDOMUtil.getChildIntValue(e, QUALITY);
        return new DirectoryEntry(key, desc, cost, quality);
    }

    /**
     * Turn this entry into an xml fragment
     */
    public Element getXml() {
        final Element root = new Element(ENTRY);
        root.addContent(JDOMUtil.simpleElement(KEY, this.theKey));
        root.addContent(JDOMUtil.simpleElement(DESC, this.description));
        root.addContent(JDOMUtil.simpleElement(COST, this.theCost));
        root.addContent(JDOMUtil.simpleElement(QUALITY, this.theQuality));
        root.addContent(JDOMUtil.simpleElement(EXPENSE, this.theExpense));
        return root;
    }
}