/*
* 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";
}
}
}