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

Java tutorial

Introduction

Here is the source code for net.pms.dlna.protocolinfo.PanasonicDmpProfiles.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.Collections;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.fourthline.cling.support.model.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.protocolinfo.PanasonicComProfileName.KnownPanasonicComProfileName;
import net.pms.util.ParseException;

/**
 * This class handles a Panasonic DMP's {@link ProfileName}s from the custom
 * {@code X-PANASONIC-DMP-Profile} HTTP header field.
 * <p>
 * The {@code X-PANASONIC-DMP-Profile} doesn't provide {@code protocolInfo}
 * information, it simply lists a set of supported DLNA and non-DLNA format
 * profiles. This class still makes {@link ProtocolInfo} instances that
 * "represent" the listed profiles when this can be "reasonably deducted". This
 * allows the information given in {@code X-PANASONIC-DMP-Profile} to be
 * accessed as a "drop in replacement" for the information gotten from renderers
 * with {@code GetProtocolInfo}.
 * <p>
 * Any instance of {@link PanasonicDmpProfiles} must be "linked" with a
 * {@link DeviceProtocolInfo} instance that represents the same device. The
 * {@link ProtocolInfo} instances parsed or added is stored in this "linked"
 * instance.
 * <p>
 * This class is thread-safe.
 *
 * @author Nadahar
 */
public class PanasonicDmpProfiles implements Serializable {

    private static final long serialVersionUID = 1L;

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

    /** The static singleton {@code X-PANASONIC-DMP-Profile} identifier */
    public static final DeviceProtocolInfoSource<PanasonicDmpProfiles> PANASONIC_DMP = new PanasonicDmpProfileType();

    /** The populated status */
    protected volatile boolean populated;

    /** The linked {@link DeviceProtocolInfo} */
    protected final DeviceProtocolInfo deviceProtocolInfo;

    /**
     * Creates a new empty instance.
     *
     * @param deviceProtocolInfo the {@link DeviceProtocolInfo} instance to link
     *            with the new instance.
     */
    public PanasonicDmpProfiles(DeviceProtocolInfo deviceProtocolInfo) {
        if (deviceProtocolInfo == null) {
            throw new IllegalArgumentException("deviceProtocolInfo cannot be null");
        }
        this.deviceProtocolInfo = deviceProtocolInfo;
    }

    /**
     * Creates a new instance, tries to parse {@code dmpProfileString} and adds
     * the resulting {@link ProtocolInfo} instances to the {@link Set} of
     * {@link ProtocolInfo} in {@code deviceProtocolInfo}.
     *
     * @param deviceProtocolInfo the {@link DeviceProtocolInfo} instance to link
     *            with the new instance.
     * @param dmpProfilesString a comma separated string of {@code protocolInfo}
     *            representations.
     */
    public PanasonicDmpProfiles(DeviceProtocolInfo deviceProtocolInfo, String dmpProfilesString) {
        if (deviceProtocolInfo == null) {
            throw new IllegalArgumentException("deviceProtocolInfo cannot be null");
        }
        this.deviceProtocolInfo = deviceProtocolInfo;
        add(dmpProfilesString);
    }

    /**
     * @return {@code true} if the {@link Set} of {@link ProtocolInfo} in
     *         {@code deviceProtocolInfo} has already been populated with
     *         information from a {@code X-PANASONIC-DMP-Profile} field,
     *         {@code false} otherwise.
     */
    public boolean isPopulated() {
        return populated;
    }

    /**
     * Tries to parse {@code dmpProfilesString} and adds the resulting
     * {@link ProtocolInfo} instances to the {@link Set} of type
     * {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}.
     *
     * @param dmpProfilesString a space separated string of format profile
     *            representations whose presence is to be ensured.
     * @return {@code true} if the {@link Set} of {@link ProtocolInfo} in
     *         {@code deviceProtocolInfo} changed as a result of the call.
     *         Returns {@code false} this already contains the specified
     *         elements.
     */
    public boolean add(String dmpProfilesString) {
        if (StringUtils.isBlank(dmpProfilesString)) {
            return false;
        }

        dmpProfilesString = dmpProfilesString.replaceFirst("\\s*X-PANASONIC-DMP-Profile:\\s*", "").trim();
        String[] elements = dmpProfilesString.trim().split("\\s+");

        SortedSet<ProtocolInfo> protocolInfoSet = new TreeSet<>();
        for (String element : elements) {
            try {
                ProtocolInfo protocolInfo = dmpProfileToProtocolInfo(element);
                if (protocolInfo != null) {
                    protocolInfoSet.add(protocolInfo);
                }
            } catch (ParseException e) {
                LOGGER.warn("Unable to parse protocolInfo from \"{}\", this profile will not be registered: {}",
                        element, e.getMessage());
                LOGGER.trace("", e);
            }
        }
        boolean result = false;
        if (!protocolInfoSet.isEmpty()) {
            result = deviceProtocolInfo.addAll(PANASONIC_DMP, protocolInfoSet);
        }

        populated |= result;
        return result;
    }

