net.pms.dlna.protocolinfo.ProtocolInfo.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.dlna.protocolinfo.ProtocolInfo.java

Source

/*
 * Universal Media Server, for streaming any media to DLNA
 * compatible renderers based on the http://www.ps3mediaserver.org.
 * Copyright (C) 2012 UMS developers.
 *
 * This program is a 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; version 2
 * of the License only.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package net.pms.dlna.protocolinfo;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import org.fourthline.cling.support.model.Protocol;
import org.fourthline.cling.support.model.dlna.DLNAAttribute;
import org.fourthline.cling.support.model.dlna.DLNAProfiles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.pms.dlna.protocolinfo.ProtocolInfoAttributeName.KnownProtocolInfoAttributeName;
import net.pms.util.ParseException;

/**
 * This immutable class represents a {@code protocolInfo} element.
 *
 * @author Nadahar
 */
public class ProtocolInfo implements Comparable<ProtocolInfo>, Serializable {

    private static final long serialVersionUID = 1L;
    private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolInfo.class);

    /** The wildcard character {@code "*"} */
    public static final String WILDCARD = "*";

    /** A static instance of an empty attribute map */
    public static final SortedMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> EMPTYMAP = Collections
            .unmodifiableSortedMap(createEmptyAttributesMap());

    /** The protocol (first field) of {@code protocolInfo} */
    protected final Protocol protocol;

    /** The network (second field) of {@code protocolInfo} */
    protected final String network;

    /** The contentType (third field) of {@code protocolInfo} */
    protected final MimeType mimeType;

    /** The {@code additionalInfo} (fourth field) of {@code protocolInfo} */
    protected final String additionalInfo;

    /** The attributes parsed from {@code additionalInfo}. */
    protected final SortedMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> attributes;

    /** The cached string representation of {@link #attributes}. */
    protected final String attributesString;

    /** The cached string representation. */
    protected final String stringValue;

    /**
     * Creates a new instance by parsing a {@code ProtocolInfo} string.
     *
     * @param protocolInfoString the {@link String} to parse.
     * @throws ParseException If {@code protocolInfoString} can't be parsed.
     */
    public ProtocolInfo(String protocolInfoString) throws ParseException {
        String tmpNetwork = WILDCARD;
        MimeType tmpMimeType = MimeType.ANYANY;
        String tmpAdditionalInfo = WILDCARD;

        if (isBlank(protocolInfoString)) {
            protocol = Protocol.ALL;
        } else {
            protocolInfoString = protocolInfoString.trim();
            String[] elements = protocolInfoString.split("\\s*:\\s*");
            protocol = Protocol.value(elements[0]);
            if (elements.length > 1) {
                tmpNetwork = elements[1];
            }
            if (elements.length > 2) {
                tmpMimeType = createMimeType(elements[2]);
            }
            if (elements.length > 3) {
                tmpAdditionalInfo = elements[3];
            }
            if (elements.length > 4) {
                throw new ParseException("Invalid protocolInfo string \"" + protocolInfoString + "\"");
            }
        }

        network = tmpNetwork;
        mimeType = tmpMimeType;
        additionalInfo = tmpAdditionalInfo;
        attributes = Collections.unmodifiableSortedMap(parseAdditionalInfo());
        attributesString = generateAttributesString();
        stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param contentFormat the content format for the new instance. Use
     *            {@code null} or blank for "any".
     * @param additionalInfo the additional information for the new instance.
     */
    public ProtocolInfo(Protocol protocol, String network, String contentFormat, String additionalInfo) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = createMimeType(contentFormat);
        this.additionalInfo = isBlank(additionalInfo) ? WILDCARD : additionalInfo;
        this.attributes = Collections.unmodifiableSortedMap(parseAdditionalInfo());
        this.attributesString = generateAttributesString();
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param mimeType the mime-type for the new instance. Use {@code null} or
     *            {@link MimeType#ANYANY} for "any".
     * @param additionalInfo the additional information for the new instance.
     */
    public ProtocolInfo(Protocol protocol, String network, MimeType mimeType, String additionalInfo) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = mimeType == null ? MimeType.ANYANY : mimeType;
        this.additionalInfo = isBlank(additionalInfo) ? WILDCARD : additionalInfo;
        this.attributes = Collections.unmodifiableSortedMap(parseAdditionalInfo());
        this.attributesString = generateAttributesString();
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param contentFormat the content format for the new instance. Use
     *            {@code null} or blank for "any".
     * @param attributes a {@link Map} of {@link ProtocolInfoAttributeName} and
     *            {@link ProtocolInfoAttribute} pairs for the new instance.
     */
    public ProtocolInfo(Protocol protocol, String network, String contentFormat,
            Map<ProtocolInfoAttributeName, ProtocolInfoAttribute> attributes) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = createMimeType(contentFormat);
        TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> tmpAttributes = createEmptyAttributesMap();
        tmpAttributes.putAll(attributes);
        this.attributes = Collections.unmodifiableSortedMap(tmpAttributes);
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param mimeType the mime-type for the new instance. Use {@code null} or
     *            {@link MimeType#ANYANY} for "any".
     * @param attributes a {@link Map} of {@link ProtocolInfoAttributeName} and
     *            {@link ProtocolInfoAttribute} pairs for the new instance.
     */
    public ProtocolInfo(Protocol protocol, String network, MimeType mimeType,
            Map<ProtocolInfoAttributeName, ProtocolInfoAttribute> attributes) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = mimeType == null ? MimeType.ANYANY : mimeType;
        TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> tmpAttributes = createEmptyAttributesMap();
        tmpAttributes.putAll(attributes);
        this.attributes = Collections.unmodifiableSortedMap(tmpAttributes);
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param contentFormat the content format for the new instance. Use
     *            {@code null} or blank for "any".
     * @param attributes an {@link EnumMap} with {@link DLNAAttribute}s the new
     *            instance.
     */
    public ProtocolInfo(Protocol protocol, String network, String contentFormat,
            EnumMap<DLNAAttribute.Type, DLNAAttribute<?>> attributes) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = createMimeType(contentFormat);
        this.attributes = Collections.unmodifiableSortedMap(dlnaAttributesToAttributes(attributes));
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance using the provided information.
     *
     * @param protocol the {@link Protocol} for the new instance. Use
     *            {@code null} for "any".
     * @param network the network for the new instance. Use {@code null} or
     *            blank for "any".
     * @param mimeType the mime-type for the new instance. Use {@code null} or
     *            {@link MimeType#ANYANY} for "any".
     * @param attributes an {@link EnumMap} with {@link DLNAAttribute}s for the
     *            new instance.
     */
    public ProtocolInfo(Protocol protocol, String network, MimeType mimeType,
            EnumMap<DLNAAttribute.Type, DLNAAttribute<?>> attributes) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = isBlank(network) ? WILDCARD : network;
        this.mimeType = mimeType == null ? MimeType.ANYANY : mimeType;
        this.attributes = Collections.unmodifiableSortedMap(dlnaAttributesToAttributes(attributes));
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance based on a {@link DLNAProfiles} profile.
     *
     * @param protocol the {@link Protocol} for the new instance.
     * @param profile the {@link DLNAProfiles} profile for the new instance.
     */
    public ProtocolInfo(Protocol protocol, DLNAProfiles profile) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = WILDCARD;
        this.mimeType = createMimeType(profile.getContentFormat());
        SortedMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> tmpAttributes = createEmptyAttributesMap();
        DLNAOrgProfileName profileName = DLNAOrgProfileName.FACTORY.createProfileName(profile.getCode());
        tmpAttributes.put(profileName.getName(), profileName);
        this.attributes = Collections.unmodifiableSortedMap(tmpAttributes);
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance based on a {@link DLNAProfiles} profile and
     * additional {@link DLNAAttribute}s.
     *
     * @param protocol the {@link Protocol} for the new instance.
     * @param profile the {@link DLNAProfiles} profile for the new instance.
     * @param dlnaAttributes an {@link EnumMap} with {@link DLNAAttribute}s for
     *            the new instance.
     */
    public ProtocolInfo(Protocol protocol, DLNAProfiles profile,
            EnumMap<DLNAAttribute.Type, DLNAAttribute<?>> dlnaAttributes) {
        this.protocol = protocol == null ? Protocol.ALL : protocol;
        this.network = WILDCARD;
        this.mimeType = createMimeType(profile.getContentFormat());
        TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> tmpAttributes = dlnaAttributesToAttributes(
                dlnaAttributes);
        DLNAOrgProfileName profileName = DLNAOrgProfileName.FACTORY.createProfileName(profile.getCode());
        tmpAttributes.put(profileName.getName(), profileName);
        this.attributes = Collections.unmodifiableSortedMap(tmpAttributes);
        this.attributesString = generateAttributesString();
        this.additionalInfo = this.attributesString;
        this.stringValue = generateStringValue();
    }

    /**
     * Creates a new instance from a {@link org.fourthline.cling.support.model.ProtocolInfo} instance.
     *
     * @param template the {@link org.fourthline.cling.support.model.ProtocolInfo} instance.
     */
    public ProtocolInfo(org.fourthline.cling.support.model.ProtocolInfo template) {
        this(template.getProtocol(), template.getNetwork(), template.getContentFormat(),
                template.getAdditionalInfo());
    }

    /**
     * @return The {@code DLNA.ORG_PN} (DLNA media format profile) for this
     *         {@link ProtocolInfo} or {@code null} if it isn't defined.
     */
    public DLNAOrgProfileName getDLNAProfileName() {
        ProtocolInfoAttribute pnAttribute = attributes.get(KnownProtocolInfoAttributeName.DLNA_ORG_PN);
        return pnAttribute instanceof DLNAOrgProfileName ? (DLNAOrgProfileName) pnAttribute : null;
    }

    /**
     * @return The {@code DLNA.ORG_OP} of this {@link ProtocolInfo} or
     *         {@code null} if it isn't defined.
     */
    public DLNAOrgOperations getDLNAOperations() {
        ProtocolInfoAttribute operationsAttribute = attributes.get(KnownProtocolInfoAttributeName.DLNA_ORG_OP);
        return operationsAttribute instanceof DLNAOrgOperations ? (DLNAOrgOperations) operationsAttribute : null;
    }

    /**
     * @return The {@code DLNA.ORG_PS} of this {@link ProtocolInfo} or
     *         {@code null} if it isn't defined.
     */
    public DLNAOrgPlaySpeeds getDLNAPlaySpeeds() {
        ProtocolInfoAttribute playSpeedsAttribute = attributes.get(KnownProtocolInfoAttributeName.DLNA_ORG_PS);
        return playSpeedsAttribute instanceof DLNAOrgPlaySpeeds ? (DLNAOrgPlaySpeeds) playSpeedsAttribute : null;
    }

    /**
     * @return The {@code DLNA.ORG_CI} of this {@link ProtocolInfo} or
     *         {@code null} if it isn't defined.
     */
    public DLNAOrgConversionIndicator getDLNAConversionIndicator() {
        ProtocolInfoAttribute conversionIndicatorAttribute = attributes
                .get(KnownProtocolInfoAttributeName.DLNA_ORG_CI);
        return conversionIndicatorAttribute instanceof DLNAOrgConversionIndicator
                ? (DLNAOrgConversionIndicator) conversionIndicatorAttribute
                : null;
    }

    /**
     * @return The {@code DLNA.ORG_FLAGS} of this {@link ProtocolInfo} or
     *         {@code null} if it isn't defined.
     */
    public DLNAOrgFlags getFlags() {
        ProtocolInfoAttribute flagsAttribute = attributes.get(KnownProtocolInfoAttributeName.DLNA_ORG_FLAGS);
        return flagsAttribute instanceof DLNAOrgFlags ? (DLNAOrgFlags) flagsAttribute : null;
    }

    /**
     * Searches for a {@link ProfileName} (any attribute name ending with
     * {@code "_PN"} among the attributes, and returns the first one found.
     * There is supposed to be zero or one {@link ProfileName} for any given
     * instance of {@link ProtocolInfo}. If none is found, {@code null} is
     * returned.
     * <p>
     * <b>Note: This will return any {@link ProfileName}, not just
     * {@link DLNAOrgProfileName}s</b>. If you're looking for a
     * {@code DLNA.ORG_PN}, use {@link #getDLNAProfileName} instead.
     *
     * @return The {@code DLNA.ORG_PN} (DLNA media format profile) for this
     *         {@link ProtocolInfo} or {@code null} if it isn't defined.
     */
    public ProfileName getProfileName() {
        for (ProtocolInfoAttribute attribute : attributes.values()) {
            if (attribute instanceof ProfileName) {
                return (ProfileName) attribute;
            }
        }
        return null;
    }

    /**
     * Gets the cached {@link String} generated from the attributes map. This
     * should be identical to {@link #getAdditionalInfo()}.
     *
     * @return The {@link String} representation of {@link #attributes}.
     */
    public String getAttributesString() {
        return attributesString;
    }

    /**
     * For internal use only. Generates a {@link String} representation of the
     * {@link ProtocolInfoAttribute}s in {@link #attributes}.
     *
     * @return The {@link String} representation.
     */
    protected String generateAttributesString() {
        if (attributes == null || attributes.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (ProtocolInfoAttribute attribute : attributes.values()) {
            String attributeString = attribute.getAttributeString();
            if (isNotBlank(attributeString)) {
                if (sb.length() > 0) {
                    sb.append(";");
                }
                sb.append(attributeString);
            }
        }
        return sb.toString();
    }

    /**
     * Gets the {@link MimeType} created from the content format of this
     * {@link ProtocolInfo}.
     *
     * @return The {@link MimeType}.
     */
    public MimeType getMimeType() {
        return mimeType;
    }

    /**
     * For internal use only, creates the {@link MimeType} that is stored in
     * {@code this.mimeType}.
     *
     * @param contentFormat the {@code protocolInfo} {@code contentFormat} to
     *            parse.
     * @return A new {@link MimeType} instance.
     */
    protected MimeType createMimeType(String contentFormat) {
        try {
            return MimeType.valueOf(contentFormat);
        } catch (ParseException e) {
            LOGGER.error("Error parsing MimeType from \"{}\": {}", contentFormat, e.getMessage());
            LOGGER.trace("", e);
        }
        return MimeType.ANYANY;
    }

    /**
     * Creates a new {@link org.seamless.util.MimeType} from the
     * {@link MimeType} of this {@link ProtocolInfo}. To get the
     * {@link MimeType}, use {@link #getMimeType()} instead.
     *
     * @return The corresponding {@link org.seamless.util.MimeType}.
     * @throws IllegalArgumentException if
     *             {@link org.seamless.util.MimeType#valueOf()} can't parse this
     *             {@link MimeType}.
     * @see #getMimeType()
     */
    public org.seamless.util.MimeType getSeamlessMimeType() throws IllegalArgumentException {
        return org.seamless.util.MimeType.valueOf(mimeType.toString());
    }

    /**
     * Parses {@code additionalInfo}.
     *
     * @return The {@link SortedMap} of parsed {@link ProtocolInfoAttribute}s.
     */
    protected SortedMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> parseAdditionalInfo() {
        if (isBlank(additionalInfo) || WILDCARD.equals(additionalInfo.trim())) {
            return EMPTYMAP;
        }
        TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> result = createEmptyAttributesMap();
        String[] attributeStrings = additionalInfo.trim().toUpperCase(Locale.ROOT).split("\\s*;\\s*");
        for (String attributeString : attributeStrings) {
            if (isBlank(attributeString)) {
                continue;
            }
            String[] attributeEntry = attributeString.split("\\s*=\\s*");
            if (attributeEntry.length == 2) {
                try {
                    ProtocolInfoAttribute attribute = ProtocolInfoAttribute.FACTORY
                            .createAttribute(attributeEntry[0], attributeEntry[1], protocol);

                    if (attribute != null) {
                        result.put(attribute.getName(), attribute);
                    } else {
                        LOGGER.debug("Failed to parse attribute \"{}\"", attributeString);
                    }
                } catch (ParseException e) {
                    LOGGER.debug("Failed to parse attribute \"{}\": {}", attributeString, e.getMessage());
                    LOGGER.trace("", e);
                }
            } else {
                LOGGER.debug("Invalid ProtocolInfo attribute \"{}\"", attributeString);
            }
        }
        return result;
    }

    /**
     * @return the {@link Protocol} of this {@link ProtocolInfo}.
     */
    public Protocol getProtocol() {
        return protocol;
    }

    /**
     * @return the {@code network} of this {@link ProtocolInfo}.
     */
    public String getNetwork() {
        return network;
    }

    /**
     * @return the {@code contentFormat} of this {@link ProtocolInfo}.
     */
    public String getContentFormat() {
        return mimeType.toString();
    }

    /**
     * @return the {@code additionalInfo} of this {@link ProtocolInfo}.
     */
    public String getAdditionalInfo() {
        return additionalInfo;
    }

    /**
     * The attributes are the content of {@code additionalInfo} in parsed form.
     *
     * @return the attributes of this {@link ProtocolInfo}.
     */
    public SortedMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> getAttributes() {
        return attributes;
    }

    /**
     * Returns a debug string representation of this {@link ProtocolInfo}.
     *
     * @return The debug {@link String} representation.
     */
    public String toDebugString() {
        StringBuilder sb = new StringBuilder();

        sb.append("Protocol: ").append(protocol).append(", Network: ").append(network)
                .append(", ContentFormat/MimeType: ").append(mimeType);

        if (isNotBlank(additionalInfo)) {
            sb.append(", AdditionalInfo: ").append(additionalInfo);
        }

        if (!mimeType.toString().equals(mimeType.toStringWithoutParameters())) {
            sb.append(", Simple MimeType: ").append(mimeType.toStringWithoutParameters());
        }
        if (mimeType.isDRM()) {
            sb.append(", DRM");
        }
        if (!mimeType.getParameters().isEmpty()) {
            sb.append(", MimeType Parameters: ").append(mimeType.getParameters());
        }

        if (attributes != null && !attributes.isEmpty()) {
            sb.append(", Attributes: ").append(attributes);
        }

        return sb.toString();
    }

    @Override
    public String toString() {
        return stringValue;
    }

    /**
     * For internal use only, generates the string representation of this
     * {@link ProtocolInfo} for use for the cached {@link #toString()} value.
     *
     * @return The string representation.
     */
    protected String generateStringValue() {
        StringBuilder sb = new StringBuilder();
        sb.append(protocol == null ? WILDCARD : protocol).append(":").append(isBlank(network) ? WILDCARD : network)
                .append(":").append(mimeType == null ? MimeType.ANYANY : mimeType).append(":")
                .append(isBlank(attributesString) ? WILDCARD : attributesString);
        return sb.toString();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
        result = prime * result + ((attributesString == null) ? 0 : attributesString.hashCode());
        result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
        result = prime * result + ((network == null) ? 0 : network.hashCode());
        result = prime * result + ((protocol == null) ? 0 : protocol.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof ProtocolInfo)) {
            return false;
        }
        ProtocolInfo other = (ProtocolInfo) obj;
        if (additionalInfo == null) {
            if (other.additionalInfo != null) {
                return false;
            }
        } else if (!additionalInfo.equals(other.additionalInfo)) {
            return false;
        }
        if (attributesString == null) {
            if (other.attributesString != null) {
                return false;
            }
        } else if (!attributesString.equals(other.attributesString)) {
            return false;
        }
        if (mimeType == null) {
            if (other.mimeType != null) {
                return false;
            }
        } else if (!mimeType.equals(other.mimeType)) {
            return false;
        }
        if (network == null) {
            if (other.network != null) {
                return false;
            }
        } else if (!network.equals(other.network)) {
            return false;
        }
        if (protocol != other.protocol) {
            return false;
        }
        return true;
    }

    /**
     * Converts an {@link EnumMap} of
     * {@link org.fourthline.cling.support.model.dlna.DLNAAttribute}s to a
     * {@link TreeMap} of {@link ProtocolInfoAttribute}s.
     *
     * @param dlnaAttributes the {@link EnumMap} of
     *            {@link org.fourthline.cling.support.model.dlna.DLNAAttribute}s
     *            to convert.
     * @return A {@link TreeMap} containing the converted
     *         {@link ProtocolInfoAttribute}s.
     */
    public static TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> dlnaAttributesToAttributes(
            EnumMap<DLNAAttribute.Type, DLNAAttribute<?>> dlnaAttributes) {
        TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> attributes = createEmptyAttributesMap();
        for (Entry<DLNAAttribute.Type, DLNAAttribute<?>> entry : dlnaAttributes.entrySet()) {
            try {
                ProtocolInfoAttribute attribute = ProtocolInfoAttribute.FACTORY.createAttribute(
                        entry.getKey().getAttributeName(), entry.getValue().getString(), Protocol.HTTP_GET);
                if (attribute != null) {
                    attributes.put(attribute.getName(), attribute);
                }
            } catch (ParseException e) {
                LOGGER.debug("Couldn't parse DLNAAttribute \"{}\" = \"{}\": {}", entry.getKey().getAttributeName(),
                        entry.getValue().getString(), e.getMessage());
                LOGGER.trace("", e);
            }
        }
        return attributes;
    }

    /**
     * A convenience method to create an empty {@link TreeMap} of
     * {@link ProtocolInfoAttribute}s with the correct {@link Comparator}
     * {@link AttributeComparator}.
     *
     * @return The empty attributes map.
     */
    public static TreeMap<ProtocolInfoAttributeName, ProtocolInfoAttribute> createEmptyAttributesMap() {
        return new TreeMap<>(new AttributeComparator());
    }

    /**
     * DLNA requires {@code protocolInfo} attributes to appear in a certain
     * order. Any {@link SortedMap} that is initialized with this
     * {@link Comparator} will automatically sort its elements according to this
     * custom order.
     *
     * It is vital that any {@link Map} used to store
     * {@link ProtocolInfoAttribute}s in relation to {@link ProtocolInfo} use
     * this class as it's {@link Comparator}.
     *
     * @author Nadahar
     */
    public static class AttributeComparator implements Comparator<ProtocolInfoAttributeName>, Serializable {

        private static final long serialVersionUID = 1L;
        /** Defines the sort order for known attributes */
        public static final List<ProtocolInfoAttributeName> DEFINED_ORDER = Collections
                .unmodifiableList(Arrays.asList(new ProtocolInfoAttributeName[] {
                        KnownProtocolInfoAttributeName.DLNA_ORG_PN, KnownProtocolInfoAttributeName.DLNA_ORG_OP,
                        KnownProtocolInfoAttributeName.DLNA_ORG_PS, KnownProtocolInfoAttributeName.DLNA_ORG_CI,
                        KnownProtocolInfoAttributeName.DLNA_ORG_FLAGS, KnownProtocolInfoAttributeName.ARIB_OR_JP_PN,
                        KnownProtocolInfoAttributeName.DTV_MVP_PN, KnownProtocolInfoAttributeName.PANASONIC_COM_PN,
                        KnownProtocolInfoAttributeName.MICROSOFT_COM_PN,
                        KnownProtocolInfoAttributeName.SHARP_COM_PN, KnownProtocolInfoAttributeName.SONY_COM_PN }));

        @Override
        public int compare(ProtocolInfoAttributeName o1, ProtocolInfoAttributeName o2) {
            if (o1 == null && o2 == null) {
                return 0;
            }
            if (o1 == null) {
                return 1;
            }
            if (o2 == null) {
                return -1;
            }

            int o1Index = DEFINED_ORDER.indexOf(o1);
            int o2Index = DEFINED_ORDER.indexOf(o2);

            // Sort by defined order if both arguments are defined
            if (o1Index >= 0 && o2Index >= 0) {
                return o1Index - o2Index;
            }

            // Sort by string value if none of the arguments are defined
            if (o1Index < 0 && o2Index < 0) {
                return o1.getName().compareTo(o2.getName());
            }

            // Sort defined arguments before undefined arguments
            if (o1Index < 0) {
                return 1;
            }
            if (o2Index < 0) {
                return -1;
            }

            // Sort alphabetically by name
            String o1Name = o1.getName();
            String o2Name = o2.getName();

            if (o1Name == null && o2Name == null) {
                return 0;
            }
            if (o1Name == null) {
                return 1;
            }
            if (o2Name == null) {
                return -1;
            }
            return o1Name.compareTo(o2Name);
        }
    }

    /**
     * Compares {@link ProtocolInfo} instances for sorting. Sorting is done in
     * this order: {@code protocol}, {@code network}, {@code contentFormat} and
     * {@code additionalInfo}.
     *
     * @param other the {@link ProtocolInfo} instance to compare to.
     */
    @Override
    public int compareTo(ProtocolInfo other) {

        if (other == null) {
            return -1;
        }

        // Protocol
        if (protocol == null && other.protocol != null) {
            return 1;
        }
        if (protocol != null && other.protocol == null) {
            return -1;
        }
        int result;
        if (protocol != null && other.protocol != null) {
            result = protocol.compareTo(other.protocol);
            if (result != 0) {
                return result;
            }
        }

        // Network
        if (network == null && other.network != null) {
            return 1;
        }
        if (network != null && other.network == null) {
            return -1;
        }
        if (network != null && other.network != null) {
            result = network.compareTo(other.network);
            if (result != 0) {
                return result;
            }
        }

        // ContentFormat/MimeType
        if (mimeType == null && other.mimeType != null) {
            return 1;
        }
        if (mimeType != null && other.mimeType == null) {
            return -1;
        }
        if (mimeType != null && other.mimeType != null) {
            result = mimeType.compareTo(other.mimeType);
            if (result != 0) {
                return result;
            }
        }

        // AdditionalInfo
        if (additionalInfo == null && other.additionalInfo != null) {
            return 1;
        }
        if (additionalInfo != null && other.additionalInfo == null) {
            return -1;
        }
        if (additionalInfo != null && other.additionalInfo != null) {
            return additionalInfo.compareTo(other.additionalInfo);
        }

        return 0;
    }
}