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

Java tutorial

Introduction

Here is the source code for net.pms.dlna.protocolinfo.DeviceProtocolInfo.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 java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.pms.dlna.DLNAImageProfile;
import net.pms.util.ParseException;

/**
 * This class represents a device's {@code ProtocolInfo} elements, typically
 * {@code Source} or {@code Sink} from {@code GetProtocolInfo}.
 * <p>
 * This class is thread-safe.
 *
 * @author Nadahar
 */
public class DeviceProtocolInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /** The logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(DeviceProtocolInfo.class);

    /** The static singleton {@code GetProtocolInfo} {@code Source} identifier */
    public static final DeviceProtocolInfoSource<DeviceProtocolInfo> GET_PROTOCOLINFO_SOURCE = new GetProtocolInfoType() {

        private static final long serialVersionUID = 1L;

        @Override
        public String getType() {
            return "GetProtocolInfo Source";
        }
    };

    /** The static singleton {@code GetProtocolInfo} {@code Sink} identifier */
    public static final DeviceProtocolInfoSource<DeviceProtocolInfo> GET_PROTOCOLINFO_SINK = new GetProtocolInfoType() {

        private static final long serialVersionUID = 1L;

        @Override
        public String getType() {
            return "GetProtocolInfo Sink";
        }
    };

    /**
     * A regex for splitting a comma separated list of {@code protocolInfo}
     * entries returned from {@code GetProtocolInfo} while taking DLNA comma
     * escaping rules into account.
     */
    public static final String COMMA_SPLIT_REGEX = "\\s*(?:(?<!\\\\),|(?<!\\\\)\\\\\\\\,)\\s*";

    /**
     * A {@link CharSequenceTranslator} for unescaping individual
     * {@code GetProtocolInfo} elements.
     */
    public static final CharSequenceTranslator PROTOCOLINFO_UNESCAPE = new LookupTranslator(
            new String[][] { { "\\\\", "\\" }, { "\\,", "," } });

    /**
     * A {@link CharSequenceTranslator} for escaping individual
     * {@code GetProtocolInfo} elements.
     */
    public static final CharSequenceTranslator PROTOCOLINFO_ESCAPE = new LookupTranslator(
            new String[][] { { ",", "\\," }, { "\\", "\\\\" }, });

    /** The sets lock. */
    protected final ReentrantReadWriteLock setsLock = new ReentrantReadWriteLock();

    /** The {@link Map} of {@link ProtocolInfo} {@link Set}s. */
    protected final HashMap<DeviceProtocolInfoSource<?>, SortedSet<ProtocolInfo>> protocolInfoSets = new HashMap<>();

    /** The image profile set. */
    protected final SortedSet<DLNAImageProfile> imageProfileSet = new TreeSet<>();

    /**
     * Creates a new empty instance.
     */
    public DeviceProtocolInfo() {
    }

    /**
     * Creates a new instance containing the content from the parsing of
     * {@code protocolInfoString}.
     *
     * @param type The {@link DeviceProtocolInfoSource} of
     *            {@code protocolInfoString}, must be either
     *            {@link #GET_PROTOCOLINFO_SINK} or
     *            {@link #GET_PROTOCOLINFO_SOURCE}.
     * @param protocolInfoString a comma separated string of
     *            {@code protocolInfo} representations.
     */
    public DeviceProtocolInfo(GetProtocolInfoType type, String protocolInfoString) {
        add(type, protocolInfoString);
    }

    /**
     * Tries to parse {@code protocolInfoString} and add the resulting
     * {@link ProtocolInfo} instances.
     *
     * @param type The {@link DeviceProtocolInfoSource} that identifies the
     *            source of these {@code protocolInfo}s.
     * @param protocolInfoString a comma separated string of
     *            {@code protocolInfo} representations whose presence is to be
     *            ensured.
     * @return {@code true} if this changed as a result of the call. Returns
     *         {@code false} this already contains the specified element(s).
     */
    public boolean add(DeviceProtocolInfoSource<?> type, String protocolInfoString) {
        if (StringUtils.isBlank(protocolInfoString)) {
            return false;
        }

        String[] elements = protocolInfoString.trim().split(COMMA_SPLIT_REGEX);
        boolean result = false;
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> currentSet;
            if (protocolInfoSets.containsKey(type)) {
                currentSet = protocolInfoSets.get(type);
            } else {
                currentSet = new TreeSet<ProtocolInfo>();
                protocolInfoSets.put(type, currentSet);
            }

            SortedSet<ProtocolInfo> tempSet = null;
            for (String element : elements) {
                try {
                    tempSet = handleSpecialCaseString(element);
                    if (tempSet == null) {
                        // No special handling
                        result |= currentSet.add(new ProtocolInfo(unescapeString(element)));
                    } else {
                        // Add the special handling results
                        result |= currentSet.addAll(tempSet);
                        tempSet = null;
                    }
                } catch (ParseException e) {
                    LOGGER.warn("Unable to parse protocolInfo from \"{}\", this profile will not be registered: {}",
                            element, e.getMessage());
                    LOGGER.trace("", e);
                }
            }
            updateImageProfiles();
        } finally {
            setsLock.writeLock().unlock();
        }
        return result;
    }

    /**
     * Re-parses {@code protocolInfoSet} and stores the results in
     * {@code imageProfileSet}.
     */
    protected void updateImageProfiles() {
        setsLock.writeLock().lock();
        try {
            imageProfileSet.clear();
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                for (ProtocolInfo protocolInfo : set) {
                    imageProfileSet.addAll(DLNAImageProfile.toDLNAImageProfiles(protocolInfo));
                }
            }
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    // Standard java.util.Collection methods.

    /**
     * Returns the number of elements of the given
     * {@link DeviceProtocolInfoSource} type. If this contains more than
     * {@link Integer#MAX_VALUE} elements, returns {@link Integer#MAX_VALUE}.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to get the number
     *            of elements for.
     * @return The number of elements in the {@link Set} for {@code type}.
     */
    public int size(DeviceProtocolInfoSource<?> type) {
        setsLock.readLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            return set == null ? 0 : set.size();
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns the total number of elements of all
     * {@link DeviceProtocolInfoSource} types. If the result is greater than
     * {@link Integer#MAX_VALUE} elements, returns {@link Integer#MAX_VALUE}.
     *
     * @return The number of elements.
     */
    public int size() {
        long result = 0;
        setsLock.readLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                if (set != null) {
                    result += set.size();
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return result > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) result;
    }

    /**
     * Checks if the {@link Set} for the given {@link DeviceProtocolInfoSource}
     * type is empty.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to check.
     * @return {@code true} if {@code protocolInfoSets} contains no elements of
     *         {@code type}.
     */
    public boolean isEmpty(DeviceProtocolInfoSource<?> type) {
        setsLock.readLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            return set == null ? true : set.isEmpty();
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Checks if all the {@link DeviceProtocolInfoSource} {@link Set}s are
     * empty.
     *
     * @return {@code true} if neither of the {@link DeviceProtocolInfoSource}
     *         {@link Set}s contain any elements, {@code false} otherwise.
     */
    public boolean isEmpty() {
        setsLock.readLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                if (set != null && !set.isEmpty()) {
                    return false;
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return true;
    }

    /**
     * Returns {@code true} if the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} contains the specified element.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to check.
     * @param protocolInfo the element whose presence is to be tested.
     * @return {@code true} if the {@link Set} for the given
     *         {@link DeviceProtocolInfoSource} contains the specified element,
     *         {@code false} otherwise.
     */
    public boolean contains(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
        setsLock.readLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            return set == null ? false : set.contains(protocolInfo);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns {@code true} if any of the {@link DeviceProtocolInfoSource}
     * {@link Set}s contains the specified element.
     *
     * @param protocolInfo the element whose presence is to be tested.
     * @return {@code true} if any of the {@link DeviceProtocolInfoSource}
     *         {@link Set}s contains the specified element {@code false}
     *         otherwise.
     */
    public boolean contains(ProtocolInfo protocolInfo) {
        setsLock.readLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                if (set != null && set.contains(protocolInfo)) {
                    return true;
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return false;
    }

    /**
     * Returns a sorted array containing all of the elements in the {@link Set}
     * for the given {@link DeviceProtocolInfoSource}.
     * <p>
     * The returned array will be "safe" in that no reference to it is
     * maintained. (In other words, this method must allocate a new array). The
     * caller is thus free to modify the returned array.
     *
     * @param type the {@link DeviceProtocolInfoSource} type whose elements to
     *            convert to an {@code array}.
     * @return An array containing all the {@link ProtocolInfo} instances in the
     *         {@link Set} for {@code type}.
     */
    public ProtocolInfo[] toArray(DeviceProtocolInfoSource<?> type) {
        setsLock.readLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            return set == null ? null : set.toArray(new ProtocolInfo[protocolInfoSets.size()]);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns a sorted array containing all of the elements for all of the
     * {@link DeviceProtocolInfoSource} {@link Set}s.
     * <p>
     * The returned array will be "safe" in that no reference to it is
     * maintained. (In other words, this method must allocate a new array). The
     * caller is thus free to modify the returned array.
     *
     * @return An array containing all the {@link ProtocolInfo} instances.
     */
    public ProtocolInfo[] toArray() {
        SortedSet<ProtocolInfo> result = new TreeSet<>();
        setsLock.readLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                if (set != null) {
                    result.addAll(set);
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return result.toArray(new ProtocolInfo[result.size()]);
    }

    /**
     * Returns {@code true} if the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} contains all of the elements in the
     * specified collection.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to check.
     * @param collection a {@link Collection} to be checked for containment.
     * @return {@code true} if {@link Set} for {@code type} contains all of the
     *         elements in {@code collection}.
     *
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #containsAll(Collection)
     */

    public boolean containsAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
        setsLock.readLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            return set == null ? false : set.containsAll(collection);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns {@code true} if any of the {@link DeviceProtocolInfoSource}
     * {@link Set}s contains the elements in the specified collection.
     *
     * @param collection a {@link Collection} to be checked for containment.
     * @return {@code true} if any of the {@link DeviceProtocolInfoSource}
     *         {@link Set}s contains the elements in {@code collection}.
     *
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #containsAll(DeviceProtocolInfoSource, Collection)
     */
    public boolean containsAll(Collection<ProtocolInfo> collection) {
        setsLock.readLock().lock();
        try {
            for (ProtocolInfo protocolInfo : collection) {
                if (!contains(protocolInfo)) {
                    return false;
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return true;
    }

    /**
     * Removes all elements in the {@link Set} for the given
     * {@link DeviceProtocolInfoSource}.
     *
     * @param type The {@link DeviceProtocolInfoSource} type to clear.
     */
    public void clear(DeviceProtocolInfoSource<?> type) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            if (set != null) {
                set.clear();
                updateImageProfiles();
            }
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Removes all elements in all of the {@link DeviceProtocolInfoSource}
     * {@link Set}s.
     */
    public void clear() {
        setsLock.writeLock().lock();
        try {
            protocolInfoSets.clear();
            imageProfileSet.clear();
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Ensures that the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} contains the specified element. Returns
     * {@code true} if {@code protocolInfo} was added a result of the call, or
     * {@code false} the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} already contains the specified element.
     *
     * @param type the {@link DeviceProtocolInfoSource} type.
     * @param protocolInfo element whose presence is to be ensured.
     * @return {@code true} if the {@link Set} for {@code type} changed as a
     *         result of the call, {@code false} otherwise.
     */
    public boolean add(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> currentSet;
            if (protocolInfoSets.containsKey(type)) {
                currentSet = protocolInfoSets.get(type);
            } else {
                currentSet = new TreeSet<ProtocolInfo>();
                protocolInfoSets.put(type, currentSet);
            }

            if (currentSet.add(protocolInfo)) {
                updateImageProfiles();
                return true;
            }
            return false;
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Ensures that the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} contains all of the elements in the
     * specified collection.
     *
     * @param type the {@link DeviceProtocolInfoSource} type.
     * @param collection a {@link Collection} containing elements to be added.
     * @return {@code true} if the {@link Set} for {@code type} changed as a
     *         result of the call, {@code false} otherwise.
     *
     * @see #add(DeviceProtocolInfoSource, ProtocolInfo)
     */
    public boolean addAll(DeviceProtocolInfoSource<?> type, Collection<? extends ProtocolInfo> collection) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> currentSet;
            if (protocolInfoSets.containsKey(type)) {
                currentSet = protocolInfoSets.get(type);
            } else {
                currentSet = new TreeSet<ProtocolInfo>();
                protocolInfoSets.put(type, currentSet);
            }

            if (currentSet.addAll(collection)) {
                updateImageProfiles();
                return true;
            }
            return false;
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Removes a given instance of {@link ProtocolInfo}, if it is present in the
     * {@link Set} for the given {@link DeviceProtocolInfoSource}. Returns
     * {@code true} if the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} contained the specified element (or
     * equivalently, if the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} changed as a result of the call).
     *
     * @param type the {@link DeviceProtocolInfoSource} type.
     * @param protocolInfo element to be removed, if present.
     * @return {@code true} if an element was removed from the {@link Set} for
     *         {@code type} as a result of this call, {@code false} otherwise.
     */
    public boolean remove(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            if (set != null) {
                if (set.remove(protocolInfo)) {
                    updateImageProfiles();
                    return true;
                }
            }
            return false;
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Removes all instances of {@code protocolInfo} from all the
     * {@link DeviceProtocolInfoSource} {@link Set}s, if it is present. Returns
     * {@code true} if any of the {@link DeviceProtocolInfoSource} {@link Set}s
     * contained the specified element (or equivalently, if any of the
     * {@link DeviceProtocolInfoSource} {@link Set}s changed as a result of the
     * call).
     *
     * @param protocolInfo element to be removed, if present.
     * @return {@code true} if an element was removed as a result of this call,
     *         {@code false} otherwise.
     */
    public boolean remove(ProtocolInfo protocolInfo) {
        boolean result = false;
        setsLock.writeLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                result |= set != null && set.remove(protocolInfo);
            }
            if (result) {
                updateImageProfiles();
            }
        } finally {
            setsLock.writeLock().unlock();
        }
        return result;
    }

    /**
     * Removes all elements from the {@link Set} for the given
     * {@link DeviceProtocolInfoSource} that are also contained in
     * {@code collection}.
     *
     * @param type the {@link DeviceProtocolInfoSource} type.
     * @param collection a {@link Collection} containing the elements to be
     *            removed.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #remove(ProtocolInfo)
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #removeAll(Collection)
     */
    public boolean removeAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            if (set != null && set.removeAll(collection)) {
                updateImageProfiles();
                return true;
            }
            return false;
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Removes all elements from all the {@link DeviceProtocolInfoSource}
     * {@link Set}s that are also contained in {@code collection}.
     *
     * @param collection a {@link Collection} containing the elements to be
     *            removed.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #remove(ProtocolInfo)
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #removeAll(DeviceProtocolInfoSource, Collection)
     */
    public boolean removeAll(Collection<ProtocolInfo> collection) {
        boolean result = false;
        setsLock.writeLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                result |= set != null && set.removeAll(collection);
            }
            if (result) {
                updateImageProfiles();
            }
        } finally {
            setsLock.writeLock().unlock();
        }
        return result;
    }

    /**
     * Retains only the elements that are contained in {@code collection} from
     * the {@link Set} for the given {@link DeviceProtocolInfoSource}. In other
     * words, removes all elements that are not contained in {@code collection}
     * from the {@link Set} for the given {@link DeviceProtocolInfoSource}.
     *
     * @param type the {@link DeviceProtocolInfoSource} type.
     * @param collection a {@link Collection} containing elements to be
     *            retained.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(ProtocolInfo)
     * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #retainAll(Collection)
     */
    public boolean retainAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
        setsLock.writeLock().lock();
        try {
            SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
            if (set != null && set.retainAll(collection)) {
                updateImageProfiles();
                return true;
            }
            return false;
        } finally {
            setsLock.writeLock().unlock();
        }
    }

    /**
     * Retains only the elements that are contained in {@code collection} in all
     * the {@link DeviceProtocolInfoSource} {@link Set}s. In other words,
     * removes all elements from all the {@link DeviceProtocolInfoSource}
     * {@link Set}s that are not contained in {@code collection}.
     *
     * @param collection a {@link Collection} containing elements to be
     *            retained.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(ProtocolInfo)
     * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #contains(ProtocolInfo)
     * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
     * @see #retainAll(DeviceProtocolInfoSource, Collection)
     */
    public boolean retainAll(Collection<ProtocolInfo> collection) {
        boolean result = false;
        setsLock.writeLock().lock();
        try {
            for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
                result |= set != null && set.retainAll(collection);
            }
            if (result) {
                updateImageProfiles();
            }
        } finally {
            setsLock.writeLock().unlock();
        }
        return result;
    }

    // imageProfileSet "java.util.Collection methods" getters

    /**
     * Returns the number of {@link DLNAImageProfile} elements. If this contains
     * more than {@link Integer#MAX_VALUE} elements, returns
     * {@link Integer#MAX_VALUE}.
     *
     * @return The number of {@link DLNAImageProfile} elements.
     */
    public int imageProfilesSize() {
        setsLock.readLock().lock();
        try {
            return imageProfileSet.size();
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Checks if is image profiles empty.
     *
     * @return {@code true} if this contains no {@link DLNAImageProfile}
     *         elements.
     */
    public boolean isImageProfilesEmpty() {
        setsLock.readLock().lock();
        try {
            return imageProfileSet.isEmpty();
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns {@code true} if this contains the specified
     * {@link DLNAImageProfile} instance.
     *
     * @param imageProfile the {@link DLNAImageProfile} instance whose presence
     *            is to be tested.
     * @return {@code true} if this contains the specified
     *         {@link DLNAImageProfile} instance.
     */
    public boolean imageProfilesContains(DLNAImageProfile imageProfile) {
        setsLock.readLock().lock();
        try {
            return imageProfileSet.contains(imageProfile);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns an array containing all the {@link DLNAImageProfile} instances in
     * an unspecified order.
     * <p>
     * The returned array will be "safe" in that no references to it are
     * maintained. (In other words, this method must allocate a new array). The
     * caller is thus free to modify the returned array.
     *
     * @return An array containing all the {@link DLNAImageProfile} instances.
     */
    public DLNAImageProfile[] imageProfilesToArray() {
        setsLock.readLock().lock();
        try {
            return imageProfileSet.toArray(new DLNAImageProfile[imageProfileSet.size()]);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    /**
     * Returns {@code true} if this contains all the {@link DLNAImageProfile} instances in the
     * specified collection.
     *
     * @param collection a {@link Collection} to be checked for containment.
     * @return {@code true} if this collection contains all of the {@link DLNAImageProfile} instances in
     *         {@code collection}.
     *
     * @see #imageProfilesContains(DLNAImageProfile)
     */
    public boolean imageProfilesContainsAll(Collection<DLNAImageProfile> collection) {
        setsLock.readLock().lock();
        try {
            return imageProfileSet.containsAll(collection);
        } finally {
            setsLock.readLock().unlock();
        }
    }

    @Override
    public String toString() {
        return toString(null, false);
    }

    /**
     * Returns a string representation of this {@link DeviceProtocolInfo}
     * instance showing only the {@link ProtocolInfo} instances of the given
     * {@link DeviceProtocolInfoSource} type. If {@code debug} is {@code true},
     * verbose output is returned.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to include.
     * @return A string representation of this {@link DeviceProtocolInfo}.
     */
    public String toString(DeviceProtocolInfoSource<?> type) {
        return toString(type, false);
    }

    /**
     * Returns a string representation of this {@link DeviceProtocolInfo}
     * instance. If {@code debug} is {@code true}, verbose output is returned.
     *
     * @param debug whether or not verbose output should be generated.
     * @return A string representation of this {@link DeviceProtocolInfo}.
     */
    public String toString(boolean debug) {
        return toString(null, debug);
    }

    /**
     * Returns a string representation of this {@link DeviceProtocolInfo}
     * instance showing only the {@link ProtocolInfo} instances of the given
     * {@link DeviceProtocolInfoSource} type. If {@code debug} is {@code true},
     * verbose output is returned.
     *
     * @param type the {@link DeviceProtocolInfoSource} type to include. Use
     *            {@code null} for all types.
     * @param debug whether or not verbose output should be generated.
     * @return A string representation of this {@link DeviceProtocolInfo}.
     */
    public String toString(DeviceProtocolInfoSource<?> type, boolean debug) {
        StringBuilder sb = new StringBuilder();
        setsLock.readLock().lock();
        try {
            if (protocolInfoSets != null && !protocolInfoSets.isEmpty()) {
                for (Entry<DeviceProtocolInfoSource<?>, SortedSet<ProtocolInfo>> entry : protocolInfoSets
                        .entrySet()) {
                    if (type == null || type.equals(entry.getKey())) {
                        if (!entry.getValue().isEmpty()) {
                            sb.append(entry.getKey().getType()).append(" entries:\n");
                            for (ProtocolInfo protocolInfo : entry.getValue()) {
                                if (protocolInfo != null) {
                                    sb.append("  ")
                                            .append(debug ? protocolInfo.toDebugString() : protocolInfo.toString())
                                            .append("\n");
                                }
                            }
                            sb.append("\n");
                        }
                    }
                }
            }
            if (imageProfileSet != null && !imageProfileSet.isEmpty()) {
                sb.append("DLNAImageProfile entries:\n");
                for (DLNAImageProfile imageProfile : imageProfileSet) {
                    if (imageProfile != null) {
                        sb.append("  ").append(imageProfile).append("\n");
                    }
                }
            }
        } finally {
            setsLock.readLock().unlock();
        }
        return sb.toString();
    }

    // Static methods

    /**
     * Escapes {@code protocolInfo} strings for use in {@code GetProtocolInfo}
     * in accordance with DLNA comma escaping rules.
     *
     * @param unescapedString the {@code protocolInfo} string to escape.
     * @return The escaped {@link String};
     */
    public static String escapeString(String unescapedString) {
        return PROTOCOLINFO_ESCAPE.translate(unescapedString);
    }

    /**
     * Unescapes {@code protocolInfo} strings after splitting a string from
     * {@code GetProtocolInfo} into individual elements in accordance with DLNA
     * comma escaping rules.
     *
     * @param escapedString the {@code protocolInfo} string to unescape.
     * @return The unescaped {@link String};
     */
    public static String unescapeString(String escapedString) {
        return PROTOCOLINFO_UNESCAPE.translate(escapedString);
    }

    /**
     * Handles known special cases, i.e bugs in renderers' {@code protocolInfo}
     * output so that we are able to parse them despite them being broken.
     *
     * @param element the {@code protocolInfo} element to handle if needed.
     * @return {@code null} if {@code element} doesn't match a known special
     *         case, or a {@link SortedSet} of {@link ProtocolInfo} instances
     *         with the result of the parsed special case.
     * @throws ParseException If {@code element} is a known special case but the
     *             parsing fails.
     */
    public static SortedSet<ProtocolInfo> handleSpecialCaseString(String element) throws ParseException {
        if (isBlank(element)) {
            return null;
        }
        switch (element) {
        /*
         * Seen on a LG-BP550-1, missing comma between elements
         */
        case "http-get:*:audio/sonyoma:*http-get:*:audio/ogg:*":
            SortedSet<ProtocolInfo> currentSet = new TreeSet<>();
            currentSet.add(new ProtocolInfo("http-get:*:audio/sonyoma:*"));
            currentSet.add(new ProtocolInfo("http-get:*:audio/ogg:*"));
            return currentSet;
        }
        return null;
    }

    /**
     * This is an abstract implementation of {@link DeviceProtocolInfoSource}
     * where {@link DeviceProtocolInfo} is the parsing class.
     *
     * @author Nadahar
     */
    public abstract static class GetProtocolInfoType extends DeviceProtocolInfoSource<DeviceProtocolInfo> {

        private static final long serialVersionUID = 1L;

        @Override
        public Class<DeviceProtocolInfo> getClazz() {
            return DeviceProtocolInfo.class;
        }
    }
}