    // Standard java.util.Collection methods

    /**
     * Returns the number of elements of type {@link PanasonicDmpProfileType} in
     * {@code deviceProtocolInfo}. If this contains more than
     * {@link Integer#MAX_VALUE} elements, returns {@link Integer#MAX_VALUE}.
     *
     * @return The number of elements in the {@link Set} for
     *         {@link PanasonicDmpProfiles}.
     */
    public int size() {
        return deviceProtocolInfo.size(PANASONIC_DMP);
    }

    /**
     * Checks if the {@link Set} of type {@link PanasonicDmpProfileType} in
     * {@code deviceProtocolInfo} is empty.
     *
     * @return {@code true} if {@code deviceProtocolInfo} contains no elements
     *         of type {@link PanasonicDmpProfileType}, {@code false} otherwise.
     */
    public boolean isEmpty() {
        return deviceProtocolInfo.isEmpty(PANASONIC_DMP);
    }

    /**
     * Returns {@code true} if the {@link Set} of type
     * {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo} contains
     * the specified element.
     *
     * @param protocolInfo the element whose presence is to be tested.
     * @return {@code true} if the {@link Set} of type
     *         {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}
     *         contains the specified element, {@code false} otherwise.
     */
    public boolean contains(ProtocolInfo protocolInfo) {
        return deviceProtocolInfo.contains(PANASONIC_DMP, protocolInfo);
    }

    /**
     * Returns a sorted array containing all of the elements of the {@link Set}
     * of type {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}.
     * <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 the {@link ProtocolInfo} instances.
     */
    public ProtocolInfo[] toArray() {
        return deviceProtocolInfo.toArray(PANASONIC_DMP);
    }

    /**
     * Returns {@code true} if the {@link Set} of type
     * {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo} contains
     * all of the elements in the specified collection.
     *
     * @param collection a {@link Collection} to be checked for containment.
     * @return {@code true} if the {@link Set} of type
     *         {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}
     *         collection contains all of the elements in {@code collection}.
     *
     * @see #contains(ProtocolInfo))
     */
    public boolean containsAll(Collection<ProtocolInfo> collection) {
        return deviceProtocolInfo.containsAll(PANASONIC_DMP, collection);
    }

    /**
     * Removes all elements from the {@link Set} of type
     * {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}.
     */
    public void clear() {
        deviceProtocolInfo.clear(PANASONIC_DMP);
        populated = false;
    }

    /**
     * Ensures that the the {@link Set} of type {@link PanasonicDmpProfileType}
     * in {@code deviceProtocolInfo} contains the specified element. Returns
     * {@code true} if the {@link Set} changed as a result of the call. Returns
     * {@code false} the {@link Set} already contains the specified element.
     *
     * @param protocolInfo element whose presence is to be ensured.
     * @return {@code true} if the the {@link Set} of type
     *         {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}
     *         changed as a result of the call, {@code false} otherwise.
     */
    public boolean add(ProtocolInfo protocolInfo) {
        if (deviceProtocolInfo.add(PANASONIC_DMP, protocolInfo)) {
            populated = true;
            return true;
        }
        return false;
    }

    /**
     * Adds all of the elements in the specified collection to the {@link Set}
     * of type {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}.
     *
     * @param collection a {@link Collection} containing the elements to be
     *            added.
     * @return {@code true} if the {@link Set} of type
     *         {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}
     *         changed as a result of the call, {@code false} otherwise.
     *
     * @see #add(ProtocolInfo)
     */
    public boolean addAll(Collection<? extends ProtocolInfo> collection) {
        if (deviceProtocolInfo.addAll(PANASONIC_DMP, collection)) {
            populated = true;
            return true;
        }
        return false;
    }

