/*
* This software copyright by various authors including the RPTools.net
* development team, and licensed under the LGPL Version 3 or, at your
* option, any later version.
*
* Portions of this software were originally covered under the Apache
* Software License, Version 1.1 or Version 2.0.
*
* See the file LICENSE elsewhere in this distribution for license details.
*/
package net.sbbi.upnp.jmx;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import net.sbbi.upnp.services.ServiceStateVariable;
import net.sbbi.upnp.services.ServiceStateVariableTypes;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Class to expose an JMX Mbean as an UPNP device service, this class shouldn't be used directly
*
* @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
* @version 1.0
*/
public class UPNPMBeanService {
private final static Log log = LogFactory.getLog(UPNPMBeanService.class);
private final String serviceType;
private final String serviceId;
private final String serviceUUID;
private final String deviceUUID;
private final String deviceSCDP;
private Map<String, String> operationsStateVariables;
private final MBeanServer targetServer;
private final MBeanInfo mbeanInfo;
private final ObjectName mbeanName;
private Class<?> targetBeanClass;
protected UPNPMBeanService(String deviceUUID, String vendorDomain, String serviceId, String serviceType, int serviceVersion,
MBeanInfo mbeanInfo, ObjectName mbeanName, MBeanServer targetServer) throws IOException {
this.deviceUUID = deviceUUID;
if (serviceId == null) {
serviceId = generateServiceId(mbeanName);
}
this.serviceUUID = deviceUUID + "/" + serviceId;
this.serviceType = "urn:" + vendorDomain + ":service:" + serviceType + ":" + serviceVersion;
this.serviceId = "urn:" + vendorDomain + ":serviceId:" + serviceId;
deviceSCDP = getDeviceSSDP(mbeanInfo);
try {
targetBeanClass = Class.forName(mbeanInfo.getClassName());
} catch (ClassNotFoundException ex) {
IOException ex2 = new IOException("Unable to find target MBean class " + mbeanInfo.getClassName());
ex2.initCause(ex);
throw ex2;
}
this.mbeanName = mbeanName;
this.mbeanInfo = mbeanInfo;
this.targetServer = targetServer;
}
protected String getServiceUUID() {
return serviceUUID;
}
protected String getDeviceUUID() {
return deviceUUID;
}
private String generateServiceId(ObjectName mbeanName) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
// the uuid is based on the device type, the internal id
// and the host name
md5.update(mbeanName.toString().getBytes());
StringBuffer hexString = new StringBuffer();
byte[] digest = md5.digest();
for (int i = 0; i < digest.length; i++) {
hexString.append(Integer.toHexString(0xFF & digest[i]));
}
return hexString.toString().toUpperCase();
} catch (Exception ex) {
RuntimeException runTimeEx = new RuntimeException("Unexpected error during MD5 hash creation, check your JRE");
runTimeEx.initCause(ex);
throw runTimeEx;
}
}
protected String getServiceInfo() {
StringBuffer rtrVal = new StringBuffer();
rtrVal.append("<service>\r\n");
rtrVal.append("<serviceType>").append(serviceType).append("</serviceType>\r\n");
rtrVal.append("<serviceId>").append(serviceId).append("</serviceId>\r\n");
rtrVal.append("<controlURL>").append("/").append(serviceUUID).append("/control").append("</controlURL>\r\n");
rtrVal.append("<eventSubURL>").append("/").append(serviceUUID).append("/events").append("</eventSubURL>\r\n");
rtrVal.append("<SCPDURL>").append("/").append(serviceUUID).append("/scpd.xml").append("</SCPDURL>\r\n");
rtrVal.append("</service>\r\n");
return rtrVal.toString();
}
protected Map<String, String> getOperationsStateVariables() {
return operationsStateVariables;
}
protected String getDeviceSCDP() {
return deviceSCDP;
}
protected String getServiceType() {
return serviceType;
}
protected Class<?> getTargetBeanClass() {
return targetBeanClass;
}
protected ObjectName getObjectName() {
return mbeanName;
}
protected Object getAttribute(String attributeName) throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException {
return targetServer.getAttribute(mbeanName, attributeName);
}
protected Object invoke(String actionName, Object[] params, String[] signature) throws ReflectionException, InstanceNotFoundException, MBeanException {
return targetServer.invoke(mbeanName, actionName, params, signature);
}
private String getDeviceSSDP(MBeanInfo info) throws IllegalArgumentException {
MBeanOperationInfo[] ops = info.getOperations();
MBeanAttributeInfo[] atts = info.getAttributes();
if ((ops == null || ops.length == 0) &&
(atts == null || atts.length == 0)) {
throw new IllegalArgumentException("MBean has no operation and no attribute and cannot be exposed as an UPNP device, provide at least one attribute");
}
Set<String> deployedActionNames = new HashSet<String>();
operationsStateVariables = new HashMap<String, String>();
StringBuffer rtrVal = new StringBuffer();
rtrVal.append("<?xml version=\"1.0\" ?>\r\n");
rtrVal.append("<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">\r\n");
rtrVal.append("<specVersion><major>1</major><minor>0</minor></specVersion>\r\n");
if (ops != null && ops.length > 0) {
rtrVal.append("<actionList>\r\n");
for (int i = 0; i < ops.length; i++) {
MBeanOperationInfo op = ops[i];
StringBuffer action = new StringBuffer();
if (deployedActionNames.contains(op.getName())) {
log.debug("The " + op.getName() + " is allready deplyoed and cannot be reused, skipping operation deployment");
continue;
}
action.append("<action>\r\n");
action.append("<name>");
action.append(op.getName());
action.append("</name>\r\n");
action.append("<argumentList>\r\n");
// output argument
action.append("<argument>\r\n");
action.append("<name>");
// TODO handle specific output vars
String outVarName = op.getName() + "_out";
String actionOutDataType = ServiceStateVariable.getUPNPDataTypeMapping(op.getReturnType());
if (actionOutDataType == null)
actionOutDataType = ServiceStateVariableTypes.STRING;
action.append(outVarName);
action.append("</name>\r\n");
action.append("<direction>out</direction>\r\n");
action.append("<relatedStateVariable>");
action.append(outVarName);
action.append("</relatedStateVariable>\r\n");
action.append("</argument>\r\n");
// handle now for all input argument
boolean nonPrimitiveInputType = false;
boolean duplicatedInputVarname = false;
Map<String, String> operationsInputStateVariables = new HashMap<String, String>();
if (op.getSignature() != null) {
for (int z = 0; z < op.getSignature().length; z++) {
MBeanParameterInfo param = op.getSignature()[z];
// do some sanity checks
String actionInDataType = ServiceStateVariable.getUPNPDataTypeMapping(param.getType());
if (actionInDataType == null) {
nonPrimitiveInputType = true;
log.debug("The " + param.getType() + " type is not an UPNP compatible data type, use only primitives");
break;
}
String inVarName = param.getName();
// check that if the name does allready exists it
// has the same type
String existing = operationsStateVariables.get(inVarName);
if (existing != null && !existing.equals(actionInDataType)) {
String msg = "The operation " + op.getName() + " " + inVarName +
" parameter already exists for another method with another data type (" +
existing + ") either match the data type or change the parameter name" +
" in you MBeanParameterInfo object for this operation";
duplicatedInputVarname = true;
log.debug(msg);
break;
}
action.append("<argument>\r\n");
action.append("<name>");
operationsInputStateVariables.put(inVarName, actionInDataType);
action.append(inVarName);
action.append("</name>\r\n");
action.append("<direction>in</direction>\r\n");
action.append("<relatedStateVariable>");
action.append(inVarName);
action.append("</relatedStateVariable>\r\n");
action.append("</argument>\r\n");
}
}
action.append("</argumentList>\r\n");
action.append("</action>\r\n");
// finally the action is only added to the UPNP SSDP if no problems have been detected
// with the input parameters type and names.
if (!nonPrimitiveInputType && !duplicatedInputVarname) {
operationsStateVariables.putAll(operationsInputStateVariables);
operationsStateVariables.put(outVarName, actionOutDataType);
rtrVal.append(action.toString());
deployedActionNames.add(op.getName());
}
}
rtrVal.append("</actionList>\r\n");
} else {
rtrVal.append("<actionList/>\r\n");
}
// now append the operation created state vars
rtrVal.append("<serviceStateTable>\r\n");
for (Iterator<String> i = operationsStateVariables.keySet().iterator(); i.hasNext();) {
String name = i.next();
String type = operationsStateVariables.get(name);
// TODO handle sendevents with mbean notifications ???
// TODO handle defaultValue and allowedValueList values
rtrVal.append("<stateVariable sendEvents=\"no\">\r\n");
rtrVal.append("<name>");
rtrVal.append(name);
rtrVal.append("</name>\r\n");
rtrVal.append("<dataType>");
rtrVal.append(type);
rtrVal.append("</dataType>\r\n");
rtrVal.append("</stateVariable>\r\n");
}
if (atts != null && atts.length > 0) {
for (int i = 0; i < atts.length; i++) {
MBeanAttributeInfo att = atts[i];
if (att.isReadable()) {
rtrVal.append("<stateVariable sendEvents=\"no\">\r\n");
rtrVal.append("<name>");
rtrVal.append(att.getName());
rtrVal.append("</name>\r\n");
rtrVal.append("<dataType>");
// TODO check if this works
String stateVarType = ServiceStateVariable.getUPNPDataTypeMapping(att.getType());
if (stateVarType == null)
stateVarType = ServiceStateVariableTypes.STRING;
rtrVal.append(stateVarType);
rtrVal.append("</dataType>\r\n");
rtrVal.append("</stateVariable>\r\n");
}
}
}
rtrVal.append("</serviceStateTable>\r\n");
rtrVal.append("</scpd>");
return rtrVal.toString();
}
public MBeanInfo getMBeanInfo() {
return mbeanInfo;
}
}