    /**
     * Removes a single instance of {@link ProtocolInfo} from the {@link Set} of
     * type {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo}, if it
     * is present. Returns {@code true} if the {@link Set} contained the
     * specified element (or equivalently, if the {@link Set} of type
     * {@link PanasonicDmpProfileType} in {@code deviceProtocolInfo} 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) {
        return deviceProtocolInfo.remove(PANASONIC_DMP, protocolInfo);
    }

    /**
     * Removes all elements that are also contained in {@code collection} from
     * the {@link Set} of type {@link PanasonicDmpProfileType} in
     * {@code deviceProtocolInfo}.
     *
     * @param collection a {@link Collection} containing the elements to be
     *            removed.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(ProtocolInfo)
     * @see #contains(ProtocolInfo)
     */
    public boolean removeAll(Collection<ProtocolInfo> collection) {
        if (deviceProtocolInfo.removeAll(PANASONIC_DMP, collection)) {
            if (deviceProtocolInfo.isEmpty(PANASONIC_DMP)) {
                populated = false;
            }
            return true;
        }
        return false;
    }

    /**
     * Retains only the elements that are contained in {@code collection} in the
     * {@link Set} of type {@link PanasonicDmpProfileType} in
     * {@code deviceProtocolInfo}. In other words, removes all elements that are
     * not contained in {@code collection}.
     *
     * @param collection a {@link Collection} containing the elements to be
     *            retained.
     * @return {@code true} if this call resulted in a change.
     *
     * @see #remove(ProtocolInfo)
     * @see #contains(ProtocolInfo)
     */
    public boolean retainAll(Collection<ProtocolInfo> collection) {
        if (deviceProtocolInfo.removeAll(collection)) {
            if (deviceProtocolInfo.isEmpty()) {
                populated = false;
            }
            return true;
        }
        return false;
    }

    @Override
    public String toString() {
        return deviceProtocolInfo.toString(PANASONIC_DMP, false);
    }

    /**
     * Returns a string representation of the {@link Set} of type
     * {@link PanasonicDmpProfileType} in the linked {@link DeviceProtocolInfo}
     * instance. If {@code debug} is {@code true}, verbose output is returned.
     *
     * @param debug whether or not verbose output should be generated.
     * @return The string representation.
     */
    public String toString(boolean debug) {
        return deviceProtocolInfo.toString(PANASONIC_DMP, debug);
    }

    // Static methods

    /**
     * Creates a {@link DeviPanasonicDmpProfiles} instance for {@code renderer}
     * as needed. Parses {@code dmpProfilesString} and store the results in the
     * linked {@link DeviceProtocolInfo} instance. This method assumes that the
     * renderer sends the same string every time.
     *
     * @param dmpProfilesString the {@code X-PANASONIC-DMP-Profile} string to
     *            parse.
     * @param renderer the {@link RendererConfiguration} for which to apply the
     *            parsing results.
     * @throws IllegalStateException If {@code renderer}'s
     *             {@code deviceProtocolInfo} is {@code null}.
     */
    public static void parsePanasonicDmpProfiles(String dmpProfilesString, RendererConfiguration renderer) {
        if (renderer == null) {
            return;
        }
        if (renderer.deviceProtocolInfo == null) {
            throw new IllegalStateException(
                    "Panasonic DMP profiles cannot be parsed before the renderer's deviceProtocolInfo is instantiated");
        }
        boolean added = false;
        if (renderer.panasonicDmpProfiles == null) {
            renderer.panasonicDmpProfiles = new PanasonicDmpProfiles(renderer.deviceProtocolInfo,
                    dmpProfilesString);
            added = true;
        } else if (!renderer.panasonicDmpProfiles.isPopulated()) {
            added = renderer.panasonicDmpProfiles.add(dmpProfilesString);
        }
        if (added && LOGGER.isTraceEnabled() && !renderer.panasonicDmpProfiles.isEmpty()) {
            LOGGER.trace("Received X-PANASONIC-DMP-Profiles from \"{}\":\n\n{}", renderer.getConfName(),
                    renderer.panasonicDmpProfiles);
        }
    }

    /**
     * Tries to convert an element from a {@code X-PANASONIC-DMP-Profile} string
     * to a {@link ProtocolInfo} instance. This is a manual mapping which must
     * be extended when new profiles are discovered.
     *
     * @param dmpProfile the {@code X-PANASONIC-DMP-Profile} string element.
     * @return A new {@link ProtocolInfo} instance if one could be created, or
     *         {@code null}.
     * @throws ParseException If there is a problem while parsing
     *             {@code dmpProfile}.
     */
    public static ProtocolInfo dmpProfileToProtocolInfo(String dmpProfile) throws ParseException {
        if (isBlank(dmpProfile)) {
            return null;
        }
        dmpProfile = dmpProfile.trim();
        ProtocolInfoAttribute attribute = DLNAOrgProfileName.FACTORY.getProfileName(dmpProfile);
        if (attribute != null) {
            // Mime-types must be mapped manually, as this information is missing from X-PANASONIC-DMP-Profile
            MimeType mimeType;
            if (attribute.getValue().startsWith("JPEG")) {
                mimeType = new MimeType("image", "jpeg");
            } else if (attribute.getValue().startsWith("PNG")) {
                mimeType = new MimeType("image", "png");
            } else if (attribute.getValue().startsWith("GIF")) {
                mimeType = new MimeType("image", "gif");
            } else if (attribute.getValue().startsWith("MPEG")) {
                mimeType = new MimeType("video", "mpeg");
            } else if (attribute.getValue().startsWith("AC3")) {
                mimeType = new MimeType("audio", "vnd.dolby.dd-raw");
            } else if (attribute.getValue().startsWith("AMR")) {
                mimeType = new MimeType("audio", "3gpp");
            } else if (attribute.getValue().startsWith("LPCM")) {
                mimeType = new MimeType("audio", "L16");
            } else if (attribute.getValue().startsWith("MP2")) {
                mimeType = new MimeType("audio", "mpeg");
            } else if (attribute.getValue().startsWith("MP3")) {
                mimeType = new MimeType("audio", "mpeg");
            } else if (attribute.getValue().startsWith("AAC")) {
                mimeType = new MimeType("audio", "mp4");
            } else if (attribute.getValue().startsWith("WMA")) {
                mimeType = new MimeType("audio", "x-ms-wma");
            } else if (attribute.getValue().startsWith("MPEG4")) {
                mimeType = new MimeType("video", "mp4");
            } else if (attribute.getValue().startsWith("MPEG")) {
                mimeType = new MimeType("video", "mpeg");
            } else if (attribute.getValue().startsWith("WMV")) {
                mimeType = new MimeType("video", "x-ms-wmv");
            } else if (attribute.getValue().startsWith("VC1")) {
                mimeType = new MimeType("video", "mpeg");
            } else {
                throw new ParseException("Can't infer mime-type for \"" + attribute + "\"");
            }
            return new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType,
                    Collections.singletonMap(attribute.getName(), attribute));
        }
        attribute = PanasonicComProfileName.FACTORY.getProfileName(dmpProfile);
        if (attribute != null) {
            // Mime-types must be mapped manually, as this information is missing from X-PANASONIC-DMP-Profile
            if (attribute instanceof KnownPanasonicComProfileName) {
                MimeType mimeType;
                switch ((KnownPanasonicComProfileName) attribute) {
                case MPO_3D:
                    mimeType = new MimeType("image", "mpo");
                    break;
                case PV_DIVX_DIV3:
                case PV_DIVX_DIV4:
                case PV_DIVX_DIVX:
                case PV_DIVX_DX50:
                    mimeType = new MimeType("video", "divx");
                    break;
                case PV_DRM_DIVX_DIV3:
                case PV_DRM_DIVX_DIV4:
                case PV_DRM_DIVX_DIVX:
                case PV_DRM_DIVX_DX50:
                    // No idea what mime-type they use for DRM, or how to handle them
                    return null;
                default:
                    throw new ParseException("Unimplemented PANASONIC.COM_PN profile \"" + attribute + "\"");
                }
                return new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType,
                        Collections.singletonMap(attribute.getName(), attribute));
            }
            throw new ParseException("Can't infer mime-type for \"" + attribute + "\"");
        }

        // No match found for known DLNA.ORG_PN or PANASONIC.COM_PN profiles
        LOGGER.debug("Warning: Unable to parse X-PANASONIC-DMP-Profile \"{}\"", dmpProfile);
        return null;
    }

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

        private static final long serialVersionUID = 1L;

        /**
         * Not to be instantiated, use
         * {@link PanasonicDmpProfiles#PANASONIC_DMP} instead.
         */
        protected PanasonicDmpProfileType() {
        }

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

        @Override
        public String getType() {
            return "X-PANASONIC-DMP-Profile";
        }
    }

}