() {
+ }).fire(
+ new RegistryShutdown()
+ );
+ }
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/ManagedUpnpServiceConfiguration.java b/clinglibrary/src/main/java/org/fourthline/cling/ManagedUpnpServiceConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..3166fce2dfa3d618a08d5afda6553ca06a23ed5b
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/ManagedUpnpServiceConfiguration.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling;
+
+import org.fourthline.cling.binding.xml.DeviceDescriptorBinder;
+import org.fourthline.cling.binding.xml.ServiceDescriptorBinder;
+import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl;
+import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl;
+import org.fourthline.cling.model.ModelUtil;
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.message.UpnpHeaders;
+import org.fourthline.cling.model.meta.RemoteDeviceIdentity;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.transport.impl.DatagramIOConfigurationImpl;
+import org.fourthline.cling.transport.impl.DatagramIOImpl;
+import org.fourthline.cling.transport.impl.GENAEventProcessorImpl;
+import org.fourthline.cling.transport.impl.MulticastReceiverConfigurationImpl;
+import org.fourthline.cling.transport.impl.MulticastReceiverImpl;
+import org.fourthline.cling.transport.impl.NetworkAddressFactoryImpl;
+import org.fourthline.cling.transport.impl.SOAPActionProcessorImpl;
+import org.fourthline.cling.transport.impl.StreamClientConfigurationImpl;
+import org.fourthline.cling.transport.impl.StreamClientImpl;
+import org.fourthline.cling.transport.impl.StreamServerConfigurationImpl;
+import org.fourthline.cling.transport.impl.StreamServerImpl;
+import org.fourthline.cling.transport.spi.DatagramIO;
+import org.fourthline.cling.transport.spi.DatagramProcessor;
+import org.fourthline.cling.transport.spi.GENAEventProcessor;
+import org.fourthline.cling.transport.spi.MulticastReceiver;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+import org.fourthline.cling.transport.spi.SOAPActionProcessor;
+import org.fourthline.cling.transport.spi.StreamClient;
+import org.fourthline.cling.transport.spi.StreamServer;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Logger;
+
+/**
+ * Adapter for CDI environments.
+ *
+ * @author Christian Bauer
+ */
+@ApplicationScoped
+public class ManagedUpnpServiceConfiguration implements UpnpServiceConfiguration {
+
+ private static Logger log = Logger.getLogger(DefaultUpnpServiceConfiguration.class.getName());
+
+ // TODO: All of these fields should be injected so users can provide values through CDI
+
+ private int streamListenPort;
+
+ private ExecutorService defaultExecutorService;
+
+ @Inject
+ protected DatagramProcessor datagramProcessor;
+
+ private SOAPActionProcessor soapActionProcessor;
+ private GENAEventProcessor genaEventProcessor;
+
+ private DeviceDescriptorBinder deviceDescriptorBinderUDA10;
+ private ServiceDescriptorBinder serviceDescriptorBinderUDA10;
+
+ private Namespace namespace;
+
+ @PostConstruct
+ public void init() {
+
+ if (ModelUtil.ANDROID_RUNTIME) {
+ throw new Error("Unsupported runtime environment, use org.fourthline.cling.android.AndroidUpnpServiceConfiguration");
+ }
+
+ this.streamListenPort = NetworkAddressFactoryImpl.DEFAULT_TCP_HTTP_LISTEN_PORT;
+
+ defaultExecutorService = createDefaultExecutorService();
+
+ soapActionProcessor = createSOAPActionProcessor();
+ genaEventProcessor = createGENAEventProcessor();
+
+ deviceDescriptorBinderUDA10 = createDeviceDescriptorBinderUDA10();
+ serviceDescriptorBinderUDA10 = createServiceDescriptorBinderUDA10();
+
+ namespace = createNamespace();
+ }
+
+ public DatagramProcessor getDatagramProcessor() {
+ return datagramProcessor;
+ }
+
+ public SOAPActionProcessor getSoapActionProcessor() {
+ return soapActionProcessor;
+ }
+
+ public GENAEventProcessor getGenaEventProcessor() {
+ return genaEventProcessor;
+ }
+
+ public StreamClient createStreamClient() {
+ return new StreamClientImpl(
+ new StreamClientConfigurationImpl(
+ getSyncProtocolExecutorService()
+ )
+ );
+ }
+
+ public MulticastReceiver createMulticastReceiver(NetworkAddressFactory networkAddressFactory) {
+ return new MulticastReceiverImpl(
+ new MulticastReceiverConfigurationImpl(
+ networkAddressFactory.getMulticastGroup(),
+ networkAddressFactory.getMulticastPort()
+ )
+ );
+ }
+
+ public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) {
+ return new DatagramIOImpl(new DatagramIOConfigurationImpl());
+ }
+
+ public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
+ return new StreamServerImpl(
+ new StreamServerConfigurationImpl(
+ networkAddressFactory.getStreamListenPort()
+ )
+ );
+ }
+
+ public Executor getMulticastReceiverExecutor() {
+ return getDefaultExecutorService();
+ }
+
+ public Executor getDatagramIOExecutor() {
+ return getDefaultExecutorService();
+ }
+
+ public ExecutorService getStreamServerExecutorService() {
+ return getDefaultExecutorService();
+ }
+
+ public DeviceDescriptorBinder getDeviceDescriptorBinderUDA10() {
+ return deviceDescriptorBinderUDA10;
+ }
+
+ public ServiceDescriptorBinder getServiceDescriptorBinderUDA10() {
+ return serviceDescriptorBinderUDA10;
+ }
+
+ public ServiceType[] getExclusiveServiceTypes() {
+ return new ServiceType[0];
+ }
+
+ /**
+ * @return Defaults to false
.
+ */
+ public boolean isReceivedSubscriptionTimeoutIgnored() {
+ return false;
+ }
+
+ public UpnpHeaders getDescriptorRetrievalHeaders(RemoteDeviceIdentity identity) {
+ return null;
+ }
+
+ public UpnpHeaders getEventSubscriptionHeaders(RemoteService service) {
+ return null;
+ }
+
+ /**
+ * @return Defaults to 1000 milliseconds.
+ */
+ public int getRegistryMaintenanceIntervalMillis() {
+ return 1000;
+ }
+
+ /**
+ * @return Defaults to zero, disabling ALIVE flooding.
+ */
+ public int getAliveIntervalMillis() {
+ return 0;
+ }
+
+ public Integer getRemoteDeviceMaxAgeSeconds() {
+ return null;
+ }
+
+ public Executor getAsyncProtocolExecutor() {
+ return getDefaultExecutorService();
+ }
+
+ public ExecutorService getSyncProtocolExecutorService() {
+ return getDefaultExecutorService();
+ }
+
+ public Namespace getNamespace() {
+ return namespace;
+ }
+
+ public Executor getRegistryMaintainerExecutor() {
+ return getDefaultExecutorService();
+ }
+
+ public Executor getRegistryListenerExecutor() {
+ return getDefaultExecutorService();
+ }
+
+ public NetworkAddressFactory createNetworkAddressFactory() {
+ return createNetworkAddressFactory(streamListenPort);
+ }
+
+ public void shutdown() {
+ log.fine("Shutting down default executor service");
+ getDefaultExecutorService().shutdownNow();
+ }
+
+ protected NetworkAddressFactory createNetworkAddressFactory(int streamListenPort) {
+ return new NetworkAddressFactoryImpl(streamListenPort);
+ }
+
+ protected SOAPActionProcessor createSOAPActionProcessor() {
+ return new SOAPActionProcessorImpl();
+ }
+
+ protected GENAEventProcessor createGENAEventProcessor() {
+ return new GENAEventProcessorImpl();
+ }
+
+ protected DeviceDescriptorBinder createDeviceDescriptorBinderUDA10() {
+ return new UDA10DeviceDescriptorBinderImpl();
+ }
+
+ protected ServiceDescriptorBinder createServiceDescriptorBinderUDA10() {
+ return new UDA10ServiceDescriptorBinderImpl();
+ }
+
+ protected Namespace createNamespace() {
+ return new Namespace();
+ }
+
+ protected ExecutorService getDefaultExecutorService() {
+ return defaultExecutorService;
+ }
+
+ protected ExecutorService createDefaultExecutorService() {
+ return new DefaultUpnpServiceConfiguration.ClingExecutor();
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/UpnpService.java b/clinglibrary/src/main/java/org/fourthline/cling/UpnpService.java
new file mode 100644
index 0000000000000000000000000000000000000000..efda5287d8ac1a8b992c35172932d2d103f07c74
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/UpnpService.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling;
+
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.transport.Router;
+
+/**
+ * Primary interface of the Cling Core UPnP stack.
+ *
+ * An implementation can either start immediately when constructed or offer an additional
+ * method that starts the UPnP stack on-demand. Implementations are not required to be
+ * restartable after shutdown.
+ *
+ *
+ * Implementations are always thread-safe and can be shared and called concurrently.
+ *
+ *
+ * @author Christian Bauer
+ */
+public interface UpnpService {
+
+ public UpnpServiceConfiguration getConfiguration();
+
+ public ControlPoint getControlPoint();
+
+ public ProtocolFactory getProtocolFactory();
+
+ public Registry getRegistry();
+
+ public Router getRouter();
+
+ /**
+ * Stopping the UPnP stack.
+ *
+ * Clients are required to stop the UPnP stack properly. Notifications for
+ * disappearing devices will be multicast'ed, existing event subscriptions cancelled.
+ *
+ */
+ public void shutdown();
+
+ static public class Start {
+
+ }
+
+ static public class Shutdown {
+
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java b/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f069796200cb29b7718e7499ab7bdd2e43b03ad
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling;
+
+import org.fourthline.cling.binding.xml.DeviceDescriptorBinder;
+import org.fourthline.cling.binding.xml.ServiceDescriptorBinder;
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.message.UpnpHeaders;
+import org.fourthline.cling.model.meta.RemoteDeviceIdentity;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.transport.spi.DatagramIO;
+import org.fourthline.cling.transport.spi.DatagramProcessor;
+import org.fourthline.cling.transport.spi.GENAEventProcessor;
+import org.fourthline.cling.transport.spi.MulticastReceiver;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+import org.fourthline.cling.transport.spi.SOAPActionProcessor;
+import org.fourthline.cling.transport.spi.StreamClient;
+import org.fourthline.cling.transport.spi.StreamServer;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Shared configuration data of the UPnP stack.
+ *
+ * This interface offers methods for retrieval of configuration data by the
+ * {@link org.fourthline.cling.transport.Router} and the {@link org.fourthline.cling.registry.Registry},
+ * as well as other parts of the UPnP stack.
+ *
+ *
+ * You can re-use this interface if you implement a subclass of {@link UpnpServiceImpl} or
+ * if you create a new implementation of {@link UpnpService}.
+ *
+ *
+ * @author Christian Bauer
+ */
+public interface UpnpServiceConfiguration {
+
+ /**
+ * @return A new instance of the {@link org.fourthline.cling.transport.spi.NetworkAddressFactory} interface.
+ */
+ public NetworkAddressFactory createNetworkAddressFactory();
+
+ /**
+ * @return The shared implementation of {@link org.fourthline.cling.transport.spi.DatagramProcessor}.
+ */
+ public DatagramProcessor getDatagramProcessor();
+
+ /**
+ * @return The shared implementation of {@link org.fourthline.cling.transport.spi.SOAPActionProcessor}.
+ */
+ public SOAPActionProcessor getSoapActionProcessor();
+
+ /**
+ * @return The shared implementation of {@link org.fourthline.cling.transport.spi.GENAEventProcessor}.
+ */
+ public GENAEventProcessor getGenaEventProcessor();
+
+ /**
+ * @return A new instance of the {@link org.fourthline.cling.transport.spi.StreamClient} interface.
+ */
+ public StreamClient createStreamClient();
+
+ /**
+ * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}.
+ * @return A new instance of the {@link org.fourthline.cling.transport.spi.MulticastReceiver} interface.
+ */
+ public MulticastReceiver createMulticastReceiver(NetworkAddressFactory networkAddressFactory);
+
+ /**
+ * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}.
+ * @return A new instance of the {@link org.fourthline.cling.transport.spi.DatagramIO} interface.
+ */
+ public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory);
+
+ /**
+ * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}.
+ * @return A new instance of the {@link org.fourthline.cling.transport.spi.StreamServer} interface.
+ */
+ public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory);
+
+ /**
+ * @return The executor which runs the listening background threads for multicast datagrams.
+ */
+ public Executor getMulticastReceiverExecutor();
+
+ /**
+ * @return The executor which runs the listening background threads for unicast datagrams.
+ */
+ public Executor getDatagramIOExecutor();
+
+ /**
+ * @return The executor which runs the listening background threads for HTTP requests.
+ */
+ public ExecutorService getStreamServerExecutorService();
+
+ /**
+ * @return The shared implementation of {@link org.fourthline.cling.binding.xml.DeviceDescriptorBinder} for the UPnP 1.0 Device Architecture..
+ */
+ public DeviceDescriptorBinder getDeviceDescriptorBinderUDA10();
+
+ /**
+ * @return The shared implementation of {@link org.fourthline.cling.binding.xml.ServiceDescriptorBinder} for the UPnP 1.0 Device Architecture..
+ */
+ public ServiceDescriptorBinder getServiceDescriptorBinderUDA10();
+
+ /**
+ * Returns service types that can be handled by this UPnP stack, all others will be ignored.
+ *
+ * Return null
to completely disable remote device and service discovery.
+ * All incoming notifications and search responses will then be dropped immediately.
+ * This is mostly useful in applications that only provide services with no (remote)
+ * control point functionality.
+ *
+ *
+ * Note that a discovered service type with version 2 or 3 will match an exclusive
+ * service type with version 1. UPnP services are required to be backwards
+ * compatible, version 2 is a superset of version 1, and version 3 is a superset
+ * of version 2, etc.
+ *
+ *
+ * @return An array of service types that are exclusively discovered, no other service will
+ * be discovered. A null
return value will disable discovery!
+ * An empty array means all services will be discovered.
+ */
+ public ServiceType[] getExclusiveServiceTypes();
+
+ /**
+ * @return The time in milliseconds to wait between each registry maintenance operation.
+ */
+ public int getRegistryMaintenanceIntervalMillis();
+
+ /**
+ * Optional setting for flooding alive NOTIFY messages for local devices.
+ *
+ * Use this to advertise local devices at the specified interval, independent of its
+ * {@link org.fourthline.cling.model.meta.DeviceIdentity#maxAgeSeconds} value. Note
+ * that this will increase network traffic.
+ *
+ *
+ * Some control points (XBMC and other Platinum UPnP SDK based devices, OPPO-93) seem
+ * to not properly receive SSDP M-SEARCH replies sent by Cling, but will handle NOTIFY
+ * alive messages just fine.
+ *
+ *
+ * @return The time in milliseconds for ALIVE message intervals, set to 0
to disable
+ */
+ public int getAliveIntervalMillis();
+
+ /**
+ * Ignore the received event subscription timeout from remote control points.
+ *
+ * Some control points have trouble renewing subscriptions properly; enabling this option
+ * in conjunction with a high value for
+ * {@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS}
+ * ensures that your devices will not disappear on such control points.
+ *
+ *
+ * @return true
if the timeout in incoming event subscriptions should be ignored
+ * and the default value ({@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS})
+ * should be used instead.
+ *
+ */
+ public boolean isReceivedSubscriptionTimeoutIgnored();
+
+ /**
+ * Returns the time in seconds a remote device will be registered until it is expired.
+ *
+ * This setting is useful on systems which do not support multicast networking
+ * (Android on HTC phones, for example). On such a system you will not receive messages when a
+ * remote device disappears from the network and you will not receive its periodic heartbeat
+ * alive messages. Only an initial search response (UDP unicast) has been received from the
+ * remote device, with its proposed maximum age. To avoid (early) expiration of the remote
+ * device, you can override its maximum age with this configuration setting, ignoring the
+ * initial maximum age sent by the device. You most likely want to return
+ * 0
in this case, so that the remote device is never expired unless you
+ * manually remove it from the {@link org.fourthline.cling.registry.Registry}. You typically remove
+ * the device when an action or GENA subscription request to the remote device failed.
+ *
+ *
+ * @return null
(the default) to accept the remote device's proposed maximum age, or
+ * 0
for unlimited age, or a value in seconds.
+ */
+ public Integer getRemoteDeviceMaxAgeSeconds();
+
+ /**
+ * Optional extra headers for device descriptor retrieval HTTP requests.
+ *
+ * Some devices might require extra headers to recognize your control point, use this
+ * method to set these headers. They will be used for every descriptor (XML) retrieval
+ * HTTP request by Cling. See {@link org.fourthline.cling.model.profile.ClientInfo} for
+ * action request messages.
+ *
+ *
+ * @param identity The (so far) discovered identity of the remote device.
+ * @return null
or extra HTTP headers.
+ */
+ public UpnpHeaders getDescriptorRetrievalHeaders(RemoteDeviceIdentity identity);
+
+ /**
+ * Optional extra headers for event subscription (almost HTTP) messages.
+ *
+ * Some devices might require extra headers to recognize your control point, use this
+ * method to set these headers for GENA subscriptions. Note that the headers will
+ * not be applied to actual event messages, only subscribe, unsubscribe, and renewal.
+ *
+ *
+ * @return null
or extra HTTP headers.
+ */
+ public UpnpHeaders getEventSubscriptionHeaders(RemoteService service);
+
+ /**
+ * @return The executor which runs the processing of asynchronous aspects of the UPnP stack (discovery).
+ */
+ public Executor getAsyncProtocolExecutor();
+
+ /**
+ * @return The executor service which runs the processing of synchronous aspects of the UPnP stack (description, control, GENA).
+ */
+ public ExecutorService getSyncProtocolExecutorService();
+
+ /**
+ * @return An instance of {@link org.fourthline.cling.model.Namespace} for this UPnP stack.
+ */
+ public Namespace getNamespace();
+
+ /**
+ * @return The executor which runs the background thread for maintaining the registry.
+ */
+ public Executor getRegistryMaintainerExecutor();
+
+ /**
+ * @return The executor which runs the notification threads of registry listeners.
+ */
+ public Executor getRegistryListenerExecutor();
+
+ /**
+ * Called by the {@link org.fourthline.cling.UpnpService} on shutdown, useful to e.g. shutdown thread pools.
+ */
+ public void shutdown();
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c23e3b0cc6577aec0d30d4c263e8d3028240931
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/UpnpServiceImpl.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling;
+
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.controlpoint.ControlPointImpl;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.protocol.ProtocolFactoryImpl;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.registry.RegistryImpl;
+import org.fourthline.cling.registry.RegistryListener;
+import org.fourthline.cling.transport.Router;
+import org.fourthline.cling.transport.RouterException;
+import org.fourthline.cling.transport.RouterImpl;
+import org.seamless.util.Exceptions;
+
+import javax.enterprise.inject.Alternative;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Default implementation of {@link UpnpService}, starts immediately on construction.
+ *
+ * If no {@link UpnpServiceConfiguration} is provided it will automatically
+ * instantiate {@link DefaultUpnpServiceConfiguration}. This configuration does not
+ * work on Android! Use the {@link org.fourthline.cling.android.AndroidUpnpService}
+ * application component instead.
+ *
+ *
+ * Override the various create...() methods to customize instantiation of protocol factory,
+ * router, etc.
+ *
+ *
+ * @author Christian Bauer
+ */
+@Alternative
+public class UpnpServiceImpl implements UpnpService {
+
+ private static Logger log = Logger.getLogger(UpnpServiceImpl.class.getName());
+
+ protected final UpnpServiceConfiguration configuration;
+ protected final ControlPoint controlPoint;
+ protected final ProtocolFactory protocolFactory;
+ protected final Registry registry;
+ protected final Router router;
+
+ public UpnpServiceImpl() {
+ this(new DefaultUpnpServiceConfiguration());
+ }
+
+ public UpnpServiceImpl(RegistryListener... registryListeners) {
+ this(new DefaultUpnpServiceConfiguration(), registryListeners);
+ }
+
+ public UpnpServiceImpl(UpnpServiceConfiguration configuration, RegistryListener... registryListeners) {
+ this.configuration = configuration;
+
+ log.info(">>> Starting UPnP service...");
+
+ log.info("Using configuration: " + getConfiguration().getClass().getName());
+
+ // Instantiation order is important: Router needs to start its network services after registry is ready
+
+ this.protocolFactory = createProtocolFactory();
+
+ this.registry = createRegistry(protocolFactory);
+ for (RegistryListener registryListener : registryListeners) {
+ this.registry.addListener(registryListener);
+ }
+
+ this.router = createRouter(protocolFactory, registry);
+
+ try {
+ this.router.enable();
+ } catch (RouterException ex) {
+ throw new RuntimeException("Enabling network router failed: " + ex, ex);
+ }
+
+ this.controlPoint = createControlPoint(protocolFactory, registry);
+
+ log.info("<<< UPnP service started successfully");
+ }
+
+ protected ProtocolFactory createProtocolFactory() {
+ return new ProtocolFactoryImpl(this);
+ }
+
+ protected Registry createRegistry(ProtocolFactory protocolFactory) {
+ return new RegistryImpl(this);
+ }
+
+ protected Router createRouter(ProtocolFactory protocolFactory, Registry registry) {
+ return new RouterImpl(getConfiguration(), protocolFactory);
+ }
+
+ protected ControlPoint createControlPoint(ProtocolFactory protocolFactory, Registry registry) {
+ return new ControlPointImpl(getConfiguration(), protocolFactory, registry);
+ }
+
+ public UpnpServiceConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public ControlPoint getControlPoint() {
+ return controlPoint;
+ }
+
+ public ProtocolFactory getProtocolFactory() {
+ return protocolFactory;
+ }
+
+ public Registry getRegistry() {
+ return registry;
+ }
+
+ public Router getRouter() {
+ return router;
+ }
+
+ synchronized public void shutdown() {
+ shutdown(false);
+ }
+
+ protected void shutdown(boolean separateThread) {
+ Runnable shutdown = new Runnable() {
+ @Override
+ public void run() {
+ log.info(">>> Shutting down UPnP service...");
+ shutdownRegistry();
+ shutdownRouter();
+ shutdownConfiguration();
+ log.info("<<< UPnP service shutdown completed");
+ }
+ };
+ if (separateThread) {
+ // This is not a daemon thread, it has to complete!
+ new Thread(shutdown).start();
+ } else {
+ shutdown.run();
+ }
+ }
+
+ protected void shutdownRegistry() {
+ getRegistry().shutdown();
+ }
+
+ protected void shutdownRouter() {
+ try {
+ getRouter().shutdown();
+ } catch (RouterException ex) {
+ Throwable cause = Exceptions.unwrap(ex);
+ if (cause instanceof InterruptedException) {
+ log.log(Level.INFO, "Router shutdown was interrupted: " + ex, cause);
+ } else {
+ log.log(Level.SEVERE, "Router error on shutdown: " + ex, cause);
+ }
+ }
+ }
+
+ protected void shutdownConfiguration() {
+ getConfiguration().shutdown();
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidNetworkAddressFactory.java b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidNetworkAddressFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..74d8bd479819cdecc937e4a2a3a9e5f71cc963bc
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidNetworkAddressFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import org.fourthline.cling.transport.impl.NetworkAddressFactoryImpl;
+import org.fourthline.cling.transport.spi.InitializationException;
+
+import java.lang.reflect.Field;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This factory tries to work around and patch some Android bugs.
+ *
+ * @author Michael Pujos
+ * @author Christian Bauer
+ */
+public class AndroidNetworkAddressFactory extends NetworkAddressFactoryImpl {
+
+ final private static Logger log = Logger.getLogger(AndroidUpnpServiceConfiguration.class.getName());
+
+ public AndroidNetworkAddressFactory(int streamListenPort) {
+ super(streamListenPort);
+ }
+
+ @Override
+ protected boolean requiresNetworkInterface() {
+ return false;
+ }
+
+ @Override
+ protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) {
+ boolean result = super.isUsableAddress(networkInterface, address);
+ if (result) {
+ // TODO: Workaround Android DNS reverse lookup issue, still a problem on ICS+?
+ // http://4thline.org/projects/mailinglists.html#nabble-td3011461
+ String hostName = address.getHostAddress();
+
+ Field field0 = null;
+ Object target = null;
+
+ try {
+
+ try {
+ field0 = InetAddress.class.getDeclaredField("holder");
+ field0.setAccessible(true);
+ target = field0.get(address);
+ field0 = target.getClass().getDeclaredField("hostName");
+ } catch( NoSuchFieldException e ) {
+ // Let's try the non-OpenJDK variant
+ field0 = InetAddress.class.getDeclaredField("hostName");
+ target = address;
+ }
+
+ if (field0 != null && target != null && hostName != null) {
+ field0.setAccessible(true);
+ field0.set(target, hostName);
+ } else {
+ return false;
+ }
+
+ } catch (Exception ex) {
+ log.log(Level.SEVERE,
+ "Failed injecting hostName to work around Android InetAddress DNS bug: " + address,
+ ex
+ );
+ return false;
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public InetAddress getLocalAddress(NetworkInterface networkInterface, boolean isIPv6, InetAddress remoteAddress) {
+ // TODO: This is totally random because we can't access low level InterfaceAddress on Android!
+ for (InetAddress localAddress : getInetAddresses(networkInterface)) {
+ if (isIPv6 && localAddress instanceof Inet6Address)
+ return localAddress;
+ if (!isIPv6 && localAddress instanceof Inet4Address)
+ return localAddress;
+ }
+ throw new IllegalStateException("Can't find any IPv4 or IPv6 address on interface: " + networkInterface.getDisplayName());
+ }
+
+ @Override
+ protected void discoverNetworkInterfaces() throws InitializationException {
+ try {
+ super.discoverNetworkInterfaces();
+ } catch (Exception ex) {
+ // TODO: ICS bug on some models with network interface disappearing while enumerated
+ // http://code.google.com/p/android/issues/detail?id=33661
+ log.warning("Exception while enumerating network interfaces, trying once more: " + ex);
+ super.discoverNetworkInterfaces();
+ }
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidRouter.java b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidRouter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ebb1b179dc637246335656f766e862ca20a440d
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidRouter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.model.ModelUtil;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.transport.Router;
+import org.fourthline.cling.transport.RouterException;
+import org.fourthline.cling.transport.RouterImpl;
+import org.fourthline.cling.transport.spi.InitializationException;
+import org.seamless.util.Exceptions;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Monitors all network connectivity changes, switching the router accordingly.
+ *
+ * @author Michael Pujos
+ * @author Christian Bauer
+ */
+public class AndroidRouter extends RouterImpl {
+
+ final private static Logger log = Logger.getLogger(Router.class.getName());
+
+ final private Context context;
+
+ final private WifiManager wifiManager;
+ protected WifiManager.MulticastLock multicastLock;
+ protected WifiManager.WifiLock wifiLock;
+ protected NetworkInfo networkInfo;
+ protected BroadcastReceiver broadcastReceiver;
+
+ public AndroidRouter(UpnpServiceConfiguration configuration,
+ ProtocolFactory protocolFactory,
+ Context context) throws InitializationException {
+ super(configuration, protocolFactory);
+
+ this.context = context;
+ this.wifiManager = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE));
+ this.networkInfo = NetworkUtils.getConnectedNetworkInfo(context);
+
+ // Only register for network connectivity changes if we are not running on emulator
+ if (!ModelUtil.ANDROID_EMULATOR) {
+ this.broadcastReceiver = createConnectivityBroadcastReceiver();
+ context.registerReceiver(broadcastReceiver, new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"));
+ }
+ }
+
+ protected BroadcastReceiver createConnectivityBroadcastReceiver() {
+ return new ConnectivityBroadcastReceiver();
+ }
+
+ @Override
+ protected int getLockTimeoutMillis() {
+ return 15000;
+ }
+
+ @Override
+ public void shutdown() throws RouterException {
+ super.shutdown();
+ unregisterBroadcastReceiver();
+ }
+
+ @Override
+ public boolean enable() throws RouterException {
+ lock(writeLock);
+ try {
+ boolean enabled;
+ if ((enabled = super.enable())) {
+ // Enable multicast on the WiFi network interface,
+ // requires android.permission.CHANGE_WIFI_MULTICAST_STATE
+ if (isWifi()) {
+ setWiFiMulticastLock(true);
+ setWifiLock(true);
+ }
+ }
+ return enabled;
+ } finally {
+ unlock(writeLock);
+ }
+ }
+
+ @Override
+ public boolean disable() throws RouterException {
+ lock(writeLock);
+ try {
+ // Disable multicast on WiFi network interface,
+ // requires android.permission.CHANGE_WIFI_MULTICAST_STATE
+ if (isWifi()) {
+ setWiFiMulticastLock(false);
+ setWifiLock(false);
+ }
+ return super.disable();
+ } finally {
+ unlock(writeLock);
+ }
+ }
+
+ public NetworkInfo getNetworkInfo() {
+ return networkInfo;
+ }
+
+ public boolean isMobile() {
+ return NetworkUtils.isMobile(networkInfo);
+ }
+
+ public boolean isWifi() {
+ return NetworkUtils.isWifi(networkInfo);
+ }
+
+ public boolean isEthernet() {
+ return NetworkUtils.isEthernet(networkInfo);
+ }
+
+ public boolean enableWiFi() {
+ log.info("Enabling WiFi...");
+ try {
+ return wifiManager.setWifiEnabled(true);
+ } catch (Throwable t) {
+ // workaround (HTC One X, 4.0.3)
+ //java.lang.SecurityException: Permission Denial: writing com.android.providers.settings.SettingsProvider
+ // uri content://settings/system from pid=4691, uid=10226 requires android.permission.WRITE_SETTINGS
+ // at android.os.Parcel.readException(Parcel.java:1332)
+ // at android.os.Parcel.readException(Parcel.java:1286)
+ // at android.net.wifi.IWifiManager$Stub$Proxy.setWifiEnabled(IWifiManager.java:1115)
+ // at android.net.wifi.WifiManager.setWifiEnabled(WifiManager.java:946)
+ log.log(Level.WARNING, "SetWifiEnabled failed", t);
+ return false;
+ }
+ }
+
+ public void unregisterBroadcastReceiver() {
+ if (broadcastReceiver != null) {
+ context.unregisterReceiver(broadcastReceiver);
+ broadcastReceiver = null;
+ }
+ }
+
+ protected void setWiFiMulticastLock(boolean enable) {
+ if (multicastLock == null) {
+ multicastLock = wifiManager.createMulticastLock(getClass().getSimpleName());
+ }
+
+ if (enable) {
+ if (multicastLock.isHeld()) {
+ log.warning("WiFi multicast lock already acquired");
+ } else {
+ log.info("WiFi multicast lock acquired");
+ multicastLock.acquire();
+ }
+ } else {
+ if (multicastLock.isHeld()) {
+ log.info("WiFi multicast lock released");
+ multicastLock.release();
+ } else {
+ log.warning("WiFi multicast lock already released");
+ }
+ }
+ }
+
+ protected void setWifiLock(boolean enable) {
+ if (wifiLock == null) {
+ wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, getClass().getSimpleName());
+ }
+
+ if (enable) {
+ if (wifiLock.isHeld()) {
+ log.warning("WiFi lock already acquired");
+ } else {
+ log.info("WiFi lock acquired");
+ wifiLock.acquire();
+ }
+ } else {
+ if (wifiLock.isHeld()) {
+ log.info("WiFi lock released");
+ wifiLock.release();
+ } else {
+ log.warning("WiFi lock already released");
+ }
+ }
+ }
+
+ /**
+ * Can be overriden by subclasses to do additional work.
+ *
+ * @param oldNetwork null
when first called by constructor.
+ */
+ protected void onNetworkTypeChange(NetworkInfo oldNetwork, NetworkInfo newNetwork) throws RouterException {
+ log.info(String.format("Network type changed %s => %s",
+ oldNetwork == null ? "" : oldNetwork.getTypeName(),
+ newNetwork == null ? "NONE" : newNetwork.getTypeName()));
+
+ if (disable()) {
+ log.info(String.format(
+ "Disabled router on network type change (old network: %s)",
+ oldNetwork == null ? "NONE" : oldNetwork.getTypeName()
+ ));
+ }
+
+ networkInfo = newNetwork;
+ if (enable()) {
+ // Can return false (via earlier InitializationException thrown by NetworkAddressFactory) if
+ // no bindable network address found!
+ log.info(String.format(
+ "Enabled router on network type change (new network: %s)",
+ newNetwork == null ? "NONE" : newNetwork.getTypeName()
+ ));
+ }
+ }
+
+ /**
+ * Handles errors when network has been switched, during reception of
+ * network switch broadcast. Logs a warning by default, override to
+ * change this behavior.
+ */
+ protected void handleRouterExceptionOnNetworkTypeChange(RouterException ex) {
+ Throwable cause = Exceptions.unwrap(ex);
+ if (cause instanceof InterruptedException) {
+ log.log(Level.INFO, "Router was interrupted: " + ex, cause);
+ } else {
+ log.log(Level.WARNING, "Router error on network change: " + ex, ex);
+ }
+ }
+
+ class ConnectivityBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION))
+ return;
+
+ displayIntentInfo(intent);
+
+ NetworkInfo newNetworkInfo = NetworkUtils.getConnectedNetworkInfo(context);
+
+ // When Android switches WiFI => MOBILE, sometimes we may have a short transition
+ // with no network: WIFI => NONE, NONE => MOBILE
+ // The code below attempts to make it look like a single WIFI => MOBILE
+ // transition, retrying up to 3 times getting the current network.
+ //
+ // Note: this can block the UI thread for up to 3s
+ if (networkInfo != null && newNetworkInfo == null) {
+ for (int i = 1; i <= 3; i++) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ log.warning(String.format(
+ "%s => NONE network transition, waiting for new network... retry #%d",
+ networkInfo.getTypeName(), i
+ ));
+ newNetworkInfo = NetworkUtils.getConnectedNetworkInfo(context);
+ if (newNetworkInfo != null)
+ break;
+ }
+ }
+
+ if (isSameNetworkType(networkInfo, newNetworkInfo)) {
+ log.info("No actual network change... ignoring event!");
+ } else {
+ try {
+ onNetworkTypeChange(networkInfo, newNetworkInfo);
+ } catch (RouterException ex) {
+ handleRouterExceptionOnNetworkTypeChange(ex);
+ }
+ }
+ }
+
+ protected boolean isSameNetworkType(NetworkInfo network1, NetworkInfo network2) {
+ if (network1 == null && network2 == null)
+ return true;
+ if (network1 == null || network2 == null)
+ return false;
+ return network1.getType() == network2.getType();
+ }
+
+ protected void displayIntentInfo(Intent intent) {
+ boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+ String reason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON);
+ boolean isFailover = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);
+
+ NetworkInfo currentNetworkInfo = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ NetworkInfo otherNetworkInfo = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
+
+ log.info("Connectivity change detected...");
+ log.info("EXTRA_NO_CONNECTIVITY: " + noConnectivity);
+ log.info("EXTRA_REASON: " + reason);
+ log.info("EXTRA_IS_FAILOVER: " + isFailover);
+ log.info("EXTRA_NETWORK_INFO: " + (currentNetworkInfo == null ? "none" : currentNetworkInfo));
+ log.info("EXTRA_OTHER_NETWORK_INFO: " + (otherNetworkInfo == null ? "none" : otherNetworkInfo));
+ log.info("EXTRA_EXTRA_INFO: " + intent.getStringExtra(ConnectivityManager.EXTRA_EXTRA_INFO));
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpService.java b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpService.java
new file mode 100644
index 0000000000000000000000000000000000000000..b15d1ea1eb8ebec80a0400715a9df0aaddf823e2
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpService.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import org.fourthline.cling.UpnpService;
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.registry.Registry;
+
+/**
+ * Interface of the Android UPnP application service component.
+ *
+ * Usage example in an Android activity:
+ *
+ * {@code
+ *AndroidUpnpService upnpService;
+ *
+ *ServiceConnection serviceConnection = new ServiceConnection() {
+ * public void onServiceConnected(ComponentName className, IBinder service) {
+ * upnpService = (AndroidUpnpService) service;
+ * }
+ * public void onServiceDisconnected(ComponentName className) {
+ * upnpService = null;
+ * }
+ *};
+ *
+ *public void onCreate(...) {
+ * ...
+ * getApplicationContext().bindService(
+ * new Intent(this, AndroidUpnpServiceImpl.class),
+ * serviceConnection,
+ * Context.BIND_AUTO_CREATE
+ * );
+ *}}
+ *
+ * The default implementation requires permissions in AndroidManifest.xml
:
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ *
+ *}
+ *
+ * You also have to add the application service component:
+ *
+ * {@code
+ *
+ * ...
+ *
+ *
+ * }
+ *
+ * @author Christian Bauer
+ */
+// DOC:CLASS
+public interface AndroidUpnpService {
+
+ /**
+ * @return The actual main instance and interface of the UPnP service.
+ */
+ public UpnpService get();
+
+ /**
+ * @return The configuration of the UPnP service.
+ */
+ public UpnpServiceConfiguration getConfiguration();
+
+ /**
+ * @return The registry of the UPnP service.
+ */
+ public Registry getRegistry();
+
+ /**
+ * @return The client API of the UPnP service.
+ */
+ public ControlPoint getControlPoint();
+
+}
+// DOC:CLASS
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceConfiguration.java b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..82a8b0a8b8cd293d9c45f8ffb45988ab9d3dafa6
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceConfiguration.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import android.os.Build;
+import org.fourthline.cling.DefaultUpnpServiceConfiguration;
+import org.fourthline.cling.binding.xml.DeviceDescriptorBinder;
+import org.fourthline.cling.binding.xml.RecoveringUDA10DeviceDescriptorBinderImpl;
+import org.fourthline.cling.binding.xml.ServiceDescriptorBinder;
+import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderSAXImpl;
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.ServerClientTokens;
+import org.fourthline.cling.transport.impl.AsyncServletStreamServerConfigurationImpl;
+import org.fourthline.cling.transport.impl.AsyncServletStreamServerImpl;
+import org.fourthline.cling.transport.impl.RecoveringGENAEventProcessorImpl;
+import org.fourthline.cling.transport.impl.RecoveringSOAPActionProcessorImpl;
+import org.fourthline.cling.transport.impl.jetty.JettyServletContainer;
+import org.fourthline.cling.transport.impl.jetty.StreamClientConfigurationImpl;
+import org.fourthline.cling.transport.impl.jetty.StreamClientImpl;
+import org.fourthline.cling.transport.spi.GENAEventProcessor;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+import org.fourthline.cling.transport.spi.SOAPActionProcessor;
+import org.fourthline.cling.transport.spi.StreamClient;
+import org.fourthline.cling.transport.spi.StreamServer;
+
+/**
+ * Configuration settings for deployment on Android.
+ *
+ * This configuration utilizes the Jetty transport implementation
+ * found in {@link org.fourthline.cling.transport.impl.jetty} for TCP/HTTP networking, as
+ * client and server. The servlet context path for UPnP is set to /upnp
.
+ *
+ *
+ * The kxml2 implementation of org.xmlpull
is available on Android, therefore
+ * this configuration uses {@link RecoveringUDA10DeviceDescriptorBinderImpl},
+ * {@link RecoveringSOAPActionProcessorImpl}, and {@link RecoveringGENAEventProcessorImpl}.
+ *
+ *
+ * This configuration utilizes {@link UDA10ServiceDescriptorBinderSAXImpl}, the system property
+ * org.xml.sax.driver
is set to org.xmlpull.v1.sax2.Driver
.
+ *
+ *
+ * To preserve battery, the {@link org.fourthline.cling.registry.Registry} will only
+ * be maintained every 3 seconds.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class AndroidUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration {
+
+ public AndroidUpnpServiceConfiguration() {
+ this(0); // Ephemeral port
+ }
+
+ public AndroidUpnpServiceConfiguration(int streamListenPort) {
+ super(streamListenPort, false);
+
+ // This should be the default on Android 2.1 but it's not set by default
+ System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
+ }
+
+ @Override
+ protected NetworkAddressFactory createNetworkAddressFactory(int streamListenPort) {
+ return new AndroidNetworkAddressFactory(streamListenPort);
+ }
+
+ @Override
+ protected Namespace createNamespace() {
+ // For the Jetty server, this is the servlet context path
+ return new Namespace("/upnp");
+ }
+
+ @Override
+ public StreamClient createStreamClient() {
+ // Use Jetty
+ return new StreamClientImpl(
+ new StreamClientConfigurationImpl(
+ getSyncProtocolExecutorService()
+ ) {
+ @Override
+ public String getUserAgentValue(int majorVersion, int minorVersion) {
+ // TODO: UPNP VIOLATION: Synology NAS requires User-Agent to contain
+ // "Android" to return DLNA protocolInfo required to stream to Samsung TV
+ // see: http://two-play.com/forums/viewtopic.php?f=6&t=81
+ ServerClientTokens tokens = new ServerClientTokens(majorVersion, minorVersion);
+ tokens.setOsName("Android");
+ tokens.setOsVersion(Build.VERSION.RELEASE);
+ return tokens.toString();
+ }
+ }
+ );
+ }
+
+ @Override
+ public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
+ // Use Jetty, start/stop a new shared instance of JettyServletContainer
+ return new AsyncServletStreamServerImpl(
+ new AsyncServletStreamServerConfigurationImpl(
+ JettyServletContainer.INSTANCE,
+ networkAddressFactory.getStreamListenPort()
+ )
+ );
+ }
+
+ @Override
+ protected DeviceDescriptorBinder createDeviceDescriptorBinderUDA10() {
+ return new RecoveringUDA10DeviceDescriptorBinderImpl();
+ }
+
+ @Override
+ protected ServiceDescriptorBinder createServiceDescriptorBinderUDA10() {
+ return new UDA10ServiceDescriptorBinderSAXImpl();
+ }
+
+ @Override
+ protected SOAPActionProcessor createSOAPActionProcessor() {
+ return new RecoveringSOAPActionProcessorImpl();
+ }
+
+ @Override
+ protected GENAEventProcessor createGENAEventProcessor() {
+ return new RecoveringGENAEventProcessorImpl();
+ }
+
+ @Override
+ public int getRegistryMaintenanceIntervalMillis() {
+ return 3000; // Preserve battery on Android, only run every 3 seconds
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..61a18bb7b6158e47cf7d452766f2b96306082870
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/AndroidUpnpServiceImpl.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import org.fourthline.cling.UpnpService;
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.UpnpServiceImpl;
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.transport.Router;
+
+/**
+ * Provides a UPnP stack with Android configuration as an application service component.
+ *
+ * Sends a search for all UPnP devices on instantiation. See the
+ * {@link org.fourthline.cling.android.AndroidUpnpService} interface for a usage example.
+ *
+ *
+ * Override the {@link #createRouter(org.fourthline.cling.UpnpServiceConfiguration, org.fourthline.cling.protocol.ProtocolFactory, android.content.Context)}
+ * and {@link #createConfiguration()} methods to customize the service.
+ *
+ * @author Christian Bauer
+ */
+public class AndroidUpnpServiceImpl extends Service {
+
+ protected UpnpService upnpService;
+ protected Binder binder = new Binder();
+
+ /**
+ * Starts the UPnP service.
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ upnpService = new UpnpServiceImpl(createConfiguration()) {
+
+ @Override
+ protected Router createRouter(ProtocolFactory protocolFactory, Registry registry) {
+ return AndroidUpnpServiceImpl.this.createRouter(
+ getConfiguration(),
+ protocolFactory,
+ AndroidUpnpServiceImpl.this
+ );
+ }
+
+ @Override
+ public synchronized void shutdown() {
+ // First have to remove the receiver, so Android won't complain about it leaking
+ // when the main UI thread exits.
+ ((AndroidRouter)getRouter()).unregisterBroadcastReceiver();
+
+ // Now we can concurrently run the Cling shutdown code, without occupying the
+ // Android main UI thread. This will complete probably after the main UI thread
+ // is done.
+ super.shutdown(true);
+ }
+ };
+ }
+
+ protected UpnpServiceConfiguration createConfiguration() {
+ return new AndroidUpnpServiceConfiguration();
+ }
+
+ protected AndroidRouter createRouter(UpnpServiceConfiguration configuration,
+ ProtocolFactory protocolFactory,
+ Context context) {
+ return new AndroidRouter(configuration, protocolFactory, context);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ /**
+ * Stops the UPnP service, when the last Activity unbinds from this Service.
+ */
+ @Override
+ public void onDestroy() {
+ upnpService.shutdown();
+ super.onDestroy();
+ }
+
+ protected class Binder extends android.os.Binder implements AndroidUpnpService {
+
+ public UpnpService get() {
+ return upnpService;
+ }
+
+ public UpnpServiceConfiguration getConfiguration() {
+ return upnpService.getConfiguration();
+ }
+
+ public Registry getRegistry() {
+ return upnpService.getRegistry();
+ }
+
+ public ControlPoint getControlPoint() {
+ return upnpService.getControlPoint();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/FixedAndroidLogHandler.java b/clinglibrary/src/main/java/org/fourthline/cling/android/FixedAndroidLogHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..6d20e0ebf386f88b86bf268aff9d81bbbd1cfbb2
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/FixedAndroidLogHandler.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.fourthline.cling.android;
+
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/*
+Taken from: http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob_plain;f=core/java/com/android/internal/logging/AndroidHandler.java;hb=c2ad241504fcaa12d4579d3b0b4038d1ca8d08c9
+ */
+public class FixedAndroidLogHandler extends Handler {
+ /**
+ * Holds the formatter for all Android log handlers.
+ */
+ private static final Formatter THE_FORMATTER = new Formatter() {
+ @Override
+ public String format(LogRecord r) {
+ Throwable thrown = r.getThrown();
+ if (thrown != null) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ sw.write(r.getMessage());
+ sw.write("\n");
+ thrown.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ } else {
+ return r.getMessage();
+ }
+ }
+ };
+
+ /**
+ * Constructs a new instance of the Android log handler.
+ */
+ public FixedAndroidLogHandler() {
+ setFormatter(THE_FORMATTER);
+ }
+
+ @Override
+ public void close() {
+ // No need to close, but must implement abstract method.
+ }
+
+ @Override
+ public void flush() {
+ // No need to flush, but must implement abstract method.
+ }
+
+ @Override
+ public void publish(LogRecord record) {
+ try {
+ int level = getAndroidLevel(record.getLevel());
+ String tag = record.getLoggerName();
+
+ if (tag == null) {
+ // Anonymous logger.
+ tag = "null";
+ } else {
+ // Tags must be <= 23 characters.
+ int length = tag.length();
+ if (length > 23) {
+ // Most loggers use the full class name. Try dropping the
+ // package.
+ int lastPeriod = tag.lastIndexOf(".");
+ if (length - lastPeriod - 1 <= 23) {
+ tag = tag.substring(lastPeriod + 1);
+ } else {
+ // Use last 23 chars.
+ tag = tag.substring(tag.length() - 23);
+ }
+ }
+ }
+
+ /* ############################################################################################
+
+ Instead of using the perfectly fine java.util.logging API for setting the
+ loggable levels, this call relies on a totally obscure "local.prop" file which you have to place on
+ your device. By default, if you do not have that file and if you do not execute some magic
+ "setprop" commands on your device, only INFO/WARN/ERROR is loggable. So whatever you do with
+ java.util.logging.Logger.setLevel(...) doesn't have any effect. The debug messages might arrive
+ here but they are dropped because you _also_ have to set the Android internal logging level with
+ the aforementioned magic switches.
+
+ Also, consider that you have to understand how a JUL logger name is mapped to the "tag" of
+ the Android log. Basically, the whole cutting and cropping procedure above is what you have to
+ memorize if you want to log with JUL and configure Android for debug output.
+
+ I actually admire the pure evil of this setup, even Mr. Ceki can learn something!
+
+ Commenting out these lines makes it all work as expected:
+
+ if (!Log.isLoggable(tag, level)) {
+ return;
+ }
+
+ ############################################################################################### */
+
+ String message = getFormatter().format(record);
+ Log.println(level, tag, message);
+ } catch (RuntimeException e) {
+ Log.e("AndroidHandler", "Error logging message.", e);
+ }
+ }
+
+ /**
+ * Converts a {@link java.util.logging.Logger} logging level into an Android one.
+ *
+ * @param level The {@link java.util.logging.Logger} logging level.
+ *
+ * @return The resulting Android logging level.
+ */
+ static int getAndroidLevel(Level level) {
+ int value = level.intValue();
+ if (value >= 1000) { // SEVERE
+ return Log.ERROR;
+ } else if (value >= 900) { // WARNING
+ return Log.WARN;
+ } else if (value >= 800) { // INFO
+ return Log.INFO;
+ } else {
+ return Log.DEBUG;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/android/NetworkUtils.java b/clinglibrary/src/main/java/org/fourthline/cling/android/NetworkUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ea212ec574fe9dc9a17bc7f2c081dc95a34aa92
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/android/NetworkUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.android;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import org.fourthline.cling.model.ModelUtil;
+
+import java.util.logging.Logger;
+
+/**
+ * Android network helpers.
+ *
+ * @author Michael Pujos
+ */
+public class NetworkUtils {
+
+ final private static Logger log = Logger.getLogger(NetworkUtils.class.getName());
+
+ static public NetworkInfo getConnectedNetworkInfo(Context context) {
+
+ ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected()) {
+ return networkInfo;
+ }
+
+ networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected()) return networkInfo;
+
+ networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected()) return networkInfo;
+
+ networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIMAX);
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected()) return networkInfo;
+
+ networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected()) return networkInfo;
+
+ log.info("Could not find any connected network...");
+
+ return null;
+ }
+
+ static public boolean isEthernet(NetworkInfo networkInfo) {
+ return isNetworkType(networkInfo, ConnectivityManager.TYPE_ETHERNET);
+ }
+
+ static public boolean isWifi(NetworkInfo networkInfo) {
+ return isNetworkType(networkInfo, ConnectivityManager.TYPE_WIFI) || ModelUtil.ANDROID_EMULATOR;
+ }
+
+ static public boolean isMobile(NetworkInfo networkInfo) {
+ return isNetworkType(networkInfo, ConnectivityManager.TYPE_MOBILE) || isNetworkType(networkInfo, ConnectivityManager.TYPE_WIMAX);
+ }
+
+ static public boolean isNetworkType(NetworkInfo networkInfo, int type) {
+ return networkInfo != null && networkInfo.getType() == type;
+ }
+
+ static public boolean isSSDPAwareNetwork(NetworkInfo networkInfo) {
+ return isWifi(networkInfo) || isEthernet(networkInfo);
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueProvider.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..8211b4084e66f229d7559836e8a02a15f6635065
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding;
+
+/**
+ * @author Christian Bauer
+ */
+public interface AllowedValueProvider {
+
+ public String[] getValues();
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueRangeProvider.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueRangeProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f8a6980f42a3e9530e4031d5fc4163862dcd17c
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/AllowedValueRangeProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding;
+
+/**
+ * @author Christian Bauer
+ */
+public interface AllowedValueRangeProvider {
+
+ public long getMinimum();
+ public long getMaximum();
+ public long getStep();
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0b4feedd2fe850ce6bad9e2b250bd57efdad9a1
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBinder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding;
+
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.ServiceType;
+
+/**
+ * Reads {@link org.fourthline.cling.model.meta.LocalService} metadata given a Java class.
+ *
+ * @author Christian Bauer
+ */
+public interface LocalServiceBinder {
+
+ /**
+ * @param clazz The Java class that is the source of the service metadata.
+ * @return The produced metadata.
+ * @throws LocalServiceBindingException If binding failed.
+ */
+ public LocalService read(Class> clazz) throws LocalServiceBindingException;
+
+ /**
+ *
+ * @param clazz The Java class that is the source of the service metadata.
+ * @param id The pre-defined identifier of the service.
+ * @param type The pre-defined type of the service.
+ * @param supportsQueryStateVariables true
if the service should support the
+ * deprecated "query any state variable value" action.
+ * @param stringConvertibleTypes A list of Java classes which map directly to string-typed
+ * UPnP state variables.
+ * @return The produced metadata.
+ * @throws LocalServiceBindingException If binding failed.
+ */
+ public LocalService read(Class> clazz, ServiceId id, ServiceType type,
+ boolean supportsQueryStateVariables, Class[] stringConvertibleTypes) throws LocalServiceBindingException;
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBindingException.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBindingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a211feef9cef9c3923cddadc5856665e30083cdb
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/LocalServiceBindingException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding;
+
+/**
+ * Thrown when reading/writing {@link org.fourthline.cling.model.meta.LocalService} metadata failed.
+ *
+ * @author Christian Bauer
+ */
+public class LocalServiceBindingException extends RuntimeException {
+
+ public LocalServiceBindingException(String s) {
+ super(s);
+ }
+
+ public LocalServiceBindingException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..fcb7028f44e7292b281398d5cfaff4c24194947e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import org.fourthline.cling.binding.LocalServiceBindingException;
+import org.fourthline.cling.model.Constants;
+import org.fourthline.cling.model.ModelUtil;
+import org.fourthline.cling.model.action.ActionExecutor;
+import org.fourthline.cling.model.action.MethodActionExecutor;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.profile.RemoteClientInfo;
+import org.fourthline.cling.model.state.GetterStateVariableAccessor;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.Datatype;
+import org.seamless.util.Reflections;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * @author Christian Bauer
+ */
+public class AnnotationActionBinder {
+
+ private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
+
+ protected UpnpAction annotation;
+ protected Method method;
+ protected Map stateVariables;
+ protected Set stringConvertibleTypes;
+
+ public AnnotationActionBinder(Method method, Map stateVariables, Set stringConvertibleTypes) {
+ this.annotation = method.getAnnotation(UpnpAction.class);
+ this.stateVariables = stateVariables;
+ this.method = method;
+ this.stringConvertibleTypes = stringConvertibleTypes;
+ }
+
+ public UpnpAction getAnnotation() {
+ return annotation;
+ }
+
+ public Map getStateVariables() {
+ return stateVariables;
+ }
+
+ public Method getMethod() {
+ return method;
+ }
+
+ public Set getStringConvertibleTypes() {
+ return stringConvertibleTypes;
+ }
+
+ public Action appendAction(Map actions) throws LocalServiceBindingException {
+
+ String name;
+ if (getAnnotation().name().length() != 0) {
+ name = getAnnotation().name();
+ } else {
+ name = AnnotationLocalServiceBinder.toUpnpActionName(getMethod().getName());
+ }
+
+ log.fine("Creating action and executor: " + name);
+
+ List inputArguments = createInputArguments();
+ Map, StateVariableAccessor> outputArguments = createOutputArguments();
+
+ inputArguments.addAll(outputArguments.keySet());
+ ActionArgument[] actionArguments =
+ inputArguments.toArray(new ActionArgument[inputArguments.size()]);
+
+ Action action = new Action(name, actionArguments);
+ ActionExecutor executor = createExecutor(outputArguments);
+
+ actions.put(action, executor);
+ return action;
+ }
+
+ protected ActionExecutor createExecutor(Map, StateVariableAccessor> outputArguments) {
+ // TODO: Invent an annotation for this configuration
+ return new MethodActionExecutor(outputArguments, getMethod());
+ }
+
+ protected List createInputArguments() throws LocalServiceBindingException {
+
+ List list = new ArrayList<>();
+
+ // Input arguments are always method parameters
+ int annotatedParams = 0;
+ Annotation[][] params = getMethod().getParameterAnnotations();
+ for (int i = 0; i < params.length; i++) {
+ Annotation[] param = params[i];
+ for (Annotation paramAnnotation : param) {
+ if (paramAnnotation instanceof UpnpInputArgument) {
+ UpnpInputArgument inputArgumentAnnotation = (UpnpInputArgument) paramAnnotation;
+ annotatedParams++;
+
+ String argumentName =
+ inputArgumentAnnotation.name();
+
+ StateVariable stateVariable =
+ findRelatedStateVariable(
+ inputArgumentAnnotation.stateVariable(),
+ argumentName,
+ getMethod().getName()
+ );
+
+ if (stateVariable == null) {
+ throw new LocalServiceBindingException(
+ "Could not detected related state variable of argument: " + argumentName
+ );
+ }
+
+ validateType(stateVariable, getMethod().getParameterTypes()[i]);
+
+ ActionArgument inputArgument = new ActionArgument(
+ argumentName,
+ inputArgumentAnnotation.aliases(),
+ stateVariable.getName(),
+ ActionArgument.Direction.IN
+ );
+
+ list.add(inputArgument);
+ }
+ }
+ }
+ // A method can't have any parameters that are not annotated with @UpnpInputArgument - we wouldn't know what
+ // value to pass when we invoke it later on... unless the last parameter is of type RemoteClientInfo
+ if (annotatedParams < getMethod().getParameterTypes().length
+ && !RemoteClientInfo.class.isAssignableFrom(method.getParameterTypes()[method.getParameterTypes().length-1])) {
+ throw new LocalServiceBindingException("Method has parameters that are not input arguments: " + getMethod().getName());
+ }
+
+ return list;
+ }
+
+ protected Map, StateVariableAccessor> createOutputArguments() throws LocalServiceBindingException {
+
+ Map, StateVariableAccessor> map = new LinkedHashMap<>(); // !!! Insertion order!
+
+ UpnpAction actionAnnotation = getMethod().getAnnotation(UpnpAction.class);
+ if (actionAnnotation.out().length == 0) return map;
+
+ boolean hasMultipleOutputArguments = actionAnnotation.out().length > 1;
+
+ for (UpnpOutputArgument outputArgumentAnnotation : actionAnnotation.out()) {
+
+ String argumentName = outputArgumentAnnotation.name();
+
+ StateVariable stateVariable = findRelatedStateVariable(
+ outputArgumentAnnotation.stateVariable(),
+ argumentName,
+ getMethod().getName()
+ );
+
+ // Might-just-work attempt, try the name of the getter
+ if (stateVariable == null && outputArgumentAnnotation.getterName().length() > 0) {
+ stateVariable = findRelatedStateVariable(null, null, outputArgumentAnnotation.getterName());
+ }
+
+ if (stateVariable == null) {
+ throw new LocalServiceBindingException(
+ "Related state variable not found for output argument: " + argumentName
+ );
+ }
+
+ StateVariableAccessor accessor = findOutputArgumentAccessor(
+ stateVariable,
+ outputArgumentAnnotation.getterName(),
+ hasMultipleOutputArguments
+ );
+
+ log.finer("Found related state variable for output argument '" + argumentName + "': " + stateVariable);
+
+ ActionArgument outputArgument = new ActionArgument(
+ argumentName,
+ stateVariable.getName(),
+ ActionArgument.Direction.OUT,
+ !hasMultipleOutputArguments
+ );
+
+ map.put(outputArgument, accessor);
+ }
+
+ return map;
+ }
+
+ protected StateVariableAccessor findOutputArgumentAccessor(StateVariable stateVariable, String getterName, boolean multipleArguments)
+ throws LocalServiceBindingException {
+
+ boolean isVoid = getMethod().getReturnType().equals(Void.TYPE);
+
+ if (isVoid) {
+
+ if (getterName != null && getterName.length() > 0) {
+ log.finer("Action method is void, will use getter method named: " + getterName);
+
+ // Use the same class as the action method
+ Method getter = Reflections.getMethod(getMethod().getDeclaringClass(), getterName);
+ if (getter == null)
+ throw new LocalServiceBindingException(
+ "Declared getter method '" + getterName + "' not found on: " + getMethod().getDeclaringClass()
+ );
+
+ validateType(stateVariable, getter.getReturnType());
+
+ return new GetterStateVariableAccessor(getter);
+
+ } else {
+ log.finer("Action method is void, trying to find existing accessor of related: " + stateVariable);
+ return getStateVariables().get(stateVariable);
+ }
+
+
+ } else if (getterName != null && getterName.length() > 0) {
+ log.finer("Action method is not void, will use getter method on returned instance: " + getterName);
+
+ // Use the returned class
+ Method getter = Reflections.getMethod(getMethod().getReturnType(), getterName);
+ if (getter == null)
+ throw new LocalServiceBindingException(
+ "Declared getter method '" + getterName + "' not found on return type: " + getMethod().getReturnType()
+ );
+
+ validateType(stateVariable, getter.getReturnType());
+
+ return new GetterStateVariableAccessor(getter);
+
+ } else if (!multipleArguments) {
+ log.finer("Action method is not void, will use the returned instance: " + getMethod().getReturnType());
+ validateType(stateVariable, getMethod().getReturnType());
+ }
+
+ return null;
+ }
+
+ protected StateVariable findRelatedStateVariable(String declaredName, String argumentName, String methodName)
+ throws LocalServiceBindingException {
+
+ StateVariable relatedStateVariable = null;
+
+ if (declaredName != null && declaredName.length() > 0) {
+ relatedStateVariable = getStateVariable(declaredName);
+ }
+
+ if (relatedStateVariable == null && argumentName != null && argumentName.length() > 0) {
+ String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName);
+ log.finer("Finding related state variable with argument name (converted to UPnP name): " + actualName);
+ relatedStateVariable = getStateVariable(argumentName);
+ }
+
+ if (relatedStateVariable == null && argumentName != null && argumentName.length() > 0) {
+ // Try with A_ARG_TYPE prefix
+ String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName);
+ actualName = Constants.ARG_TYPE_PREFIX + actualName;
+ log.finer("Finding related state variable with prefixed argument name (converted to UPnP name): " + actualName);
+ relatedStateVariable = getStateVariable(actualName);
+ }
+
+ if (relatedStateVariable == null && methodName != null && methodName.length() > 0) {
+ // TODO: Well, this is often a nice shortcut but sometimes might have false positives
+ String methodPropertyName = Reflections.getMethodPropertyName(methodName);
+ if (methodPropertyName != null) {
+ log.finer("Finding related state variable with method property name: " + methodPropertyName);
+ relatedStateVariable =
+ getStateVariable(
+ AnnotationLocalServiceBinder.toUpnpStateVariableName(methodPropertyName)
+ );
+ }
+ }
+
+ return relatedStateVariable;
+ }
+
+ protected void validateType(StateVariable stateVariable, Class type) throws LocalServiceBindingException {
+
+ // Validate datatype as good as we can
+ // (for enums and other convertible types, the state variable type should be STRING)
+
+ Datatype.Default expectedDefaultMapping =
+ ModelUtil.isStringConvertibleType(getStringConvertibleTypes(), type)
+ ? Datatype.Default.STRING
+ : Datatype.Default.getByJavaType(type);
+
+ log.finer("Expecting '" + stateVariable + "' to match default mapping: " + expectedDefaultMapping);
+
+ if (expectedDefaultMapping != null &&
+ !stateVariable.getTypeDetails().getDatatype().isHandlingJavaType(expectedDefaultMapping.getJavaType())) {
+
+ // TODO: Consider custom types?!
+ throw new LocalServiceBindingException(
+ "State variable '" + stateVariable + "' datatype can't handle action " +
+ "argument's Java type (change one): " + expectedDefaultMapping.getJavaType()
+ );
+
+ } else if (expectedDefaultMapping == null && stateVariable.getTypeDetails().getDatatype().getBuiltin() != null) {
+ throw new LocalServiceBindingException(
+ "State variable '" + stateVariable + "' should be custom datatype " +
+ "(action argument type is unknown Java type): " + type.getSimpleName()
+ );
+ }
+
+ log.finer("State variable matches required argument datatype (or can't be validated because it is custom)");
+ }
+
+ protected StateVariable getStateVariable(String name) {
+ for (StateVariable stateVariable : getStateVariables().keySet()) {
+ if (stateVariable.getName().equals(name)) {
+ return stateVariable;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d857a8a098d05883c3bc5e5646217d47b632be46
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import org.fourthline.cling.binding.LocalServiceBinder;
+import org.fourthline.cling.binding.LocalServiceBindingException;
+import org.fourthline.cling.model.ValidationError;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.action.ActionExecutor;
+import org.fourthline.cling.model.action.QueryStateVariableExecutor;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.QueryStateVariableAction;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.FieldStateVariableAccessor;
+import org.fourthline.cling.model.state.GetterStateVariableAccessor;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.model.types.UDAServiceId;
+import org.fourthline.cling.model.types.UDAServiceType;
+import org.fourthline.cling.model.types.csv.CSV;
+import org.seamless.util.Reflections;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Locale;
+import java.util.logging.Logger;
+
+/**
+ * Reads {@link org.fourthline.cling.model.meta.LocalService} metadata from annotations.
+ *
+ * @author Christian Bauer
+ */
+public class AnnotationLocalServiceBinder implements LocalServiceBinder {
+
+ private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
+
+ public LocalService read(Class> clazz) throws LocalServiceBindingException {
+ log.fine("Reading and binding annotations of service implementation class: " + clazz);
+
+ // Read the service ID and service type from the annotation
+ if (clazz.isAnnotationPresent(UpnpService.class)) {
+
+ UpnpService annotation = clazz.getAnnotation(UpnpService.class);
+ UpnpServiceId idAnnotation = annotation.serviceId();
+ UpnpServiceType typeAnnotation = annotation.serviceType();
+
+ ServiceId serviceId = idAnnotation.namespace().equals(UDAServiceId.DEFAULT_NAMESPACE)
+ ? new UDAServiceId(idAnnotation.value())
+ : new ServiceId(idAnnotation.namespace(), idAnnotation.value());
+
+ ServiceType serviceType = typeAnnotation.namespace().equals(UDAServiceType.DEFAULT_NAMESPACE)
+ ? new UDAServiceType(typeAnnotation.value(), typeAnnotation.version())
+ : new ServiceType(typeAnnotation.namespace(), typeAnnotation.value(), typeAnnotation.version());
+
+ boolean supportsQueryStateVariables = annotation.supportsQueryStateVariables();
+
+ Set stringConvertibleTypes = readStringConvertibleTypes(annotation.stringConvertibleTypes());
+
+ return read(clazz, serviceId, serviceType, supportsQueryStateVariables, stringConvertibleTypes);
+ } else {
+ throw new LocalServiceBindingException("Given class is not an @UpnpService");
+ }
+ }
+
+ public LocalService read(Class> clazz, ServiceId id, ServiceType type,
+ boolean supportsQueryStateVariables, Class[] stringConvertibleTypes) throws LocalServiceBindingException {
+ return read(clazz, id, type, supportsQueryStateVariables, new HashSet<>(Arrays.asList(stringConvertibleTypes)));
+ }
+
+ public LocalService read(Class> clazz, ServiceId id, ServiceType type,
+ boolean supportsQueryStateVariables, Set stringConvertibleTypes)
+ throws LocalServiceBindingException {
+
+ Map stateVariables = readStateVariables(clazz, stringConvertibleTypes);
+ Map actions = readActions(clazz, stateVariables, stringConvertibleTypes);
+
+ // Special treatment of the state variable querying action
+ if (supportsQueryStateVariables) {
+ actions.put(new QueryStateVariableAction(), new QueryStateVariableExecutor());
+ }
+
+ try {
+ return new LocalService(type, id, actions, stateVariables, stringConvertibleTypes, supportsQueryStateVariables);
+
+ } catch (ValidationException ex) {
+ log.severe("Could not validate device model: " + ex.toString());
+ for (ValidationError validationError : ex.getErrors()) {
+ log.severe(validationError.toString());
+ }
+ throw new LocalServiceBindingException("Validation of model failed, check the log");
+ }
+ }
+
+ protected Set readStringConvertibleTypes(Class[] declaredTypes) throws LocalServiceBindingException {
+
+ for (Class stringConvertibleType : declaredTypes) {
+ if (!Modifier.isPublic(stringConvertibleType.getModifiers())) {
+ throw new LocalServiceBindingException(
+ "Declared string-convertible type must be public: " + stringConvertibleType
+ );
+ }
+ try {
+ stringConvertibleType.getConstructor(String.class);
+ } catch (NoSuchMethodException ex) {
+ throw new LocalServiceBindingException(
+ "Declared string-convertible type needs a public single-argument String constructor: " + stringConvertibleType
+ );
+ }
+ }
+ Set stringConvertibleTypes = new HashSet(Arrays.asList(declaredTypes));
+
+ // Some defaults
+ stringConvertibleTypes.add(URI.class);
+ stringConvertibleTypes.add(URL.class);
+ stringConvertibleTypes.add(CSV.class);
+
+ return stringConvertibleTypes;
+ }
+
+ protected Map readStateVariables(Class> clazz, Set stringConvertibleTypes)
+ throws LocalServiceBindingException {
+
+ Map map = new HashMap<>();
+
+ // State variables declared on the class
+ if (clazz.isAnnotationPresent(UpnpStateVariables.class)) {
+ UpnpStateVariables variables = clazz.getAnnotation(UpnpStateVariables.class);
+ for (UpnpStateVariable v : variables.value()) {
+
+ if (v.name().length() == 0)
+ throw new LocalServiceBindingException("Class-level @UpnpStateVariable name attribute value required");
+
+ String javaPropertyName = toJavaStateVariableName(v.name());
+
+ Method getter = Reflections.getGetterMethod(clazz, javaPropertyName);
+ Field field = Reflections.getField(clazz, javaPropertyName);
+
+ StateVariableAccessor accessor = null;
+ if (getter != null && field != null) {
+ accessor = variables.preferFields() ?
+ new FieldStateVariableAccessor(field)
+ : new GetterStateVariableAccessor(getter);
+ } else if (field != null) {
+ accessor = new FieldStateVariableAccessor(field);
+ } else if (getter != null) {
+ accessor = new GetterStateVariableAccessor(getter);
+ } else {
+ log.finer("No field or getter found for state variable, skipping accessor: " + v.name());
+ }
+
+ StateVariable stateVar =
+ new AnnotationStateVariableBinder(v, v.name(), accessor, stringConvertibleTypes)
+ .createStateVariable();
+
+ map.put(stateVar, accessor);
+ }
+ }
+
+ // State variables declared on fields
+ for (Field field : Reflections.getFields(clazz, UpnpStateVariable.class)) {
+
+ UpnpStateVariable svAnnotation = field.getAnnotation(UpnpStateVariable.class);
+
+ StateVariableAccessor accessor = new FieldStateVariableAccessor(field);
+
+ StateVariable stateVar = new AnnotationStateVariableBinder(
+ svAnnotation,
+ svAnnotation.name().length() == 0
+ ? toUpnpStateVariableName(field.getName())
+ : svAnnotation.name(),
+ accessor,
+ stringConvertibleTypes
+ ).createStateVariable();
+
+ map.put(stateVar, accessor);
+ }
+
+ // State variables declared on getters
+ for (Method getter : Reflections.getMethods(clazz, UpnpStateVariable.class)) {
+
+ String propertyName = Reflections.getMethodPropertyName(getter.getName());
+ if (propertyName == null) {
+ throw new LocalServiceBindingException(
+ "Annotated method is not a getter method (: " + getter
+ );
+ }
+
+ if (getter.getParameterTypes().length > 0)
+ throw new LocalServiceBindingException(
+ "Getter method defined as @UpnpStateVariable can not have parameters: " + getter
+ );
+
+ UpnpStateVariable svAnnotation = getter.getAnnotation(UpnpStateVariable.class);
+
+ StateVariableAccessor accessor = new GetterStateVariableAccessor(getter);
+
+ StateVariable stateVar = new AnnotationStateVariableBinder(
+ svAnnotation,
+ svAnnotation.name().length() == 0
+ ?
+ toUpnpStateVariableName(propertyName)
+ : svAnnotation.name(),
+ accessor,
+ stringConvertibleTypes
+ ).createStateVariable();
+
+ map.put(stateVar, accessor);
+ }
+
+ return map;
+ }
+
+ protected Map readActions(Class> clazz,
+ Map stateVariables,
+ Set stringConvertibleTypes)
+ throws LocalServiceBindingException {
+
+ Map map = new HashMap<>();
+
+ for (Method method : Reflections.getMethods(clazz, UpnpAction.class)) {
+ AnnotationActionBinder actionBinder =
+ new AnnotationActionBinder(method, stateVariables, stringConvertibleTypes);
+ Action action = actionBinder.appendAction(map);
+ if(isActionExcluded(action)) {
+ map.remove(action);
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Override this method to exclude action/methods after they have been discovered.
+ */
+ protected boolean isActionExcluded(Action action) {
+ return false;
+ }
+
+ // TODO: I don't like the exceptions much, user has no idea what to do
+
+ static String toUpnpStateVariableName(String javaName) {
+ if (javaName.length() < 1) {
+ throw new IllegalArgumentException("Variable name must be at least 1 character long");
+ }
+ return javaName.substring(0, 1).toUpperCase(Locale.ROOT) + javaName.substring(1);
+ }
+
+ static String toJavaStateVariableName(String upnpName) {
+ if (upnpName.length() < 1) {
+ throw new IllegalArgumentException("Variable name must be at least 1 character long");
+ }
+ return upnpName.substring(0, 1).toLowerCase(Locale.ROOT) + upnpName.substring(1);
+ }
+
+
+ static String toUpnpActionName(String javaName) {
+ if (javaName.length() < 1) {
+ throw new IllegalArgumentException("Action name must be at least 1 character long");
+ }
+ return javaName.substring(0, 1).toUpperCase(Locale.ROOT) + javaName.substring(1);
+ }
+
+ static String toJavaActionName(String upnpName) {
+ if (upnpName.length() < 1) {
+ throw new IllegalArgumentException("Variable name must be at least 1 character long");
+ }
+ return upnpName.substring(0, 1).toLowerCase(Locale.ROOT) + upnpName.substring(1);
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3d050d33f72f0895b49b074ca3c217671631eff
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import org.fourthline.cling.binding.AllowedValueProvider;
+import org.fourthline.cling.binding.AllowedValueRangeProvider;
+import org.fourthline.cling.binding.LocalServiceBindingException;
+import org.fourthline.cling.model.ModelUtil;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
+import org.fourthline.cling.model.meta.StateVariableEventDetails;
+import org.fourthline.cling.model.meta.StateVariableTypeDetails;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.Datatype;
+
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * @author Christian Bauer
+ */
+public class AnnotationStateVariableBinder {
+
+ private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
+
+ protected UpnpStateVariable annotation;
+ protected String name;
+ protected StateVariableAccessor accessor;
+ protected Set stringConvertibleTypes;
+
+ public AnnotationStateVariableBinder(UpnpStateVariable annotation, String name,
+ StateVariableAccessor accessor, Set stringConvertibleTypes) {
+ this.annotation = annotation;
+ this.name = name;
+ this.accessor = accessor;
+ this.stringConvertibleTypes = stringConvertibleTypes;
+ }
+
+ public UpnpStateVariable getAnnotation() {
+ return annotation;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public StateVariableAccessor getAccessor() {
+ return accessor;
+ }
+
+ public Set getStringConvertibleTypes() {
+ return stringConvertibleTypes;
+ }
+
+ protected StateVariable createStateVariable() throws LocalServiceBindingException {
+
+ log.fine("Creating state variable '" + getName() + "' with accessor: " + getAccessor());
+
+ // Datatype
+ Datatype datatype = createDatatype();
+
+ // Default value
+ String defaultValue = createDefaultValue(datatype);
+
+ // Allowed values
+ String[] allowedValues = null;
+ if (Datatype.Builtin.STRING.equals(datatype.getBuiltin())) {
+
+ if (getAnnotation().allowedValueProvider() != void.class) {
+ allowedValues = getAllowedValuesFromProvider();
+ } else if (getAnnotation().allowedValues().length > 0) {
+ allowedValues = getAnnotation().allowedValues();
+ } else if (getAnnotation().allowedValuesEnum() != void.class) {
+ allowedValues = getAllowedValues(getAnnotation().allowedValuesEnum());
+ } else if (getAccessor() != null && getAccessor().getReturnType().isEnum()) {
+ allowedValues = getAllowedValues(getAccessor().getReturnType());
+ } else {
+ log.finer("Not restricting allowed values (of string typed state var): " + getName());
+ }
+
+ if (allowedValues != null && defaultValue != null) {
+
+ // Check if the default value is an allowed value
+ boolean foundValue = false;
+ for (String s : allowedValues) {
+ if (s.equals(defaultValue)) {
+ foundValue = true;
+ break;
+ }
+ }
+ if (!foundValue) {
+ throw new LocalServiceBindingException(
+ "Default value '" + defaultValue + "' is not in allowed values of: " + getName()
+ );
+ }
+ }
+ }
+
+ // Allowed value range
+ StateVariableAllowedValueRange allowedValueRange = null;
+ if (Datatype.Builtin.isNumeric(datatype.getBuiltin())) {
+
+ if (getAnnotation().allowedValueRangeProvider() != void.class) {
+ allowedValueRange = getAllowedRangeFromProvider();
+ } else if (getAnnotation().allowedValueMinimum() > 0 || getAnnotation().allowedValueMaximum() > 0) {
+ allowedValueRange = getAllowedValueRange(
+ getAnnotation().allowedValueMinimum(),
+ getAnnotation().allowedValueMaximum(),
+ getAnnotation().allowedValueStep()
+ );
+ } else {
+ log.finer("Not restricting allowed value range (of numeric typed state var): " + getName());
+ }
+
+ // Check if the default value is an allowed value
+ if (defaultValue != null && allowedValueRange != null) {
+
+ long v;
+ try {
+ v = Long.valueOf(defaultValue);
+ } catch (Exception ex) {
+ throw new LocalServiceBindingException(
+ "Default value '" + defaultValue + "' is not numeric (for range checking) of: " + getName()
+ );
+ }
+
+ if (!allowedValueRange.isInRange(v)) {
+ throw new LocalServiceBindingException(
+ "Default value '" + defaultValue + "' is not in allowed range of: " + getName()
+ );
+ }
+ }
+ }
+
+ // Event details
+ boolean sendEvents = getAnnotation().sendEvents();
+ if (sendEvents && getAccessor() == null) {
+ throw new LocalServiceBindingException(
+ "State variable sends events but has no accessor for field or getter: " + getName()
+ );
+ }
+
+ int eventMaximumRateMillis = 0;
+ int eventMinimumDelta = 0;
+ if (sendEvents) {
+ if (getAnnotation().eventMaximumRateMilliseconds() > 0) {
+ log.finer("Moderating state variable events using maximum rate (milliseconds): " + getAnnotation().eventMaximumRateMilliseconds());
+ eventMaximumRateMillis = getAnnotation().eventMaximumRateMilliseconds();
+ }
+
+ if (getAnnotation().eventMinimumDelta() > 0 && Datatype.Builtin.isNumeric(datatype.getBuiltin())) {
+ // TODO: Doesn't consider floating point types!
+ log.finer("Moderating state variable events using minimum delta: " + getAnnotation().eventMinimumDelta());
+ eventMinimumDelta = getAnnotation().eventMinimumDelta();
+ }
+ }
+
+ StateVariableTypeDetails typeDetails =
+ new StateVariableTypeDetails(datatype, defaultValue, allowedValues, allowedValueRange);
+
+ StateVariableEventDetails eventDetails =
+ new StateVariableEventDetails(sendEvents, eventMaximumRateMillis, eventMinimumDelta);
+
+ return new StateVariable(getName(), typeDetails, eventDetails);
+ }
+
+ protected Datatype createDatatype() throws LocalServiceBindingException {
+
+ String declaredDatatype = getAnnotation().datatype();
+
+ if (declaredDatatype.length() == 0 && getAccessor() != null) {
+ Class returnType = getAccessor().getReturnType();
+ log.finer("Using accessor return type as state variable type: " + returnType);
+
+ if (ModelUtil.isStringConvertibleType(getStringConvertibleTypes(), returnType)) {
+ // Enums and toString() convertible types are always state variables with type STRING
+ log.finer("Return type is string-convertible, using string datatype");
+ return Datatype.Default.STRING.getBuiltinType().getDatatype();
+ } else {
+ Datatype.Default defaultDatatype = Datatype.Default.getByJavaType(returnType);
+ if (defaultDatatype != null) {
+ log.finer("Return type has default UPnP datatype: " + defaultDatatype);
+ return defaultDatatype.getBuiltinType().getDatatype();
+ }
+ }
+ }
+
+ // We can also guess that if the allowed values are set then it's a string
+ if ((declaredDatatype == null || declaredDatatype.length() == 0) &&
+ (getAnnotation().allowedValues().length > 0 || getAnnotation().allowedValuesEnum() != void.class)) {
+ log.finer("State variable has restricted allowed values, hence using 'string' datatype");
+ declaredDatatype = "string";
+ }
+
+ // If we still don't have it, there is nothing more we can do
+ if (declaredDatatype == null || declaredDatatype.length() == 0) {
+ throw new LocalServiceBindingException("Could not detect datatype of state variable: " + getName());
+ }
+
+ log.finer("Trying to find built-in UPnP datatype for detected name: " + declaredDatatype);
+
+ // Now try to find the actual UPnP datatype by mapping the Default to Builtin
+ Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(declaredDatatype);
+ if (builtin != null) {
+ log.finer("Found built-in UPnP datatype: " + builtin);
+ return builtin.getDatatype();
+ } else {
+ // TODO
+ throw new LocalServiceBindingException("No built-in UPnP datatype found, using CustomDataType (TODO: NOT IMPLEMENTED)");
+ }
+ }
+
+ protected String createDefaultValue(Datatype datatype) throws LocalServiceBindingException {
+
+ // Next, the default value of the state variable, first the declared one
+ if (getAnnotation().defaultValue().length() != 0) {
+ // The declared default value needs to match the datatype
+ try {
+ datatype.valueOf(getAnnotation().defaultValue());
+ log.finer("Found state variable default value: " + getAnnotation().defaultValue());
+ return getAnnotation().defaultValue();
+ } catch (Exception ex) {
+ throw new LocalServiceBindingException(
+ "Default value doesn't match datatype of state variable '" + getName() + "': " + ex.getMessage()
+ );
+ }
+ }
+
+ return null;
+ }
+
+ protected String[] getAllowedValues(Class enumType) throws LocalServiceBindingException {
+
+ if (!enumType.isEnum()) {
+ throw new LocalServiceBindingException("Allowed values type is not an Enum: " + enumType);
+ }
+
+ log.finer("Restricting allowed values of state variable to Enum: " + getName());
+ String[] allowedValueStrings = new String[enumType.getEnumConstants().length];
+ for (int i = 0; i < enumType.getEnumConstants().length; i++) {
+ Object o = enumType.getEnumConstants()[i];
+ if (o.toString().length() > 32) {
+ throw new LocalServiceBindingException(
+ "Allowed value string (that is, Enum constant name) is longer than 32 characters: " + o.toString()
+ );
+ }
+ log.finer("Adding allowed value (converted to string): " + o.toString());
+ allowedValueStrings[i] = o.toString();
+ }
+
+ return allowedValueStrings;
+ }
+
+ protected StateVariableAllowedValueRange getAllowedValueRange(long min,
+ long max,
+ long step) throws LocalServiceBindingException {
+ if (max < min) {
+ throw new LocalServiceBindingException(
+ "Allowed value range maximum is smaller than minimum: " + getName()
+ );
+ }
+
+ return new StateVariableAllowedValueRange(min, max, step);
+ }
+
+ protected String[] getAllowedValuesFromProvider() throws LocalServiceBindingException {
+ Class provider = getAnnotation().allowedValueProvider();
+ if (!AllowedValueProvider.class.isAssignableFrom(provider))
+ throw new LocalServiceBindingException(
+ "Allowed value provider is not of type " + AllowedValueProvider.class + ": " + getName()
+ );
+ try {
+ return ((Class extends AllowedValueProvider>) provider).newInstance().getValues();
+ } catch (Exception ex) {
+ throw new LocalServiceBindingException(
+ "Allowed value provider can't be instantiated: " + getName(), ex
+ );
+ }
+ }
+
+ protected StateVariableAllowedValueRange getAllowedRangeFromProvider() throws LocalServiceBindingException {
+ Class provider = getAnnotation().allowedValueRangeProvider();
+ if (!AllowedValueRangeProvider.class.isAssignableFrom(provider))
+ throw new LocalServiceBindingException(
+ "Allowed value range provider is not of type " + AllowedValueRangeProvider.class + ": " + getName()
+ );
+ try {
+ AllowedValueRangeProvider providerInstance =
+ ((Class extends AllowedValueRangeProvider>) provider).newInstance();
+ return getAllowedValueRange(
+ providerInstance.getMinimum(),
+ providerInstance.getMaximum(),
+ providerInstance.getStep()
+ );
+ } catch (Exception ex) {
+ throw new LocalServiceBindingException(
+ "Allowed value range provider can't be instantiated: " + getName(), ex
+ );
+ }
+ }
+
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpAction.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d551bf720df63e33105256bfce6708532475d39
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpAction.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpAction {
+
+ String name() default "";
+ UpnpOutputArgument[] out() default {};
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpInputArgument.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpInputArgument.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c7c589364753f618acbb64d47c3d172406a5d21
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpInputArgument.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Retention;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpInputArgument {
+
+ String name();
+ String[] aliases() default {};
+ String stateVariable() default "";
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpOutputArgument.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpOutputArgument.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac3506c28358fe3d64b72c63a445ff43c067e529
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpOutputArgument.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Retention;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpOutputArgument {
+
+ String name();
+ String stateVariable() default "";
+ String getterName() default "";
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpService.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpService.java
new file mode 100644
index 0000000000000000000000000000000000000000..899f11f600c88be8557b8ae58f345f23b0b8331f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface UpnpService {
+
+ UpnpServiceId serviceId();
+ UpnpServiceType serviceType();
+
+ boolean supportsQueryStateVariables() default true;
+ Class[] stringConvertibleTypes() default {};
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceId.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceId.java
new file mode 100644
index 0000000000000000000000000000000000000000..b81184ac042d54f0ba21a394e2a7b8a141fdb74e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceId.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import org.fourthline.cling.model.types.UDAServiceId;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+
+@Target({})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpServiceId {
+
+ String namespace() default UDAServiceId.DEFAULT_NAMESPACE;
+ String value();
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceType.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceType.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e4dfececa7bac172412ee52fd429316571e5170
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpServiceType.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import org.fourthline.cling.model.types.UDAServiceType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Target({})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpServiceType {
+
+ String namespace() default UDAServiceType.DEFAULT_NAMESPACE;
+ String value();
+ int version() default 1;
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariable.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariable.java
new file mode 100644
index 0000000000000000000000000000000000000000..de2128edee44e8852cbf8269ea3e33542b9e14e9
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariable.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UpnpStateVariable {
+
+ String name() default "";
+ String datatype() default "";
+
+ String defaultValue() default "";
+
+ // String types
+ String[] allowedValues() default {};
+ Class allowedValuesEnum() default void.class;
+
+ // Numeric types
+ long allowedValueMinimum() default 0;
+ long allowedValueMaximum() default 0;
+ long allowedValueStep() default 1;
+
+ // Dynamic
+ Class allowedValueProvider() default void.class;
+ Class allowedValueRangeProvider() default void.class;
+
+ boolean sendEvents() default true;
+ int eventMaximumRateMilliseconds() default 0;
+ int eventMinimumDelta() default 0;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariables.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariables.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e16ccb1eafc066f0a9fa5c4df1683ba427650cc
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/annotations/UpnpStateVariables.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.annotations;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Retention;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface UpnpStateVariables {
+
+ UpnpStateVariable[] value() default {};
+ boolean preferFields() default true;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAction.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..20aa64a4099fea1279af9a2da952662a28a20a89
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAction.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.ActionArgument;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableAction {
+
+ public String name;
+ public List arguments = new ArrayList<>();
+
+ public Action build() {
+ return new Action(name, createActionArgumennts());
+ }
+
+ public ActionArgument[] createActionArgumennts() {
+ ActionArgument[] array = new ActionArgument[arguments.size()];
+ int i = 0;
+ for (MutableActionArgument argument : arguments) {
+ array[i++] = argument.build();
+ }
+ return array;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableActionArgument.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableActionArgument.java
new file mode 100644
index 0000000000000000000000000000000000000000..0369b86657ab319491db180c806c63b004685dfd
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableActionArgument.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.meta.ActionArgument;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableActionArgument {
+
+ public String name;
+ public String relatedStateVariable;
+ public ActionArgument.Direction direction;
+ public boolean retval;
+
+ public ActionArgument build() {
+ return new ActionArgument(name, relatedStateVariable, direction, retval);
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAllowedValueRange.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAllowedValueRange.java
new file mode 100644
index 0000000000000000000000000000000000000000..c87aedec4acfc400ae814cd98b2168d084b64073
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableAllowedValueRange.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableAllowedValueRange {
+
+ // TODO: UPNP VIOLATION: Some devices (Netgear Router again...) send empty elements, so use some sane defaults
+ // TODO: UPNP VIOLATION: The WANCommonInterfaceConfig example XML is even wrong, it does not include a element!
+ public Long minimum = 0l;
+ public Long maximum = Long.MAX_VALUE;
+ public Long step = 1l;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableDevice.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableDevice.java
new file mode 100644
index 0000000000000000000000000000000000000000..30175247d6bbc9f6c2156de6cedcbf76fd49471f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableDevice.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.DeviceDetails;
+import org.fourthline.cling.model.meta.Icon;
+import org.fourthline.cling.model.meta.ManufacturerDetails;
+import org.fourthline.cling.model.meta.ModelDetails;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.UDAVersion;
+import org.fourthline.cling.model.types.DLNACaps;
+import org.fourthline.cling.model.types.DLNADoc;
+import org.fourthline.cling.model.types.DeviceType;
+import org.fourthline.cling.model.types.UDN;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableDevice {
+
+ public UDN udn;
+ public MutableUDAVersion udaVersion = new MutableUDAVersion();
+ public URL baseURL;
+ public String deviceType;
+ public String friendlyName;
+ public String manufacturer;
+ public URI manufacturerURI;
+ public String modelName;
+ public String modelDescription;
+ public String modelNumber;
+ public URI modelURI;
+ public String serialNumber;
+ public String upc;
+ public URI presentationURI;
+ public List dlnaDocs = new ArrayList<>();
+ public DLNACaps dlnaCaps;
+ public List icons = new ArrayList<>();
+ public List services = new ArrayList<>();
+ public List embeddedDevices = new ArrayList<>();
+ public MutableDevice parentDevice;
+
+ public Device build(Device prototype) throws ValidationException {
+ // Note how all embedded devices inherit the version and baseURL of the root!
+ return build(prototype, createDeviceVersion(), baseURL);
+ }
+
+ public Device build(Device prototype, UDAVersion deviceVersion, URL baseURL) throws ValidationException {
+
+ List embeddedDevicesList = new ArrayList<>();
+ for (MutableDevice embeddedDevice : embeddedDevices) {
+ embeddedDevicesList.add(embeddedDevice.build(prototype, deviceVersion, baseURL));
+ }
+ return prototype.newInstance(
+ udn,
+ deviceVersion,
+ createDeviceType(),
+ createDeviceDetails(baseURL),
+ createIcons(),
+ createServices(prototype),
+ embeddedDevicesList
+ );
+ }
+
+ public UDAVersion createDeviceVersion() {
+ return new UDAVersion(udaVersion.major, udaVersion.minor);
+ }
+
+ public DeviceType createDeviceType() {
+ return DeviceType.valueOf(deviceType);
+ }
+
+ public DeviceDetails createDeviceDetails(URL baseURL) {
+ return new DeviceDetails(
+ baseURL,
+ friendlyName,
+ new ManufacturerDetails(manufacturer, manufacturerURI),
+ new ModelDetails(modelName, modelDescription, modelNumber, modelURI),
+ serialNumber, upc, presentationURI, dlnaDocs.toArray(new DLNADoc[dlnaDocs.size()]), dlnaCaps
+ );
+ }
+
+ public Icon[] createIcons() {
+ Icon[] iconArray = new Icon[icons.size()];
+ int i = 0;
+ for (MutableIcon icon : icons) {
+ iconArray[i++] = icon.build();
+ }
+ return iconArray;
+ }
+
+ public Service[] createServices(Device prototype) throws ValidationException {
+ Service[] services = prototype.newServiceArray(this.services.size());
+ int i = 0;
+ for (MutableService service : this.services) {
+ services[i++] = service.build(prototype);
+ }
+ return services;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableIcon.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableIcon.java
new file mode 100644
index 0000000000000000000000000000000000000000..b50b320b6b88525f29946c58e8dc974286f60efd
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableIcon.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.meta.Icon;
+
+import java.net.URI;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableIcon {
+
+ public String mimeType;
+ public int width;
+ public int height;
+ public int depth;
+ public URI uri;
+
+ public Icon build() {
+ return new Icon(mimeType, width, height, depth, uri);
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableService.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableService.java
new file mode 100644
index 0000000000000000000000000000000000000000..29c08c5a91dbcddbf3203f421009ed8b29f0cb39
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.ServiceType;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableService {
+
+ public ServiceType serviceType;
+ public ServiceId serviceId;
+ public URI descriptorURI;
+ public URI controlURI;
+ public URI eventSubscriptionURI;
+
+ public List actions = new ArrayList<>();
+ public List stateVariables = new ArrayList<>();
+
+ public Service build(Device prototype) throws ValidationException {
+ return prototype.newInstance(
+ serviceType, serviceId,
+ descriptorURI, controlURI, eventSubscriptionURI,
+ createActions(),
+ createStateVariables()
+ );
+ }
+
+ public Action[] createActions() {
+ Action[] array = new Action[actions.size()];
+ int i = 0;
+ for (MutableAction action : actions) {
+ array[i++] = action.build();
+ }
+ return array;
+ }
+
+ public StateVariable[] createStateVariables() {
+ StateVariable[] array = new StateVariable[stateVariables.size()];
+ int i = 0;
+ for (MutableStateVariable stateVariable : stateVariables) {
+ array[i++] = stateVariable.build();
+ }
+ return array;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableStateVariable.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableStateVariable.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ab723aad96dcdb5eabe2dff293a139ed2c1e6d5
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableStateVariable.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
+import org.fourthline.cling.model.meta.StateVariableEventDetails;
+import org.fourthline.cling.model.meta.StateVariableTypeDetails;
+import org.fourthline.cling.model.types.Datatype;
+
+import java.util.List;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableStateVariable {
+
+ public String name;
+ public Datatype dataType;
+ public String defaultValue;
+ public List allowedValues;
+ public MutableAllowedValueRange allowedValueRange;
+ public StateVariableEventDetails eventDetails;
+
+ public StateVariable build() {
+ return new StateVariable(
+ name,
+ new StateVariableTypeDetails(
+ dataType,
+ defaultValue,
+ allowedValues == null || allowedValues.size() == 0
+ ? null
+ : allowedValues.toArray(new String[allowedValues.size()]),
+ allowedValueRange == null
+ ? null :
+ new StateVariableAllowedValueRange(
+ allowedValueRange.minimum,
+ allowedValueRange.maximum,
+ allowedValueRange.step
+ )
+ ),
+ eventDetails
+ );
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableUDAVersion.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableUDAVersion.java
new file mode 100644
index 0000000000000000000000000000000000000000..432ddbf8c9a9e6c0d4674772b700f84f2368b9fa
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/staging/MutableUDAVersion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.staging;
+
+/**
+ * @author Christian Bauer
+ */
+public class MutableUDAVersion {
+ public int major = 1;
+ public int minor = 0;
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/Descriptor.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/Descriptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..95b468e9953054729841aafe61da74a55c192cac
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/Descriptor.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.w3c.dom.Node;
+
+/**
+ * Utility interface with static declarations of all "strings".
+ *
+ * @author Christian Bauer
+ */
+public abstract class Descriptor {
+
+ public interface Device {
+
+ public static final String NAMESPACE_URI = "urn:schemas-upnp-org:device-1-0";
+ public static final String DLNA_NAMESPACE_URI = "urn:schemas-dlna-org:device-1-0";
+ public static final String DLNA_PREFIX = "dlna";
+ public static final String SEC_NAMESPACE_URI = "http://www.sec.co.kr/dlna";
+ public static final String SEC_PREFIX = "sec";
+
+ public enum ELEMENT {
+ root,
+ specVersion, major, minor,
+ URLBase,
+ device,
+ UDN,
+ X_DLNADOC,
+ X_DLNACAP,
+ ProductCap,
+ X_ProductCap,
+ deviceType,
+ friendlyName,
+ manufacturer,
+ manufacturerURL,
+ modelDescription,
+ modelName,
+ modelNumber,
+ modelURL,
+ presentationURL,
+ UPC,
+ serialNumber,
+ iconList, icon, width, height, depth, url, mimetype,
+ serviceList, service, serviceType, serviceId, SCPDURL, controlURL, eventSubURL,
+ deviceList;
+
+ public static ELEMENT valueOrNullOf(String s) {
+ try {
+ return valueOf(s);
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ }
+
+ public boolean equals(Node node) {
+ return toString().equals(node.getLocalName());
+ }
+ }
+ }
+
+ public interface Service {
+
+ public static final String NAMESPACE_URI = "urn:schemas-upnp-org:service-1-0";
+
+ public enum ELEMENT {
+ scpd,
+ specVersion, major, minor,
+ actionList, action, name,
+ argumentList, argument, direction, relatedStateVariable, retval,
+ serviceStateTable, stateVariable, dataType, defaultValue,
+ allowedValueList, allowedValue, allowedValueRange, minimum, maximum, step;
+
+ public static ELEMENT valueOrNullOf(String s) {
+ try {
+ return valueOf(s);
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ }
+
+ public boolean equals(Node node) {
+ return toString().equals(node.getLocalName());
+ }
+
+ }
+
+ public enum ATTRIBUTE {
+ sendEvents
+ }
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DescriptorBindingException.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DescriptorBindingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f36f51f0eaa4d4f2a6ae4e065f6cadb1277db40c
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DescriptorBindingException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+/**
+ * Thrown if device or service descriptor metadata couldn't be read or written.
+ *
+ * @author Christian Bauer
+ */
+public class DescriptorBindingException extends Exception {
+
+ public DescriptorBindingException(String s) {
+ super(s);
+ }
+
+ public DescriptorBindingException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
+
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DeviceDescriptorBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DeviceDescriptorBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c4a5cffcc24a294a6274826f1f67371dd8586f3
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/DeviceDescriptorBinder.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.profile.RemoteClientInfo;
+import org.w3c.dom.Document;
+
+/**
+ * Reads and generates device descriptor XML metadata.
+ *
+ * @author Christian Bauer
+ */
+public interface DeviceDescriptorBinder {
+
+ public T describe(T undescribedDevice, String descriptorXml)
+ throws DescriptorBindingException, ValidationException;
+
+ public T describe(T undescribedDevice, Document dom)
+ throws DescriptorBindingException, ValidationException;
+
+ public String generate(Device device, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException;
+
+ public Document buildDOM(Device device, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..00c5f6ed06f2b86c4f72b83c9493fab72314d420
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.Device;
+import org.seamless.util.Exceptions;
+import org.seamless.xml.ParserException;
+import org.seamless.xml.XmlPullParserUtils;
+import org.xml.sax.SAXParseException;
+
+import java.util.Locale;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Michael Pujos
+ */
+public class RecoveringUDA10DeviceDescriptorBinderImpl extends UDA10DeviceDescriptorBinderImpl {
+
+ private static Logger log = Logger.getLogger(RecoveringUDA10DeviceDescriptorBinderImpl.class.getName());
+
+ @Override
+ public D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {
+
+ D device = null;
+ DescriptorBindingException originalException;
+ try {
+
+ try {
+ if (descriptorXml != null)
+ descriptorXml = descriptorXml.trim(); // Always trim whitespace
+ device = super.describe(undescribedDevice, descriptorXml);
+ return device;
+ } catch (DescriptorBindingException ex) {
+ log.warning("Regular parsing failed: " + Exceptions.unwrap(ex).getMessage());
+ originalException = ex;
+ }
+
+ String fixedXml;
+ // The following modifications are not cumulative!
+
+ fixedXml = fixGarbageLeadingChars(descriptorXml);
+ if (fixedXml != null) {
+ try {
+ device = super.describe(undescribedDevice, fixedXml);
+ return device;
+ } catch (DescriptorBindingException ex) {
+ log.warning("Removing leading garbage didn't work: " + Exceptions.unwrap(ex).getMessage());
+ }
+ }
+
+ fixedXml = fixGarbageTrailingChars(descriptorXml, originalException);
+ if (fixedXml != null) {
+ try {
+ device = super.describe(undescribedDevice, fixedXml);
+ return device;
+ } catch (DescriptorBindingException ex) {
+ log.warning("Removing trailing garbage didn't work: " + Exceptions.unwrap(ex).getMessage());
+ }
+ }
+
+ // Try to fix "up to five" missing namespace declarations
+ DescriptorBindingException lastException = originalException;
+ fixedXml = descriptorXml;
+ for (int retryCount = 0; retryCount < 5; retryCount++) {
+ fixedXml = fixMissingNamespaces(fixedXml, lastException);
+ if (fixedXml != null) {
+ try {
+ device = super.describe(undescribedDevice, fixedXml);
+ return device;
+ } catch (DescriptorBindingException ex) {
+ log.warning("Fixing namespace prefix didn't work: " + Exceptions.unwrap(ex).getMessage());
+ lastException = ex;
+ }
+ } else {
+ break; // We can stop, no more namespace fixing can be done
+ }
+ }
+
+ fixedXml = XmlPullParserUtils.fixXMLEntities(descriptorXml);
+ if(!fixedXml.equals(descriptorXml)) {
+ try {
+ device = super.describe(undescribedDevice, fixedXml);
+ return device;
+ } catch (DescriptorBindingException ex) {
+ log.warning("Fixing XML entities didn't work: " + Exceptions.unwrap(ex).getMessage());
+ }
+ }
+
+ handleInvalidDescriptor(descriptorXml, originalException);
+
+ } catch (ValidationException ex) {
+ device = handleInvalidDevice(descriptorXml, device, ex);
+ if (device != null)
+ return device;
+ }
+ throw new IllegalStateException("No device produced, did you swallow exceptions in your subclass?");
+ }
+
+ private String fixGarbageLeadingChars(String descriptorXml) {
+ /* Recover this:
+
+ HTTP/1.1 200 OK
+ Content-Length: 4268
+ Content-Type: text/xml; charset="utf-8"
+ Server: Microsoft-Windows/6.2 UPnP/1.0 UPnP-Device-Host/1.0 Microsoft-HTTPAPI/2.0
+ Date: Sun, 07 Apr 2013 02:11:30 GMT
+
+ @7:5 in java.io.StringReader@407f6b00) : HTTP/1.1 200 OK
+ Content-Length: 4268
+ Content-Type: text/xml; charset="utf-8"
+ Server: Microsoft-Windows/6.2 UPnP/1.0 UPnP-Device-Host/1.0 Microsoft-HTTPAPI/2.0
+ Date: Sun, 07 Apr 2013 02:11:30 GMT
+
+ ...
+ */
+
+ int index = descriptorXml.indexOf("");
+ if (index == -1) {
+ log.warning("No closing element in descriptor");
+ return null;
+ }
+ if (descriptorXml.length() != index + "".length()) {
+ log.warning("Detected garbage characters after node, removing");
+ return descriptorXml.substring(0, index) + "";
+ }
+ return null;
+ }
+
+ protected String fixMissingNamespaces(String descriptorXml, DescriptorBindingException ex) {
+ // Windows: org.fourthline.cling.binding.xml.DescriptorBindingException: Could not parse device descriptor: org.seamless.xml.ParserException: org.xml.sax.SAXParseException: The prefix "dlna" for element "dlna:X_DLNADOC" is not bound.
+ // Android: org.xmlpull.v1.XmlPullParserException: undefined prefix: dlna (position:START_TAG <{null}dlna:X_DLNADOC>@19:17 in java.io.StringReader@406dff48)
+
+ // We can only handle certain exceptions, depending on their type and message
+ Throwable cause = ex.getCause();
+ if (!((cause instanceof SAXParseException) || (cause instanceof ParserException)))
+ return null;
+ String message = cause.getMessage();
+ if (message == null)
+ return null;
+
+ Pattern pattern = Pattern.compile("The prefix \"(.*)\" for element"); // Windows
+ Matcher matcher = pattern.matcher(message);
+ if (!matcher.find() || matcher.groupCount() != 1) {
+ pattern = Pattern.compile("undefined prefix: ([^ ]*)"); // Android
+ matcher = pattern.matcher(message);
+ if (!matcher.find() || matcher.groupCount() != 1)
+ return null;
+ }
+
+ String missingNS = matcher.group(1);
+ log.warning("Fixing missing namespace declaration for: " + missingNS);
+
+ // Extract attributes
+ pattern = Pattern.compile("]*)");
+ matcher = pattern.matcher(descriptorXml);
+ if (!matcher.find() || matcher.groupCount() != 1) {
+ log.fine("Could not find element attributes");
+ return null;
+ }
+
+ String rootAttributes = matcher.group(1);
+ log.fine("Preserving existing element attributes/namespace declarations: " + matcher.group(0));
+
+ // Extract body
+ pattern = Pattern.compile("]*>(.*)", Pattern.DOTALL);
+ matcher = pattern.matcher(descriptorXml);
+ if (!matcher.find() || matcher.groupCount() != 1) {
+ log.fine("Could not extract body of element");
+ return null;
+ }
+
+ String rootBody = matcher.group(1);
+
+ // Add missing namespace, it only matters that it is defined, not that it is correct
+ return ""
+ + ""
+ + rootBody
+ + "";
+
+ // TODO: Should we match different undeclared prefixes with their correct namespace?
+ // So if it's "dlna" we use "urn:schemas-dlna-org:device-1-0" etc.
+ }
+
+ /**
+ * Handle processing errors while reading XML descriptors.
+ *
+ *
+ * Typically you want to log this problem or create an error report, and in any
+ * case, throw a {@link DescriptorBindingException} to notify the caller of the
+ * binder of this failure. The default implementation simply rethrows the
+ * given exception.
+ *
+ *
+ * @param xml The original XML causing the parsing failure.
+ * @param exception The original exception while parsing the XML.
+ */
+ protected void handleInvalidDescriptor(String xml, DescriptorBindingException exception)
+ throws DescriptorBindingException {
+ throw exception;
+ }
+
+ /**
+ * Handle processing errors while binding XML descriptors.
+ *
+ *
+ * Typically you want to log this problem or create an error report. You
+ * should throw a {@link ValidationException} to notify the caller of the
+ * binder of failure. The default implementation simply rethrows the
+ * given exception.
+ *
+ *
+ * This method gives you a final chance to fix the problem, instead of
+ * throwing an exception, you could try to create valid {@link Device}
+ * model and return it.
+ *
+ *
+ * @param xml The original XML causing the binding failure.
+ * @param device The unfinished {@link Device} that failed validation
+ * @param exception The errors found when validating the {@link Device} model.
+ * @return Device A "fixed" {@link Device} model, instead of throwing an exception.
+ */
+ protected D handleInvalidDevice(String xml, D device, ValidationException exception)
+ throws ValidationException {
+ throw exception;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/ServiceDescriptorBinder.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/ServiceDescriptorBinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e5545fac7970dfbb08b6eadcd7349b31b4f06dd
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/ServiceDescriptorBinder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.Service;
+import org.w3c.dom.Document;
+
+/**
+ * Reads and generates service descriptor XML metadata.
+ *
+ * @author Christian Bauer
+ */
+public interface ServiceDescriptorBinder {
+
+ public T describe(T undescribedService, String descriptorXml)
+ throws DescriptorBindingException, ValidationException;
+
+ public T describe(T undescribedService, Document dom)
+ throws DescriptorBindingException, ValidationException;
+
+ public String generate(Service service) throws DescriptorBindingException;
+
+ public Document buildDOM(Service service) throws DescriptorBindingException;
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..9359fb3f13b8a8ae5627630fb50cd6de81f6696e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import static org.fourthline.cling.model.XMLUtil.appendNewElement;
+import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull;
+
+import java.io.StringReader;
+import java.net.URI;
+import java.net.URL;
+import java.util.logging.Logger;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.fourthline.cling.binding.staging.MutableDevice;
+import org.fourthline.cling.binding.staging.MutableIcon;
+import org.fourthline.cling.binding.staging.MutableService;
+import org.fourthline.cling.binding.xml.Descriptor.Device.ELEMENT;
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.XMLUtil;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.DeviceDetails;
+import org.fourthline.cling.model.meta.Icon;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.RemoteDevice;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.profile.RemoteClientInfo;
+import org.fourthline.cling.model.types.DLNACaps;
+import org.fourthline.cling.model.types.DLNADoc;
+import org.fourthline.cling.model.types.InvalidValueException;
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.model.types.UDN;
+import org.seamless.util.Exceptions;
+import org.seamless.util.MimeType;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+/**
+ * Implementation based on JAXP DOM.
+ *
+ * @author Christian Bauer
+ */
+public class UDA10DeviceDescriptorBinderImpl implements DeviceDescriptorBinder, ErrorHandler {
+
+ private static Logger log = Logger.getLogger(DeviceDescriptorBinder.class.getName());
+
+ public D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {
+
+ if (descriptorXml == null || descriptorXml.length() == 0) {
+ throw new DescriptorBindingException("Null or empty descriptor");
+ }
+
+ try {
+ log.fine("Populating device from XML descriptor: " + undescribedDevice);
+ // We can not validate the XML document. There is no possible XML schema (maybe RELAX NG) that would properly
+ // constrain the UDA 1.0 device descriptor documents: Any unknown element or attribute must be ignored, order of elements
+ // is not guaranteed. Try to write a schema for that! No combination of and
+ // works with that... But hey, MSFT sure has great tech guys! So what we do here is just parsing out the known elements
+ // and ignoring the other shit. We'll also do some very very basic validation of required elements, but that's it.
+
+ // And by the way... try this with JAXB instead of manual DOM processing! And you thought it couldn't get worse....
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder documentBuilder = factory.newDocumentBuilder();
+ documentBuilder.setErrorHandler(this);
+
+ Document d = documentBuilder.parse(
+ new InputSource(
+ // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
+ new StringReader(descriptorXml.trim())
+ )
+ );
+
+ return describe(undescribedDevice, d);
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse device descriptor: " + ex.toString(), ex);
+ }
+ }
+
+ public D describe(D undescribedDevice, Document dom) throws DescriptorBindingException, ValidationException {
+ try {
+ log.fine("Populating device from DOM: " + undescribedDevice);
+
+ // Read the XML into a mutable descriptor graph
+ MutableDevice descriptor = new MutableDevice();
+ Element rootElement = dom.getDocumentElement();
+ hydrateRoot(descriptor, rootElement);
+
+ // Build the immutable descriptor graph
+ return buildInstance(undescribedDevice, descriptor);
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse device DOM: " + ex.toString(), ex);
+ }
+ }
+
+ public D buildInstance(D undescribedDevice, MutableDevice descriptor) throws ValidationException {
+ return (D) descriptor.build(undescribedDevice);
+ }
+
+ protected void hydrateRoot(MutableDevice descriptor, Element rootElement) throws DescriptorBindingException {
+
+ if (rootElement.getNamespaceURI() == null || !rootElement.getNamespaceURI().equals(Descriptor.Device.NAMESPACE_URI)) {
+ log.warning("Wrong XML namespace declared on root element: " + rootElement.getNamespaceURI());
+ }
+
+ if (!rootElement.getNodeName().equals(ELEMENT.root.name())) {
+ throw new DescriptorBindingException("Root element name is not : " + rootElement.getNodeName());
+ }
+
+ NodeList rootChildren = rootElement.getChildNodes();
+
+ Node deviceNode = null;
+
+ for (int i = 0; i < rootChildren.getLength(); i++) {
+ Node rootChild = rootChildren.item(i);
+
+ if (rootChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.specVersion.equals(rootChild)) {
+ hydrateSpecVersion(descriptor, rootChild);
+ } else if (ELEMENT.URLBase.equals(rootChild)) {
+ try {
+ String urlString = XMLUtil.getTextContent(rootChild);
+ if (urlString != null && urlString.length() > 0) {
+ // We hope it's RFC 2396 and RFC 2732 compliant
+ descriptor.baseURL = new URL(urlString);
+ }
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Invalid URLBase: " + ex.getMessage());
+ }
+ } else if (ELEMENT.device.equals(rootChild)) {
+ // Just sanity check here...
+ if (deviceNode != null)
+ throw new DescriptorBindingException("Found multiple elements in ");
+ deviceNode = rootChild;
+ } else {
+ log.finer("Ignoring unknown element: " + rootChild.getNodeName());
+ }
+ }
+
+ if (deviceNode == null) {
+ throw new DescriptorBindingException("No element in ");
+ }
+ hydrateDevice(descriptor, deviceNode);
+ }
+
+ public void hydrateSpecVersion(MutableDevice descriptor, Node specVersionNode) throws DescriptorBindingException {
+
+ NodeList specVersionChildren = specVersionNode.getChildNodes();
+ for (int i = 0; i < specVersionChildren.getLength(); i++) {
+ Node specVersionChild = specVersionChildren.item(i);
+
+ if (specVersionChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.major.equals(specVersionChild)) {
+ String version = XMLUtil.getTextContent(specVersionChild).trim();
+ if (!version.equals("1")) {
+ log.warning("Unsupported UDA major version, ignoring: " + version);
+ version = "1";
+ }
+ descriptor.udaVersion.major = Integer.valueOf(version);
+ } else if (ELEMENT.minor.equals(specVersionChild)) {
+ String version = XMLUtil.getTextContent(specVersionChild).trim();
+ if (!version.equals("0")) {
+ log.warning("Unsupported UDA minor version, ignoring: " + version);
+ version = "0";
+ }
+ descriptor.udaVersion.minor = Integer.valueOf(version);
+ }
+
+ }
+
+ }
+
+ public void hydrateDevice(MutableDevice descriptor, Node deviceNode) throws DescriptorBindingException {
+
+ NodeList deviceNodeChildren = deviceNode.getChildNodes();
+ for (int i = 0; i < deviceNodeChildren.getLength(); i++) {
+ Node deviceNodeChild = deviceNodeChildren.item(i);
+
+ if (deviceNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.deviceType.equals(deviceNodeChild)) {
+ descriptor.deviceType = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.friendlyName.equals(deviceNodeChild)) {
+ descriptor.friendlyName = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.manufacturer.equals(deviceNodeChild)) {
+ descriptor.manufacturer = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.manufacturerURL.equals(deviceNodeChild)) {
+ descriptor.manufacturerURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
+ } else if (ELEMENT.modelDescription.equals(deviceNodeChild)) {
+ descriptor.modelDescription = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.modelName.equals(deviceNodeChild)) {
+ descriptor.modelName = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.modelNumber.equals(deviceNodeChild)) {
+ descriptor.modelNumber = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.modelURL.equals(deviceNodeChild)) {
+ descriptor.modelURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
+ } else if (ELEMENT.presentationURL.equals(deviceNodeChild)) {
+ descriptor.presentationURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
+ } else if (ELEMENT.UPC.equals(deviceNodeChild)) {
+ descriptor.upc = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.serialNumber.equals(deviceNodeChild)) {
+ descriptor.serialNumber = XMLUtil.getTextContent(deviceNodeChild);
+ } else if (ELEMENT.UDN.equals(deviceNodeChild)) {
+ descriptor.udn = UDN.valueOf(XMLUtil.getTextContent(deviceNodeChild));
+ } else if (ELEMENT.iconList.equals(deviceNodeChild)) {
+ hydrateIconList(descriptor, deviceNodeChild);
+ } else if (ELEMENT.serviceList.equals(deviceNodeChild)) {
+ hydrateServiceList(descriptor, deviceNodeChild);
+ } else if (ELEMENT.deviceList.equals(deviceNodeChild)) {
+ hydrateDeviceList(descriptor, deviceNodeChild);
+ } else if (ELEMENT.X_DLNADOC.equals(deviceNodeChild) &&
+ Descriptor.Device.DLNA_PREFIX.equals(deviceNodeChild.getPrefix())) {
+ String txt = XMLUtil.getTextContent(deviceNodeChild);
+ try {
+ descriptor.dlnaDocs.add(DLNADoc.valueOf(txt));
+ } catch (InvalidValueException ex) {
+ log.info("Invalid X_DLNADOC value, ignoring value: " + txt);
+ }
+ } else if (ELEMENT.X_DLNACAP.equals(deviceNodeChild) &&
+ Descriptor.Device.DLNA_PREFIX.equals(deviceNodeChild.getPrefix())) {
+ descriptor.dlnaCaps = DLNACaps.valueOf(XMLUtil.getTextContent(deviceNodeChild));
+ }
+ }
+ }
+
+ public void hydrateIconList(MutableDevice descriptor, Node iconListNode) throws DescriptorBindingException {
+
+ NodeList iconListNodeChildren = iconListNode.getChildNodes();
+ for (int i = 0; i < iconListNodeChildren.getLength(); i++) {
+ Node iconListNodeChild = iconListNodeChildren.item(i);
+
+ if (iconListNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.icon.equals(iconListNodeChild)) {
+
+ MutableIcon icon = new MutableIcon();
+
+ NodeList iconChildren = iconListNodeChild.getChildNodes();
+
+ for (int x = 0; x < iconChildren.getLength(); x++) {
+ Node iconChild = iconChildren.item(x);
+
+ if (iconChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.width.equals(iconChild)) {
+ icon.width = (Integer.valueOf(XMLUtil.getTextContent(iconChild)));
+ } else if (ELEMENT.height.equals(iconChild)) {
+ icon.height = (Integer.valueOf(XMLUtil.getTextContent(iconChild)));
+ } else if (ELEMENT.depth.equals(iconChild)) {
+ String depth = XMLUtil.getTextContent(iconChild);
+ try {
+ icon.depth = (Integer.valueOf(depth));
+ } catch(NumberFormatException ex) {
+ log.warning("Invalid icon depth '" + depth + "', using 16 as default: " + ex);
+ icon.depth = 16;
+ }
+ } else if (ELEMENT.url.equals(iconChild)) {
+ icon.uri = parseURI(XMLUtil.getTextContent(iconChild));
+ } else if (ELEMENT.mimetype.equals(iconChild)) {
+ try {
+ icon.mimeType = XMLUtil.getTextContent(iconChild);
+ MimeType.valueOf(icon.mimeType);
+ } catch(IllegalArgumentException ex) {
+ log.warning("Ignoring invalid icon mime type: " + icon.mimeType);
+ icon.mimeType = "";
+ }
+ }
+
+ }
+
+ descriptor.icons.add(icon);
+ }
+ }
+ }
+
+ public void hydrateServiceList(MutableDevice descriptor, Node serviceListNode) throws DescriptorBindingException {
+
+ NodeList serviceListNodeChildren = serviceListNode.getChildNodes();
+ for (int i = 0; i < serviceListNodeChildren.getLength(); i++) {
+ Node serviceListNodeChild = serviceListNodeChildren.item(i);
+
+ if (serviceListNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.service.equals(serviceListNodeChild)) {
+
+ NodeList serviceChildren = serviceListNodeChild.getChildNodes();
+
+ try {
+ MutableService service = new MutableService();
+
+ for (int x = 0; x < serviceChildren.getLength(); x++) {
+ Node serviceChild = serviceChildren.item(x);
+
+ if (serviceChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.serviceType.equals(serviceChild)) {
+ service.serviceType = (ServiceType.valueOf(XMLUtil.getTextContent(serviceChild)));
+ } else if (ELEMENT.serviceId.equals(serviceChild)) {
+ service.serviceId = (ServiceId.valueOf(XMLUtil.getTextContent(serviceChild)));
+ } else if (ELEMENT.SCPDURL.equals(serviceChild)) {
+ service.descriptorURI = parseURI(XMLUtil.getTextContent(serviceChild));
+ } else if (ELEMENT.controlURL.equals(serviceChild)) {
+ service.controlURI = parseURI(XMLUtil.getTextContent(serviceChild));
+ } else if (ELEMENT.eventSubURL.equals(serviceChild)) {
+ service.eventSubscriptionURI = parseURI(XMLUtil.getTextContent(serviceChild));
+ }
+
+ }
+
+ descriptor.services.add(service);
+ } catch (InvalidValueException ex) {
+ log.warning(
+ "UPnP specification violation, skipping invalid service declaration. " + ex.getMessage()
+ );
+ }
+ }
+ }
+ }
+
+ public void hydrateDeviceList(MutableDevice descriptor, Node deviceListNode) throws DescriptorBindingException {
+
+ NodeList deviceListNodeChildren = deviceListNode.getChildNodes();
+ for (int i = 0; i < deviceListNodeChildren.getLength(); i++) {
+ Node deviceListNodeChild = deviceListNodeChildren.item(i);
+
+ if (deviceListNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.device.equals(deviceListNodeChild)) {
+ MutableDevice embeddedDevice = new MutableDevice();
+ embeddedDevice.parentDevice = descriptor;
+ descriptor.embeddedDevices.add(embeddedDevice);
+ hydrateDevice(embeddedDevice, deviceListNodeChild);
+ }
+ }
+
+ }
+
+ public String generate(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException {
+ try {
+ log.fine("Generating XML descriptor from device model: " + deviceModel);
+
+ return XMLUtil.documentToString(buildDOM(deviceModel, info, namespace));
+
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not build DOM: " + ex.getMessage(), ex);
+ }
+ }
+
+ public Document buildDOM(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException {
+
+ try {
+ log.fine("Generating DOM from device model: " + deviceModel);
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ Document d = factory.newDocumentBuilder().newDocument();
+ generateRoot(namespace, deviceModel, d, info);
+
+ return d;
+
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not generate device descriptor: " + ex.getMessage(), ex);
+ }
+ }
+
+ protected void generateRoot(Namespace namespace, Device deviceModel, Document descriptor, RemoteClientInfo info) {
+
+ Element rootElement = descriptor.createElementNS(Descriptor.Device.NAMESPACE_URI, ELEMENT.root.toString());
+ descriptor.appendChild(rootElement);
+
+ generateSpecVersion(namespace, deviceModel, descriptor, rootElement);
+
+ /* UDA 1.1 spec says: Don't use URLBase anymore
+ if (deviceModel.getBaseURL() != null) {
+ appendChildElementWithTextContent(descriptor, rootElement, "URLBase", deviceModel.getBaseURL());
+ }
+ */
+
+ generateDevice(namespace, deviceModel, descriptor, rootElement, info);
+ }
+
+ protected void generateSpecVersion(Namespace namespace, Device deviceModel, Document descriptor, Element rootElement) {
+ Element specVersionElement = appendNewElement(descriptor, rootElement, ELEMENT.specVersion);
+ appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.major, deviceModel.getVersion().getMajor());
+ appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.minor, deviceModel.getVersion().getMinor());
+ }
+
+ protected void generateDevice(Namespace namespace, Device deviceModel, Document descriptor, Element rootElement, RemoteClientInfo info) {
+
+ Element deviceElement = appendNewElement(descriptor, rootElement, ELEMENT.device);
+
+ appendNewElementIfNotNull(descriptor, deviceElement, ELEMENT.deviceType, deviceModel.getType());
+
+ DeviceDetails deviceModelDetails = deviceModel.getDetails(info);
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.friendlyName,
+ deviceModelDetails.getFriendlyName()
+ );
+ if (deviceModelDetails.getManufacturerDetails() != null) {
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.manufacturer,
+ deviceModelDetails.getManufacturerDetails().getManufacturer()
+ );
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.manufacturerURL,
+ deviceModelDetails.getManufacturerDetails().getManufacturerURI()
+ );
+ }
+ if (deviceModelDetails.getModelDetails() != null) {
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.modelDescription,
+ deviceModelDetails.getModelDetails().getModelDescription()
+ );
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.modelName,
+ deviceModelDetails.getModelDetails().getModelName()
+ );
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.modelNumber,
+ deviceModelDetails.getModelDetails().getModelNumber()
+ );
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.modelURL,
+ deviceModelDetails.getModelDetails().getModelURI()
+ );
+ }
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.serialNumber,
+ deviceModelDetails.getSerialNumber()
+ );
+ appendNewElementIfNotNull(descriptor, deviceElement, ELEMENT.UDN, deviceModel.getIdentity().getUdn());
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.presentationURL,
+ deviceModelDetails.getPresentationURI()
+ );
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, ELEMENT.UPC,
+ deviceModelDetails.getUpc()
+ );
+
+ if (deviceModelDetails.getDlnaDocs() != null) {
+ for (DLNADoc dlnaDoc : deviceModelDetails.getDlnaDocs()) {
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, Descriptor.Device.DLNA_PREFIX + ":" + ELEMENT.X_DLNADOC,
+ dlnaDoc, Descriptor.Device.DLNA_NAMESPACE_URI
+ );
+ }
+ }
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, Descriptor.Device.DLNA_PREFIX + ":" + ELEMENT.X_DLNACAP,
+ deviceModelDetails.getDlnaCaps(), Descriptor.Device.DLNA_NAMESPACE_URI
+ );
+
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, Descriptor.Device.SEC_PREFIX + ":" + ELEMENT.ProductCap,
+ deviceModelDetails.getSecProductCaps(), Descriptor.Device.SEC_NAMESPACE_URI
+ );
+
+ appendNewElementIfNotNull(
+ descriptor, deviceElement, Descriptor.Device.SEC_PREFIX + ":" + ELEMENT.X_ProductCap,
+ deviceModelDetails.getSecProductCaps(), Descriptor.Device.SEC_NAMESPACE_URI
+ );
+
+ generateIconList(namespace, deviceModel, descriptor, deviceElement);
+ generateServiceList(namespace, deviceModel, descriptor, deviceElement);
+ generateDeviceList(namespace, deviceModel, descriptor, deviceElement, info);
+ }
+
+ protected void generateIconList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement) {
+ if (!deviceModel.hasIcons()) return;
+
+ Element iconListElement = appendNewElement(descriptor, deviceElement, ELEMENT.iconList);
+
+ for (Icon icon : deviceModel.getIcons()) {
+ Element iconElement = appendNewElement(descriptor, iconListElement, ELEMENT.icon);
+
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.mimetype, icon.getMimeType());
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.width, icon.getWidth());
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.height, icon.getHeight());
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.depth, icon.getDepth());
+ if (deviceModel instanceof RemoteDevice) {
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.url, icon.getUri());
+ } else if (deviceModel instanceof LocalDevice) {
+ appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.url, namespace.getIconPath(icon));
+ }
+ }
+ }
+
+ protected void generateServiceList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement) {
+ if (!deviceModel.hasServices()) return;
+
+ Element serviceListElement = appendNewElement(descriptor, deviceElement, ELEMENT.serviceList);
+
+ for (Service service : deviceModel.getServices()) {
+ Element serviceElement = appendNewElement(descriptor, serviceListElement, ELEMENT.service);
+
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.serviceType, service.getServiceType());
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.serviceId, service.getServiceId());
+ if (service instanceof RemoteService) {
+ RemoteService rs = (RemoteService) service;
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.SCPDURL, rs.getDescriptorURI());
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.controlURL, rs.getControlURI());
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.eventSubURL, rs.getEventSubscriptionURI());
+ } else if (service instanceof LocalService) {
+ LocalService ls = (LocalService) service;
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.SCPDURL, namespace.getDescriptorPath(ls));
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.controlURL, namespace.getControlPath(ls));
+ appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.eventSubURL, namespace.getEventSubscriptionPath(ls));
+ }
+ }
+ }
+
+ protected void generateDeviceList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement, RemoteClientInfo info) {
+ if (!deviceModel.hasEmbeddedDevices()) return;
+
+ Element deviceListElement = appendNewElement(descriptor, deviceElement, ELEMENT.deviceList);
+
+ for (Device device : deviceModel.getEmbeddedDevices()) {
+ generateDevice(namespace, device, descriptor, deviceListElement, info);
+ }
+ }
+
+ public void warning(SAXParseException e) throws SAXException {
+ log.warning(e.toString());
+ }
+
+ public void error(SAXParseException e) throws SAXException {
+ throw e;
+ }
+
+ public void fatalError(SAXParseException e) throws SAXException {
+ throw e;
+ }
+
+ static protected URI parseURI(String uri) {
+
+ // TODO: UPNP VIOLATION: Netgear DG834 uses a non-URI: 'www.netgear.com'
+ if (uri.startsWith("www.")) {
+ uri = "http://" + uri;
+ }
+
+ // TODO: UPNP VIOLATION: Plutinosoft uses unencoded relative URIs
+ // /var/mobile/Applications/71367E68-F30F-460B-A2D2-331509441D13/Windows Media Player Streamer.app/Icon-ps3.jpg
+ if (uri.contains(" ")) {
+ // We don't want to split/encode individual parts of the URI, too much work
+ // TODO: But we probably should do this? Because browsers do it, everyone
+ // seems to think that spaces in URLs are somehow OK...
+ uri = uri.replaceAll(" ", "%20");
+ }
+
+ try {
+ return URI.create(uri);
+ } catch (Throwable ex) {
+ /*
+ catch Throwable because on Android 2.2, parsing some invalid URI like "http://..." gives:
+ java.lang.NullPointerException
+ at java.net.URI$Helper.isValidDomainName(URI.java:631)
+ at java.net.URI$Helper.isValidHost(URI.java:595)
+ at java.net.URI$Helper.parseAuthority(URI.java:544)
+ at java.net.URI$Helper.parseURI(URI.java:404)
+ at java.net.URI$Helper.access$100(URI.java:302)
+ at java.net.URI.(URI.java:87)
+ at java.net.URI.create(URI.java:968)
+ */
+ log.fine("Illegal URI, trying with ./ prefix: " + Exceptions.unwrap(ex));
+ // Ignore
+ }
+ try {
+ // The java.net.URI class can't deal with "_urn:foobar" (yeah, great idea Intel UPnP tools guy), as
+ // explained in RFC 3986:
+ //
+ // A path segment that contains a colon character (e.g., "this:that") cannot be used as the first segment
+ // of a relative-path reference, as it would be mistaken for a scheme name. Such a segment must
+ // be preceded by a dot-segment (e.g., "./this:that") to make a relative-path reference.
+ //
+ return URI.create("./" + uri);
+ } catch (IllegalArgumentException ex) {
+ log.warning("Illegal URI '" + uri + "', ignoring value: " + Exceptions.unwrap(ex));
+ // Ignore
+ }
+ return null;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..76824ccc6e8ae48470712f72b2d21637dea36128
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.binding.staging.MutableDevice;
+import org.fourthline.cling.binding.staging.MutableIcon;
+import org.fourthline.cling.binding.staging.MutableService;
+import org.fourthline.cling.binding.staging.MutableUDAVersion;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.XMLUtil;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.types.DLNACaps;
+import org.fourthline.cling.model.types.DLNADoc;
+import org.fourthline.cling.model.types.InvalidValueException;
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.model.types.UDN;
+import org.seamless.util.MimeType;
+import org.seamless.xml.SAXParser;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.StringReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.fourthline.cling.binding.xml.Descriptor.Device.ELEMENT;
+
+/**
+ * A JAXP SAX parser implementation, which is actually slower than the DOM implementation (on desktop and on Android)!
+ *
+ * @author Christian Bauer
+ */
+public class UDA10DeviceDescriptorBinderSAXImpl extends UDA10DeviceDescriptorBinderImpl {
+
+ private static Logger log = Logger.getLogger(DeviceDescriptorBinder.class.getName());
+
+ @Override
+ public D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {
+
+ if (descriptorXml == null || descriptorXml.length() == 0) {
+ throw new DescriptorBindingException("Null or empty descriptor");
+ }
+
+ try {
+ log.fine("Populating device from XML descriptor: " + undescribedDevice);
+
+ // Read the XML into a mutable descriptor graph
+
+ SAXParser parser = new SAXParser();
+
+ MutableDevice descriptor = new MutableDevice();
+ new RootHandler(descriptor, parser);
+
+ parser.parse(
+ new InputSource(
+ // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
+ new StringReader(descriptorXml.trim())
+ )
+ );
+
+ // Build the immutable descriptor graph
+ return (D) descriptor.build(undescribedDevice);
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse device descriptor: " + ex.toString(), ex);
+ }
+ }
+
+ protected static class RootHandler extends DeviceDescriptorHandler {
+
+ public RootHandler(MutableDevice instance, SAXParser parser) {
+ super(instance, parser);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+
+ if (element.equals(SpecVersionHandler.EL)) {
+ MutableUDAVersion udaVersion = new MutableUDAVersion();
+ getInstance().udaVersion = udaVersion;
+ new SpecVersionHandler(udaVersion, this);
+ }
+
+ if (element.equals(DeviceHandler.EL)) {
+ new DeviceHandler(getInstance(), this);
+ }
+
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case URLBase:
+ try {
+ String urlString = getCharacters();
+ if (urlString != null && urlString.length() > 0) {
+ // We hope it's RFC 2396 and RFC 2732 compliant
+ getInstance().baseURL = new URL(urlString);
+ }
+ } catch (Exception ex) {
+ throw new SAXException("Invalid URLBase: " + ex.toString());
+ }
+ break;
+ }
+ }
+ }
+
+ protected static class SpecVersionHandler extends DeviceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.specVersion;
+
+ public SpecVersionHandler(MutableUDAVersion instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case major:
+ String majorVersion = getCharacters().trim();
+ if (!majorVersion.equals("1")) {
+ log.warning("Unsupported UDA major version, ignoring: " + majorVersion);
+ majorVersion = "1";
+ }
+ getInstance().major = Integer.valueOf(majorVersion);
+ break;
+ case minor:
+ String minorVersion = getCharacters().trim();
+ if (!minorVersion.equals("0")) {
+ log.warning("Unsupported UDA minor version, ignoring: " + minorVersion);
+ minorVersion = "0";
+ }
+ getInstance().minor = Integer.valueOf(minorVersion);
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class DeviceHandler extends DeviceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.device;
+
+ public DeviceHandler(MutableDevice instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+
+ if (element.equals(IconListHandler.EL)) {
+ List icons = new ArrayList<>();
+ getInstance().icons = icons;
+ new IconListHandler(icons, this);
+ }
+
+ if (element.equals(ServiceListHandler.EL)) {
+ List services = new ArrayList<>();
+ getInstance().services = services;
+ new ServiceListHandler(services, this);
+ }
+
+ if (element.equals(DeviceListHandler.EL)) {
+ List devices = new ArrayList<>();
+ getInstance().embeddedDevices = devices;
+ new DeviceListHandler(devices, this);
+ }
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case deviceType:
+ getInstance().deviceType = getCharacters();
+ break;
+ case friendlyName:
+ getInstance().friendlyName = getCharacters();
+ break;
+ case manufacturer:
+ getInstance().manufacturer = getCharacters();
+ break;
+ case manufacturerURL:
+ getInstance().manufacturerURI = parseURI(getCharacters());
+ break;
+ case modelDescription:
+ getInstance().modelDescription = getCharacters();
+ break;
+ case modelName:
+ getInstance().modelName = getCharacters();
+ break;
+ case modelNumber:
+ getInstance().modelNumber = getCharacters();
+ break;
+ case modelURL:
+ getInstance().modelURI = parseURI(getCharacters());
+ break;
+ case presentationURL:
+ getInstance().presentationURI = parseURI(getCharacters());
+ break;
+ case UPC:
+ getInstance().upc = getCharacters();
+ break;
+ case serialNumber:
+ getInstance().serialNumber = getCharacters();
+ break;
+ case UDN:
+ getInstance().udn = UDN.valueOf(getCharacters());
+ break;
+ case X_DLNADOC:
+ String txt = getCharacters();
+ try {
+ getInstance().dlnaDocs.add(DLNADoc.valueOf(txt));
+ } catch (InvalidValueException ex) {
+ log.info("Invalid X_DLNADOC value, ignoring value: " + txt);
+ }
+ break;
+ case X_DLNACAP:
+ getInstance().dlnaCaps = DLNACaps.valueOf(getCharacters());
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class IconListHandler extends DeviceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.iconList;
+
+ public IconListHandler(List instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(IconHandler.EL)) {
+ MutableIcon icon = new MutableIcon();
+ getInstance().add(icon);
+ new IconHandler(icon, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class IconHandler extends DeviceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.icon;
+
+ public IconHandler(MutableIcon instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case width:
+ getInstance().width = Integer.valueOf(getCharacters());
+ break;
+ case height:
+ getInstance().height = Integer.valueOf(getCharacters());
+ break;
+ case depth:
+ try {
+ getInstance().depth = Integer.valueOf(getCharacters());
+ } catch(NumberFormatException ex) {
+ log.warning("Invalid icon depth '" + getCharacters() + "', using 16 as default: " + ex);
+ getInstance().depth = 16;
+ }
+ break;
+ case url:
+ getInstance().uri = parseURI(getCharacters());
+ break;
+ case mimetype:
+ try {
+ getInstance().mimeType = getCharacters();
+ MimeType.valueOf(getInstance().mimeType);
+ } catch(IllegalArgumentException ex) {
+ log.warning("Ignoring invalid icon mime type: " + getInstance().mimeType);
+ getInstance().mimeType = "";
+ }
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class ServiceListHandler extends DeviceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.serviceList;
+
+ public ServiceListHandler(List instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(ServiceHandler.EL)) {
+ MutableService service = new MutableService();
+ getInstance().add(service);
+ new ServiceHandler(service, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ boolean last = element.equals(EL);
+ if (last) {
+ Iterator it = getInstance().iterator();
+ while (it.hasNext()) {
+ MutableService service = it.next();
+ if (service.serviceType == null || service.serviceId == null)
+ it.remove();
+ }
+ }
+ return last;
+ }
+ }
+
+ protected static class ServiceHandler extends DeviceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.service;
+
+ public ServiceHandler(MutableService instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ try {
+ switch (element) {
+ case serviceType:
+ getInstance().serviceType = ServiceType.valueOf(getCharacters());
+ break;
+ case serviceId:
+ getInstance().serviceId = ServiceId.valueOf(getCharacters());
+ break;
+ case SCPDURL:
+ getInstance().descriptorURI = parseURI(getCharacters());
+ break;
+ case controlURL:
+ getInstance().controlURI = parseURI(getCharacters());
+ break;
+ case eventSubURL:
+ getInstance().eventSubscriptionURI = parseURI(getCharacters());
+ break;
+ }
+ } catch (InvalidValueException ex) {
+ log.warning(
+ "UPnP specification violation, skipping invalid service declaration. " + ex.getMessage()
+ );
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class DeviceListHandler extends DeviceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.deviceList;
+
+ public DeviceListHandler(List instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(DeviceHandler.EL)) {
+ MutableDevice device = new MutableDevice();
+ getInstance().add(device);
+ new DeviceHandler(device, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class DeviceDescriptorHandler extends SAXParser.Handler {
+
+ public DeviceDescriptorHandler(I instance) {
+ super(instance);
+ }
+
+ public DeviceDescriptorHandler(I instance, SAXParser parser) {
+ super(instance, parser);
+ }
+
+ public DeviceDescriptorHandler(I instance, DeviceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ public DeviceDescriptorHandler(I instance, SAXParser parser, DeviceDescriptorHandler parent) {
+ super(instance, parser, parent);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ super.startElement(uri, localName, qName, attributes);
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ if (el == null) return;
+ startElement(el, attributes);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ super.endElement(uri, localName, qName);
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ if (el == null) return;
+ endElement(el);
+ }
+
+ @Override
+ protected boolean isLastElement(String uri, String localName, String qName) {
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ return el != null && isLastElement(el);
+ }
+
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+
+ }
+
+ public void endElement(ELEMENT element) throws SAXException {
+
+ }
+
+ public boolean isLastElement(ELEMENT element) {
+ return false;
+ }
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1d349887599641bc955599ec6bd7587839e9f51
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.binding.staging.MutableAction;
+import org.fourthline.cling.binding.staging.MutableActionArgument;
+import org.fourthline.cling.binding.staging.MutableAllowedValueRange;
+import org.fourthline.cling.binding.staging.MutableService;
+import org.fourthline.cling.binding.staging.MutableStateVariable;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.XMLUtil;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.QueryStateVariableAction;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableEventDetails;
+import org.fourthline.cling.model.types.CustomDatatype;
+import org.fourthline.cling.model.types.Datatype;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Logger;
+
+import static org.fourthline.cling.binding.xml.Descriptor.Service.ATTRIBUTE;
+import static org.fourthline.cling.binding.xml.Descriptor.Service.ELEMENT;
+import static org.fourthline.cling.model.XMLUtil.appendNewElement;
+import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull;
+
+/**
+ * Implementation based on JAXP DOM.
+ *
+ * @author Christian Bauer
+ */
+public class UDA10ServiceDescriptorBinderImpl implements ServiceDescriptorBinder, ErrorHandler {
+
+ private static Logger log = Logger.getLogger(ServiceDescriptorBinder.class.getName());
+
+ public S describe(S undescribedService, String descriptorXml) throws DescriptorBindingException, ValidationException {
+ if (descriptorXml == null || descriptorXml.length() == 0) {
+ throw new DescriptorBindingException("Null or empty descriptor");
+ }
+
+ try {
+ log.fine("Populating service from XML descriptor: " + undescribedService);
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder documentBuilder = factory.newDocumentBuilder();
+ documentBuilder.setErrorHandler(this);
+
+ Document d = documentBuilder.parse(
+ new InputSource(
+ // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
+ new StringReader(descriptorXml.trim())
+ )
+ );
+
+ return describe(undescribedService, d);
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse service descriptor: " + ex.toString(), ex);
+ }
+ }
+
+ public S describe(S undescribedService, Document dom) throws DescriptorBindingException, ValidationException {
+ try {
+ log.fine("Populating service from DOM: " + undescribedService);
+
+ // Read the XML into a mutable descriptor graph
+ MutableService descriptor = new MutableService();
+
+ hydrateBasic(descriptor, undescribedService);
+
+ Element rootElement = dom.getDocumentElement();
+ hydrateRoot(descriptor, rootElement);
+
+ // Build the immutable descriptor graph
+ return buildInstance(undescribedService, descriptor);
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse service DOM: " + ex.toString(), ex);
+ }
+ }
+
+ protected S buildInstance(S undescribedService, MutableService descriptor) throws ValidationException {
+ return (S)descriptor.build(undescribedService.getDevice());
+ }
+
+ protected void hydrateBasic(MutableService descriptor, Service undescribedService) {
+ descriptor.serviceId = undescribedService.getServiceId();
+ descriptor.serviceType = undescribedService.getServiceType();
+ if (undescribedService instanceof RemoteService) {
+ RemoteService rs = (RemoteService) undescribedService;
+ descriptor.controlURI = rs.getControlURI();
+ descriptor.eventSubscriptionURI = rs.getEventSubscriptionURI();
+ descriptor.descriptorURI = rs.getDescriptorURI();
+ }
+ }
+
+ protected void hydrateRoot(MutableService descriptor, Element rootElement)
+ throws DescriptorBindingException {
+
+ // We don't check the XMLNS, nobody bothers anyway...
+
+ if (!ELEMENT.scpd.equals(rootElement)) {
+ throw new DescriptorBindingException("Root element name is not : " + rootElement.getNodeName());
+ }
+
+ NodeList rootChildren = rootElement.getChildNodes();
+
+ for (int i = 0; i < rootChildren.getLength(); i++) {
+ Node rootChild = rootChildren.item(i);
+
+ if (rootChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.specVersion.equals(rootChild)) {
+ // We don't care about UDA major/minor specVersion anymore - whoever had the brilliant idea that
+ // the spec versions can be declared on devices _AND_ on their services should have their fingers
+ // broken so they never touch a keyboard again.
+ // hydrateSpecVersion(descriptor, rootChild);
+ } else if (ELEMENT.actionList.equals(rootChild)) {
+ hydrateActionList(descriptor, rootChild);
+ } else if (ELEMENT.serviceStateTable.equals(rootChild)) {
+ hydrateServiceStateTableList(descriptor, rootChild);
+ } else {
+ log.finer("Ignoring unknown element: " + rootChild.getNodeName());
+ }
+ }
+
+ }
+
+ /*
+ public void hydrateSpecVersion(MutableService descriptor, Node specVersionNode)
+ throws DescriptorBindingException {
+
+ NodeList specVersionChildren = specVersionNode.getChildNodes();
+ for (int i = 0; i < specVersionChildren.getLength(); i++) {
+ Node specVersionChild = specVersionChildren.item(i);
+
+ if (specVersionChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ MutableUDAVersion version = new MutableUDAVersion();
+ if (ELEMENT.major.equals(specVersionChild)) {
+ version.major = Integer.valueOf(XMLUtil.getTextContent(specVersionChild));
+ } else if (ELEMENT.minor.equals(specVersionChild)) {
+ version.minor = Integer.valueOf(XMLUtil.getTextContent(specVersionChild));
+ }
+ }
+ }
+ */
+
+ public void hydrateActionList(MutableService descriptor, Node actionListNode) throws DescriptorBindingException {
+
+ NodeList actionListChildren = actionListNode.getChildNodes();
+ for (int i = 0; i < actionListChildren.getLength(); i++) {
+ Node actionListChild = actionListChildren.item(i);
+
+ if (actionListChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.action.equals(actionListChild)) {
+ MutableAction action = new MutableAction();
+ hydrateAction(action, actionListChild);
+ descriptor.actions.add(action);
+ }
+ }
+ }
+
+ public void hydrateAction(MutableAction action, Node actionNode) {
+
+ NodeList actionNodeChildren = actionNode.getChildNodes();
+ for (int i = 0; i < actionNodeChildren.getLength(); i++) {
+ Node actionNodeChild = actionNodeChildren.item(i);
+
+ if (actionNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.name.equals(actionNodeChild)) {
+ action.name = XMLUtil.getTextContent(actionNodeChild);
+ } else if (ELEMENT.argumentList.equals(actionNodeChild)) {
+
+
+ NodeList argumentChildren = actionNodeChild.getChildNodes();
+ for (int j = 0; j < argumentChildren.getLength(); j++) {
+ Node argumentChild = argumentChildren.item(j);
+
+ if (argumentChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ MutableActionArgument actionArgument = new MutableActionArgument();
+ hydrateActionArgument(actionArgument, argumentChild);
+ action.arguments.add(actionArgument);
+ }
+ }
+ }
+
+ }
+
+ public void hydrateActionArgument(MutableActionArgument actionArgument, Node actionArgumentNode) {
+
+ NodeList argumentNodeChildren = actionArgumentNode.getChildNodes();
+ for (int i = 0; i < argumentNodeChildren.getLength(); i++) {
+ Node argumentNodeChild = argumentNodeChildren.item(i);
+
+ if (argumentNodeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.name.equals(argumentNodeChild)) {
+ actionArgument.name = XMLUtil.getTextContent(argumentNodeChild);
+ } else if (ELEMENT.direction.equals(argumentNodeChild)) {
+ String directionString = XMLUtil.getTextContent(argumentNodeChild);
+ try {
+ actionArgument.direction = ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException ex) {
+ // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT
+ log.warning("UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString);
+ actionArgument.direction = ActionArgument.Direction.IN;
+ }
+ } else if (ELEMENT.relatedStateVariable.equals(argumentNodeChild)) {
+ actionArgument.relatedStateVariable = XMLUtil.getTextContent(argumentNodeChild);
+ } else if (ELEMENT.retval.equals(argumentNodeChild)) {
+ actionArgument.retval = true;
+ }
+ }
+ }
+
+ public void hydrateServiceStateTableList(MutableService descriptor, Node serviceStateTableNode) {
+
+ NodeList serviceStateTableChildren = serviceStateTableNode.getChildNodes();
+ for (int i = 0; i < serviceStateTableChildren.getLength(); i++) {
+ Node serviceStateTableChild = serviceStateTableChildren.item(i);
+
+ if (serviceStateTableChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.stateVariable.equals(serviceStateTableChild)) {
+ MutableStateVariable stateVariable = new MutableStateVariable();
+ hydrateStateVariable(stateVariable, (Element) serviceStateTableChild);
+ descriptor.stateVariables.add(stateVariable);
+ }
+ }
+ }
+
+ public void hydrateStateVariable(MutableStateVariable stateVariable, Element stateVariableElement) {
+
+ stateVariable.eventDetails = new StateVariableEventDetails(
+ stateVariableElement.getAttribute("sendEvents") != null &&
+ stateVariableElement.getAttribute(ATTRIBUTE.sendEvents.toString()).toUpperCase(Locale.ROOT).equals("YES")
+ );
+
+ NodeList stateVariableChildren = stateVariableElement.getChildNodes();
+ for (int i = 0; i < stateVariableChildren.getLength(); i++) {
+ Node stateVariableChild = stateVariableChildren.item(i);
+
+ if (stateVariableChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.name.equals(stateVariableChild)) {
+ stateVariable.name = XMLUtil.getTextContent(stateVariableChild);
+ } else if (ELEMENT.dataType.equals(stateVariableChild)) {
+ String dtName = XMLUtil.getTextContent(stateVariableChild);
+ Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(dtName);
+ stateVariable.dataType = builtin != null ? builtin.getDatatype() : new CustomDatatype(dtName);
+ } else if (ELEMENT.defaultValue.equals(stateVariableChild)) {
+ stateVariable.defaultValue = XMLUtil.getTextContent(stateVariableChild);
+ } else if (ELEMENT.allowedValueList.equals(stateVariableChild)) {
+
+ List allowedValues = new ArrayList<>();
+
+ NodeList allowedValueListChildren = stateVariableChild.getChildNodes();
+ for (int j = 0; j < allowedValueListChildren.getLength(); j++) {
+ Node allowedValueListChild = allowedValueListChildren.item(j);
+
+ if (allowedValueListChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.allowedValue.equals(allowedValueListChild))
+ allowedValues.add(XMLUtil.getTextContent(allowedValueListChild));
+ }
+
+ stateVariable.allowedValues = allowedValues;
+
+ } else if (ELEMENT.allowedValueRange.equals(stateVariableChild)) {
+
+ MutableAllowedValueRange range = new MutableAllowedValueRange();
+
+ NodeList allowedValueRangeChildren = stateVariableChild.getChildNodes();
+ for (int j = 0; j < allowedValueRangeChildren.getLength(); j++) {
+ Node allowedValueRangeChild = allowedValueRangeChildren.item(j);
+
+ if (allowedValueRangeChild.getNodeType() != Node.ELEMENT_NODE)
+ continue;
+
+ if (ELEMENT.minimum.equals(allowedValueRangeChild)) {
+ try {
+ range.minimum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
+ } catch (Exception ex) {
+ }
+ } else if (ELEMENT.maximum.equals(allowedValueRangeChild)) {
+ try {
+ range.maximum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
+ } catch (Exception ex) {
+ }
+ } else if (ELEMENT.step.equals(allowedValueRangeChild)) {
+ try {
+ range.step = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
+ } catch (Exception ex) {
+ }
+ }
+ }
+
+ stateVariable.allowedValueRange = range;
+ }
+ }
+ }
+
+ public String generate(Service service) throws DescriptorBindingException {
+ try {
+ log.fine("Generating XML descriptor from service model: " + service);
+
+ return XMLUtil.documentToString(buildDOM(service));
+
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not build DOM: " + ex.getMessage(), ex);
+ }
+ }
+
+ public Document buildDOM(Service service) throws DescriptorBindingException {
+
+ try {
+ log.fine("Generating XML descriptor from service model: " + service);
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ Document d = factory.newDocumentBuilder().newDocument();
+ generateScpd(service, d);
+
+ return d;
+
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not generate service descriptor: " + ex.getMessage(), ex);
+ }
+ }
+
+ private void generateScpd(Service serviceModel, Document descriptor) {
+
+ Element scpdElement = descriptor.createElementNS(Descriptor.Service.NAMESPACE_URI, ELEMENT.scpd.toString());
+ descriptor.appendChild(scpdElement);
+
+ generateSpecVersion(serviceModel, descriptor, scpdElement);
+ if (serviceModel.hasActions()) {
+ generateActionList(serviceModel, descriptor, scpdElement);
+ }
+ generateServiceStateTable(serviceModel, descriptor, scpdElement);
+ }
+
+ private void generateSpecVersion(Service serviceModel, Document descriptor, Element rootElement) {
+ Element specVersionElement = appendNewElement(descriptor, rootElement, ELEMENT.specVersion);
+ appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.major, serviceModel.getDevice().getVersion().getMajor());
+ appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.minor, serviceModel.getDevice().getVersion().getMinor());
+ }
+
+ private void generateActionList(Service serviceModel, Document descriptor, Element scpdElement) {
+
+ Element actionListElement = appendNewElement(descriptor, scpdElement, ELEMENT.actionList);
+
+ for (Action action : serviceModel.getActions()) {
+ if (!action.getName().equals(QueryStateVariableAction.ACTION_NAME))
+ generateAction(action, descriptor, actionListElement);
+ }
+ }
+
+ private void generateAction(Action action, Document descriptor, Element actionListElement) {
+
+ Element actionElement = appendNewElement(descriptor, actionListElement, ELEMENT.action);
+
+ appendNewElementIfNotNull(descriptor, actionElement, ELEMENT.name, action.getName());
+
+ if (action.hasArguments()) {
+ Element argumentListElement = appendNewElement(descriptor, actionElement, ELEMENT.argumentList);
+ for (ActionArgument actionArgument : action.getArguments()) {
+ generateActionArgument(actionArgument, descriptor, argumentListElement);
+ }
+ }
+ }
+
+ private void generateActionArgument(ActionArgument actionArgument, Document descriptor, Element actionElement) {
+
+ Element actionArgumentElement = appendNewElement(descriptor, actionElement, ELEMENT.argument);
+
+ appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.name, actionArgument.getName());
+ appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.direction, actionArgument.getDirection().toString().toLowerCase(Locale.ROOT));
+ if (actionArgument.isReturnValue()) {
+ // TODO: UPNP VIOLATION: WMP12 will discard RenderingControl service if it contains tags
+ log.warning("UPnP specification violation: Not producing element to be compatible with WMP12: " + actionArgument);
+ // appendNewElement(descriptor, actionArgumentElement, ELEMENT.retval);
+ }
+ appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.relatedStateVariable, actionArgument.getRelatedStateVariableName());
+ }
+
+ private void generateServiceStateTable(Service serviceModel, Document descriptor, Element scpdElement) {
+
+ Element serviceStateTableElement = appendNewElement(descriptor, scpdElement, ELEMENT.serviceStateTable);
+
+ for (StateVariable stateVariable : serviceModel.getStateVariables()) {
+ generateStateVariable(stateVariable, descriptor, serviceStateTableElement);
+ }
+ }
+
+ private void generateStateVariable(StateVariable stateVariable, Document descriptor, Element serviveStateTableElement) {
+
+ Element stateVariableElement = appendNewElement(descriptor, serviveStateTableElement, ELEMENT.stateVariable);
+
+ appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.name, stateVariable.getName());
+
+ if (stateVariable.getTypeDetails().getDatatype() instanceof CustomDatatype) {
+ appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.dataType,
+ ((CustomDatatype)stateVariable.getTypeDetails().getDatatype()).getName());
+ } else {
+ appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.dataType,
+ stateVariable.getTypeDetails().getDatatype().getBuiltin().getDescriptorName());
+ }
+
+ appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.defaultValue,
+ stateVariable.getTypeDetails().getDefaultValue());
+
+ // The default is 'yes' but we generate it anyway just to be sure
+ if (stateVariable.getEventDetails().isSendEvents()) {
+ stateVariableElement.setAttribute(ATTRIBUTE.sendEvents.toString(), "yes");
+ } else {
+ stateVariableElement.setAttribute(ATTRIBUTE.sendEvents.toString(), "no");
+ }
+
+ if (stateVariable.getTypeDetails().getAllowedValues() != null) {
+ Element allowedValueListElement = appendNewElement(descriptor, stateVariableElement, ELEMENT.allowedValueList);
+ for (String allowedValue : stateVariable.getTypeDetails().getAllowedValues()) {
+ appendNewElementIfNotNull(descriptor, allowedValueListElement, ELEMENT.allowedValue, allowedValue);
+ }
+ }
+
+ if (stateVariable.getTypeDetails().getAllowedValueRange() != null) {
+ Element allowedValueRangeElement = appendNewElement(descriptor, stateVariableElement, ELEMENT.allowedValueRange);
+ appendNewElementIfNotNull(
+ descriptor, allowedValueRangeElement, ELEMENT.minimum, stateVariable.getTypeDetails().getAllowedValueRange().getMinimum()
+ );
+ appendNewElementIfNotNull(
+ descriptor, allowedValueRangeElement, ELEMENT.maximum, stateVariable.getTypeDetails().getAllowedValueRange().getMaximum()
+ );
+ if (stateVariable.getTypeDetails().getAllowedValueRange().getStep() >= 1l) {
+ appendNewElementIfNotNull(
+ descriptor, allowedValueRangeElement, ELEMENT.step, stateVariable.getTypeDetails().getAllowedValueRange().getStep()
+ );
+ }
+ }
+
+ }
+
+ public void warning(SAXParseException e) throws SAXException {
+ log.warning(e.toString());
+ }
+
+ public void error(SAXParseException e) throws SAXException {
+ throw e;
+ }
+
+ public void fatalError(SAXParseException e) throws SAXException {
+ throw e;
+ }
+}
+
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..989c5755c36686668299d96ebab9560a3afc6d7a
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.binding.xml;
+
+import org.fourthline.cling.binding.staging.MutableAction;
+import org.fourthline.cling.binding.staging.MutableActionArgument;
+import org.fourthline.cling.binding.staging.MutableAllowedValueRange;
+import org.fourthline.cling.binding.staging.MutableService;
+import org.fourthline.cling.binding.staging.MutableStateVariable;
+import org.fourthline.cling.model.ValidationException;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.StateVariableEventDetails;
+import org.fourthline.cling.model.types.CustomDatatype;
+import org.fourthline.cling.model.types.Datatype;
+import org.seamless.xml.SAXParser;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Logger;
+
+import static org.fourthline.cling.binding.xml.Descriptor.Service.ATTRIBUTE;
+import static org.fourthline.cling.binding.xml.Descriptor.Service.ELEMENT;
+
+/**
+ * Implementation based on JAXP SAX.
+ *
+ * @author Christian Bauer
+ */
+public class UDA10ServiceDescriptorBinderSAXImpl extends UDA10ServiceDescriptorBinderImpl {
+
+ private static Logger log = Logger.getLogger(ServiceDescriptorBinder.class.getName());
+
+ @Override
+ public S describe(S undescribedService, String descriptorXml) throws DescriptorBindingException, ValidationException {
+
+ if (descriptorXml == null || descriptorXml.length() == 0) {
+ throw new DescriptorBindingException("Null or empty descriptor");
+ }
+
+ try {
+ log.fine("Reading service from XML descriptor");
+
+ SAXParser parser = new SAXParser();
+
+ MutableService descriptor = new MutableService();
+
+ hydrateBasic(descriptor, undescribedService);
+
+ new RootHandler(descriptor, parser);
+
+ parser.parse(
+ new InputSource(
+ // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
+ new StringReader(descriptorXml.trim())
+ )
+ );
+
+ // Build the immutable descriptor graph
+ return (S)descriptor.build(undescribedService.getDevice());
+
+ } catch (ValidationException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ throw new DescriptorBindingException("Could not parse service descriptor: " + ex.toString(), ex);
+ }
+ }
+
+ protected static class RootHandler extends ServiceDescriptorHandler {
+
+ public RootHandler(MutableService instance, SAXParser parser) {
+ super(instance, parser);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+
+ /*
+ if (element.equals(SpecVersionHandler.EL)) {
+ MutableUDAVersion udaVersion = new MutableUDAVersion();
+ getInstance().udaVersion = udaVersion;
+ new SpecVersionHandler(udaVersion, this);
+ }
+ */
+
+ if (element.equals(ActionListHandler.EL)) {
+ List actions = new ArrayList<>();
+ getInstance().actions = actions;
+ new ActionListHandler(actions, this);
+ }
+
+ if (element.equals(StateVariableListHandler.EL)) {
+ List stateVariables = new ArrayList<>();
+ getInstance().stateVariables = stateVariables;
+ new StateVariableListHandler(stateVariables, this);
+ }
+
+ }
+ }
+
+ /*
+ protected static class SpecVersionHandler extends ServiceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.specVersion;
+
+ public SpecVersionHandler(MutableUDAVersion instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case major:
+ getInstance().major = Integer.valueOf(getCharacters());
+ break;
+ case minor:
+ getInstance().minor = Integer.valueOf(getCharacters());
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+ */
+
+ protected static class ActionListHandler extends ServiceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.actionList;
+
+ public ActionListHandler(List instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(ActionHandler.EL)) {
+ MutableAction action = new MutableAction();
+ getInstance().add(action);
+ new ActionHandler(action, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class ActionHandler extends ServiceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.action;
+
+ public ActionHandler(MutableAction instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(ActionArgumentListHandler.EL)) {
+ List arguments = new ArrayList<>();
+ getInstance().arguments = arguments;
+ new ActionArgumentListHandler(arguments, this);
+ }
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case name:
+ getInstance().name = getCharacters();
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class ActionArgumentListHandler extends ServiceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.argumentList;
+
+ public ActionArgumentListHandler(List instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(ActionArgumentHandler.EL)) {
+ MutableActionArgument argument = new MutableActionArgument();
+ getInstance().add(argument);
+ new ActionArgumentHandler(argument, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class ActionArgumentHandler extends ServiceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.argument;
+
+ public ActionArgumentHandler(MutableActionArgument instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case name:
+ getInstance().name = getCharacters();
+ break;
+ case direction:
+ String directionString = getCharacters();
+ try {
+ getInstance().direction = ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException ex) {
+ // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT
+ log.warning("UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString);
+ getInstance().direction = ActionArgument.Direction.IN;
+ }
+ break;
+ case relatedStateVariable:
+ getInstance().relatedStateVariable = getCharacters();
+ break;
+ case retval:
+ getInstance().retval = true;
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class StateVariableListHandler extends ServiceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.serviceStateTable;
+
+ public StateVariableListHandler(List instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(StateVariableHandler.EL)) {
+ MutableStateVariable stateVariable = new MutableStateVariable();
+
+ String sendEventsAttributeValue = attributes.getValue(ATTRIBUTE.sendEvents.toString());
+ stateVariable.eventDetails = new StateVariableEventDetails(
+ sendEventsAttributeValue != null && sendEventsAttributeValue.toUpperCase(Locale.ROOT).equals("YES")
+ );
+
+ getInstance().add(stateVariable);
+ new StateVariableHandler(stateVariable, this);
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class StateVariableHandler extends ServiceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.stateVariable;
+
+ public StateVariableHandler(MutableStateVariable instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+ if (element.equals(AllowedValueListHandler.EL)) {
+ List allowedValues = new ArrayList<>();
+ getInstance().allowedValues = allowedValues;
+ new AllowedValueListHandler(allowedValues, this);
+ }
+
+ if (element.equals(AllowedValueRangeHandler.EL)) {
+ MutableAllowedValueRange allowedValueRange = new MutableAllowedValueRange();
+ getInstance().allowedValueRange = allowedValueRange;
+ new AllowedValueRangeHandler(allowedValueRange, this);
+ }
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case name:
+ getInstance().name = getCharacters();
+ break;
+ case dataType:
+ String dtName = getCharacters();
+ Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(dtName);
+ getInstance().dataType = builtin != null ? builtin.getDatatype() : new CustomDatatype(dtName);
+ break;
+ case defaultValue:
+ getInstance().defaultValue = getCharacters();
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class AllowedValueListHandler extends ServiceDescriptorHandler> {
+
+ public static final ELEMENT EL = ELEMENT.allowedValueList;
+
+ public AllowedValueListHandler(List instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ switch (element) {
+ case allowedValue:
+ getInstance().add(getCharacters());
+ break;
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class AllowedValueRangeHandler extends ServiceDescriptorHandler {
+
+ public static final ELEMENT EL = ELEMENT.allowedValueRange;
+
+ public AllowedValueRangeHandler(MutableAllowedValueRange instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ @Override
+ public void endElement(ELEMENT element) throws SAXException {
+ try {
+ switch (element) {
+ case minimum:
+ getInstance().minimum = Long.valueOf(getCharacters());
+ break;
+ case maximum:
+ getInstance().maximum = Long.valueOf(getCharacters());
+ break;
+ case step:
+ getInstance().step = Long.valueOf(getCharacters());
+ break;
+ }
+ } catch (Exception ex) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public boolean isLastElement(ELEMENT element) {
+ return element.equals(EL);
+ }
+ }
+
+ protected static class ServiceDescriptorHandler extends SAXParser.Handler {
+
+ public ServiceDescriptorHandler(I instance) {
+ super(instance);
+ }
+
+ public ServiceDescriptorHandler(I instance, SAXParser parser) {
+ super(instance, parser);
+ }
+
+ public ServiceDescriptorHandler(I instance, ServiceDescriptorHandler parent) {
+ super(instance, parent);
+ }
+
+ public ServiceDescriptorHandler(I instance, SAXParser parser, ServiceDescriptorHandler parent) {
+ super(instance, parser, parent);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ super.startElement(uri, localName, qName, attributes);
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ if (el == null) return;
+ startElement(el, attributes);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ super.endElement(uri, localName, qName);
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ if (el == null) return;
+ endElement(el);
+ }
+
+ @Override
+ protected boolean isLastElement(String uri, String localName, String qName) {
+ ELEMENT el = ELEMENT.valueOrNullOf(localName);
+ return el != null && isLastElement(el);
+ }
+
+ public void startElement(ELEMENT element, Attributes attributes) throws SAXException {
+
+ }
+
+ public void endElement(ELEMENT element) throws SAXException {
+
+ }
+
+ public boolean isLastElement(ELEMENT element) {
+ return false;
+ }
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..c734083c337769b38672c5009a41cd6f1e2e2db6
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint;
+
+import org.fourthline.cling.model.action.ActionException;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.message.control.IncomingActionResponseMessage;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.protocol.sync.SendingAction;
+
+import java.net.URL;
+
+/**
+ * Execute actions on any service.
+ *
+ * Usage example for asynchronous execution in a background thread:
+ *
+ *
+ * Service service = device.findService(new UDAServiceId("SwitchPower"));
+ * Action getStatusAction = service.getAction("GetStatus");
+ * ActionInvocation getStatusInvocation = new ActionInvocation(getStatusAction);
+ *
+ * ActionCallback getStatusCallback = new ActionCallback(getStatusInvocation) {
+ *
+ * public void success(ActionInvocation invocation) {
+ * ActionArgumentValue status = invocation.getOutput("ResultStatus");
+ * assertEquals((Boolean) status.getValue(), Boolean.valueOf(false));
+ * }
+ *
+ * public void failure(ActionInvocation invocation, UpnpResponse res) {
+ * System.err.println(
+ * createDefaultFailureMessage(invocation, res)
+ * );
+ * }
+ * };
+ *
+ * upnpService.getControlPoint().execute(getStatusCallback)
+ *
+ *
+ * You can also execute the action synchronously in the same thread using the
+ * {@link org.fourthline.cling.controlpoint.ActionCallback.Default} implementation:
+ *
+ *
+ * myActionInvocation.setInput("foo", bar);
+ * new ActionCallback.Default(myActionInvocation, upnpService.getControlPoint()).run();
+ * myActionInvocation.getOutput("baz");
+ *
+ *
+ * @author Christian Bauer
+ */
+public abstract class ActionCallback implements Runnable {
+
+ /**
+ * Empty implementation of callback methods, simplifies synchronous
+ * execution of an {@link org.fourthline.cling.model.action.ActionInvocation}.
+ */
+ public static final class Default extends ActionCallback {
+
+ public Default(ActionInvocation actionInvocation, ControlPoint controlPoint) {
+ super(actionInvocation, controlPoint);
+ }
+
+ @Override
+ public void success(ActionInvocation invocation) {
+ }
+
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
+
+ }
+ }
+
+ protected final ActionInvocation actionInvocation;
+
+ protected ControlPoint controlPoint;
+
+ protected ActionCallback(ActionInvocation actionInvocation, ControlPoint controlPoint) {
+ this.actionInvocation = actionInvocation;
+ this.controlPoint = controlPoint;
+ }
+
+ protected ActionCallback(ActionInvocation actionInvocation) {
+ this.actionInvocation = actionInvocation;
+ }
+
+ public ActionInvocation getActionInvocation() {
+ return actionInvocation;
+ }
+
+ synchronized public ControlPoint getControlPoint() {
+ return controlPoint;
+ }
+
+ synchronized public ActionCallback setControlPoint(ControlPoint controlPoint) {
+ this.controlPoint = controlPoint;
+ return this;
+ }
+
+ public void run() {
+ Service service = actionInvocation.getAction().getService();
+
+ // Local execution
+ if (service instanceof LocalService) {
+ LocalService localService = (LocalService)service;
+
+ // Executor validates input inside the execute() call immediately
+ localService.getExecutor(actionInvocation.getAction()).execute(actionInvocation);
+
+ if (actionInvocation.getFailure() != null) {
+ failure(actionInvocation, null);
+ } else {
+ success(actionInvocation);
+ }
+
+ // Remote execution
+ } else if (service instanceof RemoteService){
+
+ if (getControlPoint() == null) {
+ throw new IllegalStateException("Callback must be executed through ControlPoint");
+ }
+
+ RemoteService remoteService = (RemoteService)service;
+
+ // Figure out the remote URL where we'd like to send the action request to
+ URL controLURL;
+ try {
+ controLURL = remoteService.getDevice().normalizeURI(remoteService.getControlURI());
+ } catch(IllegalArgumentException e) {
+ failure(actionInvocation, null, "bad control URL: " + remoteService.getControlURI());
+ return ;
+ }
+
+ // Do it
+ SendingAction prot = getControlPoint().getProtocolFactory().createSendingAction(actionInvocation, controLURL);
+ prot.run();
+
+ IncomingActionResponseMessage response = prot.getOutputMessage();
+
+ if (response == null) {
+ failure(actionInvocation, null);
+ } else if (response.getOperation().isFailed()) {
+ failure(actionInvocation, response.getOperation());
+ } else {
+ success(actionInvocation);
+ }
+ }
+ }
+
+ protected String createDefaultFailureMessage(ActionInvocation invocation, UpnpResponse operation) {
+ String message = "Error: ";
+ final ActionException exception = invocation.getFailure();
+ if (exception != null) {
+ message = message + exception.getMessage();
+ }
+ if (operation != null) {
+ message = message + " (HTTP response was: " + operation.getResponseDetails() + ")";
+ }
+ return message;
+ }
+
+ protected void failure(ActionInvocation invocation, UpnpResponse operation) {
+ failure(invocation, operation, createDefaultFailureMessage(invocation, operation));
+ }
+
+ /**
+ * Called when the action invocation succeeded.
+ *
+ * @param invocation The successful invocation, call its getOutput()
method for results.
+ */
+ public abstract void success(ActionInvocation invocation);
+
+ /**
+ * Called when the action invocation failed.
+ *
+ * @param invocation The failed invocation, call its getFailure()
method for more details.
+ * @param operation If the invocation was on a remote service, the response message, otherwise null.
+ * @param defaultMsg A user-friendly error message generated from the invocation exception and response.
+ * @see #createDefaultFailureMessage
+ */
+ public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg);
+
+ @Override
+ public String toString() {
+ return "(ActionCallback) " + actionInvocation;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..2fa3a14bc59141e3ae5fe2ea4bcd433caf93a28d
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint;
+
+import org.fourthline.cling.model.message.header.UpnpHeader;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.registry.Registry;
+
+import java.util.concurrent.Future;
+
+/**
+ * Unified API for the asynchronous execution of network searches, actions, event subscriptions.
+ *
+ * @author Christian Bauer
+ */
+public interface ControlPoint {
+
+ public UpnpServiceConfiguration getConfiguration();
+ public ProtocolFactory getProtocolFactory();
+ public Registry getRegistry();
+
+ public void search();
+ public void search(UpnpHeader searchType);
+ public void search(int mxSeconds);
+ public void search(UpnpHeader searchType, int mxSeconds);
+ public Future execute(ActionCallback callback);
+ public void execute(SubscriptionCallback callback);
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8fa85fd94427574990b30e67ea3c8b0d5e6341e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint;
+
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.controlpoint.event.ExecuteAction;
+import org.fourthline.cling.controlpoint.event.Search;
+import org.fourthline.cling.model.message.header.MXHeader;
+import org.fourthline.cling.model.message.header.STAllHeader;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.registry.Registry;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.event.Observes;
+import javax.inject.Inject;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+
+/**
+ * Default implementation.
+ *
+ * This implementation uses the executor returned by
+ * {@link org.fourthline.cling.UpnpServiceConfiguration#getSyncProtocolExecutorService()}.
+ *
+ *
+ * @author Christian Bauer
+ */
+@ApplicationScoped
+public class ControlPointImpl implements ControlPoint {
+
+ private static Logger log = Logger.getLogger(ControlPointImpl.class.getName());
+
+ protected UpnpServiceConfiguration configuration;
+ protected ProtocolFactory protocolFactory;
+ protected Registry registry;
+
+ protected ControlPointImpl() {
+ }
+
+ @Inject
+ public ControlPointImpl(UpnpServiceConfiguration configuration, ProtocolFactory protocolFactory, Registry registry) {
+ log.fine("Creating ControlPoint: " + getClass().getName());
+
+ this.configuration = configuration;
+ this.protocolFactory = protocolFactory;
+ this.registry = registry;
+ }
+
+ public UpnpServiceConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public ProtocolFactory getProtocolFactory() {
+ return protocolFactory;
+ }
+
+ public Registry getRegistry() {
+ return registry;
+ }
+
+ public void search(@Observes Search search) {
+ search(search.getSearchType(), search.getMxSeconds());
+ }
+
+ public void search() {
+ search(new STAllHeader(), MXHeader.DEFAULT_VALUE);
+ }
+
+ public void search(UpnpHeader searchType) {
+ search(searchType, MXHeader.DEFAULT_VALUE);
+ }
+
+ public void search(int mxSeconds) {
+ search(new STAllHeader(), mxSeconds);
+ }
+
+ public void search(UpnpHeader searchType, int mxSeconds) {
+ log.fine("Sending asynchronous search for: " + searchType.getString());
+ getConfiguration().getAsyncProtocolExecutor().execute(
+ getProtocolFactory().createSendingSearch(searchType, mxSeconds)
+ );
+ }
+
+ public void execute(ExecuteAction executeAction) {
+ execute(executeAction.getCallback());
+ }
+
+ public Future execute(ActionCallback callback) {
+ log.fine("Invoking action in background: " + callback);
+ callback.setControlPoint(this);
+ ExecutorService executor = getConfiguration().getSyncProtocolExecutorService();
+ return executor.submit(callback);
+ }
+
+ public void execute(SubscriptionCallback callback) {
+ log.fine("Invoking subscription in background: " + callback);
+ callback.setControlPoint(this);
+ getConfiguration().getSyncProtocolExecutorService().execute(callback);
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..5825a76073dbdbb5cc5f3fd4b771fe1b74728347
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint;
+
+import org.fourthline.cling.model.UnsupportedDataException;
+import org.fourthline.cling.model.UserConstants;
+import org.fourthline.cling.model.gena.CancelReason;
+import org.fourthline.cling.model.gena.GENASubscription;
+import org.fourthline.cling.model.gena.LocalGENASubscription;
+import org.fourthline.cling.model.gena.RemoteGENASubscription;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.protocol.ProtocolCreationException;
+import org.fourthline.cling.protocol.sync.SendingSubscribe;
+import org.seamless.util.Exceptions;
+
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Subscribe and receive events from a service through GENA.
+ *
+ * Usage example, establishing a subscription with a {@link org.fourthline.cling.model.meta.Service}:
+ *
+ *
+ * SubscriptionCallback callback = new SubscriptionCallback(service, 600) { // Timeout in seconds
+ *
+ * public void established(GENASubscription sub) {
+ * System.out.println("Established: " + sub.getSubscriptionId());
+ * }
+ *
+ * public void failed(GENASubscription sub, UpnpResponse response, Exception ex) {
+ * System.err.println(
+ * createDefaultFailureMessage(response, ex)
+ * );
+ * }
+ *
+ * public void ended(GENASubscription sub, CancelReason reason, UpnpResponse response) {
+ * // Reason should be null, or it didn't end regularly
+ * }
+ *
+ * public void eventReceived(GENASubscription sub) {
+ * System.out.println("Event: " + sub.getCurrentSequence().getValue());
+ * Map<String, StateVariableValue> values = sub.getCurrentValues();
+ * StateVariableValue status = values.get("Status");
+ * System.out.println("Status is: " + status.toString());
+ * }
+ *
+ * public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
+ * System.out.println("Missed events: " + numberOfMissedEvents);
+ * }
+ * };
+ *
+ * upnpService.getControlPoint().execute(callback);
+ *
+ *
+ * @author Christian Bauer
+ */
+public abstract class SubscriptionCallback implements Runnable {
+
+ protected static Logger log = Logger.getLogger(SubscriptionCallback.class.getName());
+
+ protected final Service service;
+ protected final Integer requestedDurationSeconds;
+
+ private ControlPoint controlPoint;
+ private GENASubscription subscription;
+
+ protected SubscriptionCallback(Service service) {
+ this.service = service;
+ this.requestedDurationSeconds = UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS;
+ }
+
+ protected SubscriptionCallback(Service service, int requestedDurationSeconds) {
+ this.service = service;
+ this.requestedDurationSeconds = requestedDurationSeconds;
+ }
+
+ public Service getService() {
+ return service;
+ }
+
+ synchronized public ControlPoint getControlPoint() {
+ return controlPoint;
+ }
+
+ synchronized public void setControlPoint(ControlPoint controlPoint) {
+ this.controlPoint = controlPoint;
+ }
+
+ synchronized public GENASubscription getSubscription() {
+ return subscription;
+ }
+
+ synchronized public void setSubscription(GENASubscription subscription) {
+ this.subscription = subscription;
+ }
+
+ synchronized public void run() {
+ if (getControlPoint() == null) {
+ throw new IllegalStateException("Callback must be executed through ControlPoint");
+ }
+
+ if (getService() instanceof LocalService) {
+ establishLocalSubscription((LocalService) service);
+ } else if (getService() instanceof RemoteService) {
+ establishRemoteSubscription((RemoteService) service);
+ }
+ }
+
+ private void establishLocalSubscription(LocalService service) {
+
+ if (getControlPoint().getRegistry().getLocalDevice(service.getDevice().getIdentity().getUdn(), false) == null) {
+ log.fine("Local device service is currently not registered, failing subscription immediately");
+ failed(null, null, new IllegalStateException("Local device is not registered"));
+ return;
+ }
+
+ // Local execution of subscription on local service re-uses the procedure and lifecycle that is
+ // used for inbound subscriptions from remote control points on local services!
+ // Except that it doesn't ever expire, we override the requested duration with Integer.MAX_VALUE!
+
+ LocalGENASubscription localSubscription = null;
+ try {
+ localSubscription =
+ new LocalGENASubscription(service, Integer.MAX_VALUE, Collections.EMPTY_LIST) {
+
+ public void failed(Exception ex) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(null);
+ SubscriptionCallback.this.failed(null, null, ex);
+ }
+ }
+
+ public void established() {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(this);
+ SubscriptionCallback.this.established(this);
+ }
+ }
+
+ public void ended(CancelReason reason) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(null);
+ SubscriptionCallback.this.ended(this, reason, null);
+ }
+ }
+
+ public void eventReceived() {
+ synchronized (SubscriptionCallback.this) {
+ log.fine("Local service state updated, notifying callback, sequence is: " + getCurrentSequence());
+ SubscriptionCallback.this.eventReceived(this);
+ incrementSequence();
+ }
+ }
+ };
+
+ log.fine("Local device service is currently registered, also registering subscription");
+ getControlPoint().getRegistry().addLocalSubscription(localSubscription);
+
+ log.fine("Notifying subscription callback of local subscription availablity");
+ localSubscription.establish();
+
+ log.fine("Simulating first initial event for local subscription callback, sequence: " + localSubscription.getCurrentSequence());
+ eventReceived(localSubscription);
+ localSubscription.incrementSequence();
+
+ log.fine("Starting to monitor state changes of local service");
+ localSubscription.registerOnService();
+
+ } catch (Exception ex) {
+ log.fine("Local callback creation failed: " + ex.toString());
+ log.log(Level.FINE, "Exception root cause: ", Exceptions.unwrap(ex));
+ if (localSubscription != null)
+ getControlPoint().getRegistry().removeLocalSubscription(localSubscription);
+ failed(localSubscription, null, ex);
+ }
+ }
+
+ private void establishRemoteSubscription(RemoteService service) {
+ RemoteGENASubscription remoteSubscription =
+ new RemoteGENASubscription(service, requestedDurationSeconds) {
+
+ public void failed(UpnpResponse responseStatus) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(null);
+ SubscriptionCallback.this.failed(this, responseStatus, null);
+ }
+ }
+
+ public void established() {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(this);
+ SubscriptionCallback.this.established(this);
+ }
+ }
+
+ public void ended(CancelReason reason, UpnpResponse responseStatus) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.setSubscription(null);
+ SubscriptionCallback.this.ended(this, reason, responseStatus);
+ }
+ }
+
+ public void eventReceived() {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.eventReceived(this);
+ }
+ }
+
+ public void eventsMissed(int numberOfMissedEvents) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.eventsMissed(this, numberOfMissedEvents);
+ }
+ }
+
+ public void invalidMessage(UnsupportedDataException ex) {
+ synchronized (SubscriptionCallback.this) {
+ SubscriptionCallback.this.invalidMessage(this, ex);
+ }
+ }
+ };
+
+ SendingSubscribe protocol;
+ try {
+ protocol = getControlPoint().getProtocolFactory().createSendingSubscribe(remoteSubscription);
+ } catch (ProtocolCreationException ex) {
+ failed(subscription, null, ex);
+ return;
+ }
+ protocol.run();
+ }
+
+ synchronized public void end() {
+ if (subscription == null) return;
+ if (subscription instanceof LocalGENASubscription) {
+ endLocalSubscription((LocalGENASubscription)subscription);
+ } else if (subscription instanceof RemoteGENASubscription) {
+ endRemoteSubscription((RemoteGENASubscription)subscription);
+ }
+ }
+
+ private void endLocalSubscription(LocalGENASubscription subscription) {
+ log.fine("Removing local subscription and ending it in callback: " + subscription);
+ getControlPoint().getRegistry().removeLocalSubscription(subscription);
+ subscription.end(null); // No reason, on controlpoint request
+ }
+
+ private void endRemoteSubscription(RemoteGENASubscription subscription) {
+ log.fine("Ending remote subscription: " + subscription);
+ getControlPoint().getConfiguration().getSyncProtocolExecutorService().execute(
+ getControlPoint().getProtocolFactory().createSendingUnsubscribe(subscription)
+ );
+ }
+
+ protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception) {
+ failed(subscription, responseStatus, exception, createDefaultFailureMessage(responseStatus, exception));
+ }
+
+ /**
+ * Called when establishing a local or remote subscription failed. To get a nice error message that
+ * transparently detects local or remote errors use createDefaultFailureMessage().
+ *
+ * @param subscription The failed subscription object, not very useful at this point.
+ * @param responseStatus For a remote subscription, if a response was received at all, this is it, otherwise null.
+ * @param exception For a local subscription and failed creation of a remote subscription protocol (before
+ * sending the subscribe request), any exception that caused the failure, otherwise null.
+ * @param defaultMsg A user-friendly error message.
+ * @see #createDefaultFailureMessage
+ */
+ protected abstract void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg);
+
+ /**
+ * Called when a local or remote subscription was successfully established.
+ *
+ * @param subscription The successful subscription.
+ */
+ protected abstract void established(GENASubscription subscription);
+
+ /**
+ * Called when a local or remote subscription ended, either on user request or because of a failure.
+ *
+ * @param subscription The ended subscription instance.
+ * @param reason If the subscription ended regularly (through end()), this is null.
+ * @param responseStatus For a remote subscription, if the cause implies a remopte response and it was
+ * received, this is it (e.g. renewal failure response).
+ */
+ protected abstract void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus);
+
+ /**
+ * Called when an event for an established subscription has been received.
+ *
+ * Use the {@link org.fourthline.cling.model.gena.GENASubscription#getCurrentValues()} method to obtain
+ * the evented state variable values.
+ *
+ *
+ * @param subscription The established subscription with fresh state variable values.
+ */
+ protected abstract void eventReceived(GENASubscription subscription);
+
+ /**
+ * Called when a received event was out of sequence, indicating that events have been missed.
+ *
+ * It's up to you if you want to react to missed events or if you (can) silently ignore them.
+ *
+ * @param subscription The established subscription.
+ * @param numberOfMissedEvents The number of missed events.
+ */
+ protected abstract void eventsMissed(GENASubscription subscription, int numberOfMissedEvents);
+
+ /**
+ * @param responseStatus The (HTTP) response or null
if there was no response.
+ * @param exception The exception or null
if there was no exception.
+ * @return A human-friendly error message.
+ */
+ public static String createDefaultFailureMessage(UpnpResponse responseStatus, Exception exception) {
+ String message = "Subscription failed: ";
+ if (responseStatus != null) {
+ message = message + " HTTP response was: " + responseStatus.getResponseDetails();
+ } else if (exception != null) {
+ message = message + " Exception occured: " + exception;
+ } else {
+ message = message + " No response received.";
+ }
+ return message;
+ }
+
+ /**
+ * Called when a received event message could not be parsed successfully.
+ *
+ * This typically indicates a broken device which is not UPnP compliant. You can
+ * react to this failure in any way you like, for example, you could terminate
+ * the subscription or simply create an error report/log.
+ *
+ *
+ * The default implementation will log the exception at INFO
level, and
+ * the invalid XML at FINE
level.
+ *
+ *
+ * @param remoteGENASubscription The established subscription.
+ * @param ex Call {@link org.fourthline.cling.model.UnsupportedDataException#getData()} to access the invalid XML.
+ */
+ protected void invalidMessage(RemoteGENASubscription remoteGENASubscription,
+ UnsupportedDataException ex) {
+ log.info("Invalid event message received, causing: " + ex);
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("------------------------------------------------------------------------------");
+ log.fine(ex.getData() != null ? ex.getData().toString() : "null");
+ log.fine("------------------------------------------------------------------------------");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(SubscriptionCallback) " + getService();
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..65838bc9b498499a5afabb5d5b63004951efc2e7
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint.event;
+
+import org.fourthline.cling.controlpoint.ActionCallback;
+
+/**
+ * @author Christian Bauer
+ */
+public class ExecuteAction {
+
+ protected ActionCallback callback;
+
+ public ExecuteAction(ActionCallback callback) {
+ this.callback = callback;
+ }
+
+ public ActionCallback getCallback() {
+ return callback;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/Search.java b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/Search.java
new file mode 100644
index 0000000000000000000000000000000000000000..70025239ba7e3d4821bf01ed5f7f29263e200fa4
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/controlpoint/event/Search.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.controlpoint.event;
+
+import org.fourthline.cling.model.message.header.MXHeader;
+import org.fourthline.cling.model.message.header.STAllHeader;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+
+/**
+ * @author Christian Bauer
+ */
+public class Search {
+
+ protected UpnpHeader searchType = new STAllHeader();
+ protected int mxSeconds = MXHeader.DEFAULT_VALUE;
+
+ public Search() {
+ }
+
+ public Search(UpnpHeader searchType) {
+ this.searchType = searchType;
+ }
+
+ public Search(UpnpHeader searchType, int mxSeconds) {
+ this.searchType = searchType;
+ this.mxSeconds = mxSeconds;
+ }
+
+ public Search(int mxSeconds) {
+ this.mxSeconds = mxSeconds;
+ }
+
+ public UpnpHeader getSearchType() {
+ return searchType;
+ }
+
+ public int getMxSeconds() {
+ return mxSeconds;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/mock/MockProtocolFactory.java b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockProtocolFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..292b49f9233c4c065ae7519b3897ac6ed4817e80
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockProtocolFactory.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+package org.fourthline.cling.mock;
+
+import org.fourthline.cling.UpnpService;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.gena.LocalGENASubscription;
+import org.fourthline.cling.model.gena.RemoteGENASubscription;
+import org.fourthline.cling.model.message.IncomingDatagramMessage;
+import org.fourthline.cling.model.message.StreamRequestMessage;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.protocol.ProtocolCreationException;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.protocol.ReceivingAsync;
+import org.fourthline.cling.protocol.ReceivingSync;
+import org.fourthline.cling.protocol.async.SendingNotificationAlive;
+import org.fourthline.cling.protocol.async.SendingNotificationByebye;
+import org.fourthline.cling.protocol.async.SendingSearch;
+import org.fourthline.cling.protocol.sync.SendingAction;
+import org.fourthline.cling.protocol.sync.SendingEvent;
+import org.fourthline.cling.protocol.sync.SendingRenewal;
+import org.fourthline.cling.protocol.sync.SendingSubscribe;
+import org.fourthline.cling.protocol.sync.SendingUnsubscribe;
+
+import javax.enterprise.inject.Alternative;
+import java.net.URL;
+
+/**
+ * @author Christian Bauer
+ */
+@Alternative
+public class MockProtocolFactory implements ProtocolFactory {
+
+ @Override
+ public UpnpService getUpnpService() {
+ return null;
+ }
+
+ @Override
+ public ReceivingAsync createReceivingAsync(IncomingDatagramMessage message) throws ProtocolCreationException {
+ return null;
+ }
+
+ @Override
+ public ReceivingSync createReceivingSync(StreamRequestMessage requestMessage) throws ProtocolCreationException {
+ return null;
+ }
+
+ @Override
+ public SendingNotificationAlive createSendingNotificationAlive(LocalDevice localDevice) {
+ return null;
+ }
+
+ @Override
+ public SendingNotificationByebye createSendingNotificationByebye(LocalDevice localDevice) {
+ return null;
+ }
+
+ @Override
+ public SendingSearch createSendingSearch(UpnpHeader searchTarget, int mxSeconds) {
+ return null;
+ }
+
+ @Override
+ public SendingAction createSendingAction(ActionInvocation actionInvocation, URL controlURL) {
+ return null;
+ }
+
+ @Override
+ public SendingSubscribe createSendingSubscribe(RemoteGENASubscription subscription) {
+ return null;
+ }
+
+ @Override
+ public SendingRenewal createSendingRenewal(RemoteGENASubscription subscription) {
+ return null;
+ }
+
+ @Override
+ public SendingUnsubscribe createSendingUnsubscribe(RemoteGENASubscription subscription) {
+ return null;
+ }
+
+ @Override
+ public SendingEvent createSendingEvent(LocalGENASubscription subscription) {
+ return null;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/mock/MockRouter.java b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockRouter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d4326b9e8968eb356efe21b43b4b9018b153afd
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockRouter.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+package org.fourthline.cling.mock;
+
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.model.NetworkAddress;
+import org.fourthline.cling.model.message.IncomingDatagramMessage;
+import org.fourthline.cling.model.message.OutgoingDatagramMessage;
+import org.fourthline.cling.model.message.StreamRequestMessage;
+import org.fourthline.cling.model.message.StreamResponseMessage;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.transport.Router;
+import org.fourthline.cling.transport.RouterException;
+import org.fourthline.cling.transport.impl.NetworkAddressFactoryImpl;
+import org.fourthline.cling.transport.spi.InitializationException;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+import org.fourthline.cling.transport.spi.UpnpStream;
+
+import javax.enterprise.inject.Alternative;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ *
+ * This is not a real network transport layer, it collects all messages instead and makes
+ * them available for testing with {@link #getOutgoingDatagramMessages()},
+ * {@link #getSentStreamRequestMessages()}, etc. Mock responses for TCP (HTTP) stream requests
+ * can be returned by overriding {@link #getStreamResponseMessage(org.fourthline.cling.model.message.StreamRequestMessage)}
+ * or {@link #getStreamResponseMessages()} if you know the order of requests.
+ *
+ *
+ * @author Christian Bauer
+ */
+@Alternative
+public class MockRouter implements Router {
+
+ public int counter = -1;
+ public List incomingDatagramMessages = new ArrayList<>();
+ public List outgoingDatagramMessages = new ArrayList<>();
+ public List receivedUpnpStreams = new ArrayList<>();
+ public List sentStreamRequestMessages = new ArrayList<>();
+ public List broadcastedBytes = new ArrayList<>();
+
+ protected UpnpServiceConfiguration configuration;
+ protected ProtocolFactory protocolFactory;
+
+ public MockRouter(UpnpServiceConfiguration configuration,
+ ProtocolFactory protocolFactory) {
+ this.configuration = configuration;
+ this.protocolFactory = protocolFactory;
+ }
+
+ @Override
+ public UpnpServiceConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public ProtocolFactory getProtocolFactory() {
+ return protocolFactory;
+ }
+
+ @Override
+ public boolean enable() throws RouterException {
+ return false;
+ }
+
+ @Override
+ public boolean disable() throws RouterException {
+ return false;
+ }
+
+ @Override
+ public void shutdown() throws RouterException {
+ }
+
+ @Override
+ public boolean isEnabled() throws RouterException {
+ return false;
+ }
+
+ @Override
+ public void handleStartFailure(InitializationException ex) throws InitializationException {
+ }
+
+ @Override
+ public List getActiveStreamServers(InetAddress preferredAddress) throws RouterException {
+ // Simulate an active stream server, otherwise the notification/search response
+ // protocols won't even run
+ try {
+ return Arrays.asList(
+ new NetworkAddress(
+ InetAddress.getByName("127.0.0.1"),
+ NetworkAddressFactoryImpl.DEFAULT_TCP_HTTP_LISTEN_PORT
+ )
+ );
+ } catch (UnknownHostException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public void received(IncomingDatagramMessage msg) {
+ incomingDatagramMessages.add(msg);
+ }
+
+ public void received(UpnpStream stream) {
+ receivedUpnpStreams.add(stream);
+ }
+
+ public void send(OutgoingDatagramMessage msg) throws RouterException {
+ outgoingDatagramMessages.add(msg);
+ }
+
+ public StreamResponseMessage send(StreamRequestMessage msg) throws RouterException {
+ sentStreamRequestMessages.add(msg);
+ counter++;
+ return getStreamResponseMessages() != null
+ ? getStreamResponseMessages()[counter]
+ : getStreamResponseMessage(msg);
+ }
+
+ public void broadcast(byte[] bytes) {
+ broadcastedBytes.add(bytes);
+ }
+
+ public void resetStreamRequestMessageCounter() {
+ counter = -1;
+ }
+
+ public List getIncomingDatagramMessages() {
+ return incomingDatagramMessages;
+ }
+
+ public List getOutgoingDatagramMessages() {
+ return outgoingDatagramMessages;
+ }
+
+ public List getReceivedUpnpStreams() {
+ return receivedUpnpStreams;
+ }
+
+ public List getSentStreamRequestMessages() {
+ return sentStreamRequestMessages;
+ }
+
+ public List getBroadcastedBytes() {
+ return broadcastedBytes;
+ }
+
+ public StreamResponseMessage[] getStreamResponseMessages() {
+ return null;
+ }
+
+ public StreamResponseMessage getStreamResponseMessage(StreamRequestMessage request) {
+ return null;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpService.java b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpService.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e4e602a7c527552cded0fa6808fd4bf3b11d190
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpService.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.mock;
+
+import org.fourthline.cling.UpnpService;
+import org.fourthline.cling.UpnpServiceConfiguration;
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.controlpoint.ControlPointImpl;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.protocol.ProtocolFactory;
+import org.fourthline.cling.protocol.ProtocolFactoryImpl;
+import org.fourthline.cling.protocol.async.SendingNotificationAlive;
+import org.fourthline.cling.protocol.async.SendingSearch;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.registry.RegistryImpl;
+import org.fourthline.cling.registry.RegistryMaintainer;
+import org.fourthline.cling.transport.RouterException;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+
+import javax.enterprise.inject.Alternative;
+
+/**
+ * Simplifies testing of core and non-core modules.
+ *
+ * It uses the {@link org.fourthline.cling.mock.MockUpnpService.MockProtocolFactory}.
+ *
+ *
+ * @author Christian Bauer
+ */
+@Alternative
+public class MockUpnpService implements UpnpService {
+
+ protected final UpnpServiceConfiguration configuration;
+ protected final ControlPoint controlPoint;
+ protected final ProtocolFactory protocolFactory;
+ protected final Registry registry;
+ protected final MockRouter router;
+
+ protected final NetworkAddressFactory networkAddressFactory;
+
+ /**
+ * Single-thread of execution for the whole UPnP stack, no ALIVE messages or registry maintenance.
+ */
+ public MockUpnpService() {
+ this(false, new MockUpnpServiceConfiguration(false, false));
+ }
+
+ /**
+ * No ALIVE messages.
+ */
+ public MockUpnpService(MockUpnpServiceConfiguration configuration) {
+ this(false, configuration);
+ }
+
+ /**
+ * Single-thread of execution for the whole UPnP stack, except one background registry maintenance thread.
+ */
+ public MockUpnpService(final boolean sendsAlive, final boolean maintainsRegistry) {
+ this(sendsAlive, new MockUpnpServiceConfiguration(maintainsRegistry, false));
+ }
+
+ public MockUpnpService(final boolean sendsAlive, final boolean maintainsRegistry, final boolean multiThreaded) {
+ this(sendsAlive, new MockUpnpServiceConfiguration(maintainsRegistry, multiThreaded));
+ }
+
+ public MockUpnpService(final boolean sendsAlive, final MockUpnpServiceConfiguration configuration) {
+
+ this.configuration = configuration;
+
+ this.protocolFactory = createProtocolFactory(this, sendsAlive);
+
+ this.registry = new RegistryImpl(this) {
+ @Override
+ protected RegistryMaintainer createRegistryMaintainer() {
+ return configuration.isMaintainsRegistry() ? super.createRegistryMaintainer() : null;
+ }
+ };
+
+ this.networkAddressFactory = this.configuration.createNetworkAddressFactory();
+
+ this.router = createRouter();
+
+ this.controlPoint = new ControlPointImpl(configuration, protocolFactory, registry);
+ }
+
+ protected ProtocolFactory createProtocolFactory(UpnpService service, boolean sendsAlive) {
+ return new MockProtocolFactory(service, sendsAlive);
+ }
+
+ protected MockRouter createRouter() {
+ return new MockRouter(getConfiguration(), getProtocolFactory());
+ }
+
+ /**
+ * This factory customizes several protocols.
+ *
+ * The {@link org.fourthline.cling.protocol.async.SendingNotificationAlive} protocol
+ * only sends messages if this feature is enabled when instantiating the factory.
+ *
+ *
+ * The {@link org.fourthline.cling.protocol.async.SendingSearch} protocol doesn't wait between
+ * sending search message bulks, this speeds up testing.
+ *
+ */
+ public static class MockProtocolFactory extends ProtocolFactoryImpl {
+
+ private boolean sendsAlive;
+
+ public MockProtocolFactory(UpnpService upnpService, boolean sendsAlive) {
+ super(upnpService);
+ this.sendsAlive = sendsAlive;
+ }
+
+ @Override
+ public SendingNotificationAlive createSendingNotificationAlive(LocalDevice localDevice) {
+ return new SendingNotificationAlive(getUpnpService(), localDevice) {
+ @Override
+ protected void execute() throws RouterException {
+ if (sendsAlive) super.execute();
+ }
+ };
+ }
+
+ @Override
+ public SendingSearch createSendingSearch(UpnpHeader searchTarget, int mxSeconds) {
+ return new SendingSearch(getUpnpService(), searchTarget, mxSeconds) {
+ @Override
+ public int getBulkIntervalMilliseconds() {
+ return 0; // Don't wait
+ }
+ };
+ }
+ }
+
+ public UpnpServiceConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public ControlPoint getControlPoint() {
+ return controlPoint;
+ }
+
+ public ProtocolFactory getProtocolFactory() {
+ return protocolFactory;
+ }
+
+ public Registry getRegistry() {
+ return registry;
+ }
+
+ public MockRouter getRouter() {
+ return router;
+ }
+
+ public void shutdown() {
+ getRegistry().shutdown();
+ getConfiguration().shutdown();
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpServiceConfiguration.java b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpServiceConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..80aff8101fba45ae68568031689feccf5535bbd4
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/mock/MockUpnpServiceConfiguration.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.mock;
+
+import org.fourthline.cling.DefaultUpnpServiceConfiguration;
+import org.fourthline.cling.transport.impl.NetworkAddressFactoryImpl;
+import org.fourthline.cling.transport.spi.NetworkAddressFactory;
+
+import javax.enterprise.inject.Alternative;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Christian Bauer
+ */
+@Alternative
+public class MockUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration {
+
+ final protected boolean maintainsRegistry;
+ final protected boolean multiThreaded;
+
+ /**
+ * Does not maintain registry, single threaded execution.
+ */
+ public MockUpnpServiceConfiguration() {
+ this(false, false);
+ }
+
+ /**
+ * Single threaded execution.
+ */
+ public MockUpnpServiceConfiguration(boolean maintainsRegistry) {
+ this(maintainsRegistry, false);
+ }
+
+ public MockUpnpServiceConfiguration(boolean maintainsRegistry, boolean multiThreaded) {
+ super(false);
+ this.maintainsRegistry = maintainsRegistry;
+ this.multiThreaded = multiThreaded;
+ }
+
+ public boolean isMaintainsRegistry() {
+ return maintainsRegistry;
+ }
+
+ public boolean isMultiThreaded() {
+ return multiThreaded;
+ }
+
+ @Override
+ protected NetworkAddressFactory createNetworkAddressFactory(int streamListenPort) {
+ // We are only interested in 127.0.0.1
+ return new NetworkAddressFactoryImpl(streamListenPort) {
+ @Override
+ protected boolean isUsableNetworkInterface(NetworkInterface iface) throws Exception {
+ return (iface.isLoopback());
+ }
+
+ @Override
+ protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) {
+ return (address.isLoopbackAddress() && address instanceof Inet4Address);
+ }
+
+ };
+ }
+
+ @Override
+ public Executor getRegistryMaintainerExecutor() {
+ if (isMaintainsRegistry()) {
+ return new Executor() {
+ public void execute(Runnable runnable) {
+ new Thread(runnable).start();
+ }
+ };
+ }
+ return getDefaultExecutorService();
+ }
+
+ @Override
+ protected ExecutorService getDefaultExecutorService() {
+ if (isMultiThreaded()) {
+ return super.getDefaultExecutorService();
+ }
+ return new AbstractExecutorService() {
+
+ boolean terminated;
+
+ public void shutdown() {
+ terminated = true;
+ }
+
+ public List shutdownNow() {
+ shutdown();
+ return null;
+ }
+
+ public boolean isShutdown() {
+ return terminated;
+ }
+
+ public boolean isTerminated() {
+ return terminated;
+ }
+
+ public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
+ shutdown();
+ return terminated;
+ }
+
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ };
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/Command.java b/clinglibrary/src/main/java/org/fourthline/cling/model/Command.java
new file mode 100644
index 0000000000000000000000000000000000000000..21e9dc880a132017515ead5d3902ca0d075970ed
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/Command.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Executable procedure, invoked and potentially decorated by the {@link org.fourthline.cling.model.ServiceManager}.
+ *
+ * @author Christian Bauer
+ */
+public interface Command {
+
+ public void execute(ServiceManager manager) throws Exception;
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/Constants.java b/clinglibrary/src/main/java/org/fourthline/cling/model/Constants.java
new file mode 100644
index 0000000000000000000000000000000000000000..326db1d3ca7408dff88f9faf7a90e5e042353d60
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/Constants.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Shared and immutable settings.
+ *
+ * @author Christian Bauer
+ */
+public interface Constants {
+
+ public static final String SYSTEM_PROPERTY_ANNOUNCE_MAC_ADDRESS = "org.fourthline.cling.network.announceMACAddress";
+
+ public static final int UPNP_MULTICAST_PORT = 1900;
+
+ public static final String IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250";
+
+ public static final String IPV6_UPNP_LINK_LOCAL_ADDRESS = "FF02::C";
+ public static final String IPV6_UPNP_SUBNET_ADDRESS = "FF03::C";
+ public static final String IPV6_UPNP_ADMINISTRATIVE_ADDRESS = "FF04::C";
+ public static final String IPV6_UPNP_SITE_LOCAL_ADDRESS = "FF05::C";
+ public static final String IPV6_UPNP_GLOBAL_ADDRESS = "FF0E::C";
+
+ public static final int MIN_ADVERTISEMENT_AGE_SECONDS = 1800;
+
+ // Parsing rules for: deviceType, serviceType, serviceId (UDA 1.0, section 2.5)
+
+ // TODO: UPNP VIOLATION: Microsoft Windows Media Player Sharing 4.0, X_MS_MediaReceiverRegistrar service has type with periods instead of hyphens in the namespace!
+ // UDA 1.0 spec: "Period characters in the vendor domain name MUST be replaced with hyphens in accordance with RFC 2141"
+ // TODO: UPNP VIOLATION: Azureus/Vuze 4.2.0.2 sends a URN as a service identifier, so we need to match colons!
+ // TODO: UPNP VIOLATION: Intel UPnP Tools send dots in the service identifier suffix, match that...
+
+ public static final String REGEX_NAMESPACE = "[a-zA-Z0-9\\-\\.]+";
+ public static final String REGEX_TYPE = "[a-zA-Z_0-9\\-]{1,64}";
+ public static final String REGEX_ID = "[a-zA-Z_0-9\\-:\\.]{1,64}";
+
+ /*
+ Must not contain a hyphen character (-, 2D Hex in UTF- 8). First character must be a USASCII letter (A-Z, a-z),
+ USASCII digit (0-9), an underscore ("_"), or a non-experimental Unicode letter or digit greater than U+007F.
+ Succeeding characters must be a USASCII letter (A-Z, a-z), USASCII digit (0-9), an underscore ("_"), a
+ period ("."), a Unicode combiningchar, an extender, or a non-experimental Unicode letter or digit greater
+ than U+007F. The first three letters must not be "XML" in any combination of case. Case sensitive.
+ */
+ // TODO: I have no idea how to match or what even is a "unicode extender character", neither does the Unicode book
+ public static final String REGEX_UDA_NAME = "[a-zA-Z0-9^-_\\p{L}\\p{N}]{1}[a-zA-Z0-9^-_\\.\\\\p{L}\\\\p{N}\\p{Mc}\\p{Sk}]*";
+
+ // Random patentable "inventions" by MSFT
+ public static final String SOAP_NS_ENVELOPE = "http://schemas.xmlsoap.org/soap/envelope/";
+ public static final String SOAP_URI_ENCODING_STYLE = "http://schemas.xmlsoap.org/soap/encoding/";
+ public static final String NS_UPNP_CONTROL_10 = "urn:schemas-upnp-org:control-1-0";
+ public static final String NS_UPNP_EVENT_10 = "urn:schemas-upnp-org:event-1-0";
+
+ // State variable prefixes
+ public static final String ARG_TYPE_PREFIX = "A_ARG_TYPE_";
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java b/clinglibrary/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..facde0cf8f87c5de4de012f9474493686968e204
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.seamless.util.Exceptions;
+import org.seamless.util.Reflections;
+
+/**
+ * Default implementation, creates and manages a single instance of a plain Java bean.
+ *
+ * Creates instance of the defined service class when it is first needed (acts as a factory),
+ * manages the instance in a field (it's shared), and synchronizes (locks) all
+ * multi-threaded access. A locking attempt will timeout after 500 milliseconds with
+ * a runtime exception if another operation is already in progress. Override
+ * {@link #getLockTimeoutMillis()} to customize this behavior, e.g. if your service
+ * bean is slow and requires more time for typical action executions or state
+ * variable reading.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class DefaultServiceManager implements ServiceManager {
+
+ private static Logger log = Logger.getLogger(DefaultServiceManager.class.getName());
+
+ final protected LocalService service;
+ final protected Class serviceClass;
+ final protected ReentrantLock lock = new ReentrantLock(true);
+
+ // Locking!
+ protected T serviceImpl;
+ protected PropertyChangeSupport propertyChangeSupport;
+
+ protected DefaultServiceManager(LocalService service) {
+ this(service, null);
+ }
+
+ public DefaultServiceManager(LocalService service, Class serviceClass) {
+ this.service = service;
+ this.serviceClass = serviceClass;
+ }
+
+ // The monitor entry and exit methods
+
+ protected void lock() {
+ try {
+ if (lock.tryLock(getLockTimeoutMillis(), TimeUnit.MILLISECONDS)) {
+ if (log.isLoggable(Level.FINEST))
+ log.finest("Acquired lock");
+ } else {
+ throw new RuntimeException("Failed to acquire lock in milliseconds: " + getLockTimeoutMillis());
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to acquire lock:" + e);
+ }
+ }
+
+ protected void unlock() {
+ if (log.isLoggable(Level.FINEST))
+ log.finest("Releasing lock");
+ lock.unlock();
+ }
+
+ protected int getLockTimeoutMillis() {
+ return 500;
+ }
+
+ public LocalService getService() {
+ return service;
+ }
+
+ public T getImplementation() {
+ lock();
+ try {
+ if (serviceImpl == null) {
+ init();
+ }
+ return serviceImpl;
+ } finally {
+ unlock();
+ }
+ }
+
+ public PropertyChangeSupport getPropertyChangeSupport() {
+ lock();
+ try {
+ if (propertyChangeSupport == null) {
+ init();
+ }
+ return propertyChangeSupport;
+ } finally {
+ unlock();
+ }
+ }
+
+ public void execute(Command cmd) throws Exception {
+ lock();
+ try {
+ cmd.execute(this);
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public Collection getCurrentState() throws Exception {
+ lock();
+ try {
+ Collection values = readInitialEventedStateVariableValues();
+ if (values != null) {
+ log.fine("Obtained initial state variable values for event, skipping individual state variable accessors");
+ return values;
+ }
+ values = new ArrayList<>();
+ for (StateVariable stateVariable : getService().getStateVariables()) {
+ if (stateVariable.getEventDetails().isSendEvents()) {
+ StateVariableAccessor accessor = getService().getAccessor(stateVariable);
+ if (accessor == null)
+ throw new IllegalStateException("No accessor for evented state variable");
+ values.add(accessor.read(stateVariable, getImplementation()));
+ }
+ }
+ return values;
+ } finally {
+ unlock();
+ }
+ }
+
+ protected Collection getCurrentState(String[] variableNames) throws Exception {
+ lock();
+ try {
+ Collection values = new ArrayList<>();
+ for (String variableName : variableNames) {
+ variableName = variableName.trim();
+
+ StateVariable stateVariable = getService().getStateVariable(variableName);
+ if (stateVariable == null || !stateVariable.getEventDetails().isSendEvents()) {
+ log.fine("Ignoring unknown or non-evented state variable: " + variableName);
+ continue;
+ }
+
+ StateVariableAccessor accessor = getService().getAccessor(stateVariable);
+ if (accessor == null) {
+ log.warning("Ignoring evented state variable without accessor: " + variableName);
+ continue;
+ }
+ values.add(accessor.read(stateVariable, getImplementation()));
+ }
+ return values;
+ } finally {
+ unlock();
+ }
+ }
+
+ protected void init() {
+ log.fine("No service implementation instance available, initializing...");
+ try {
+ // The actual instance we ware going to use and hold a reference to (1:1 instance for manager)
+ serviceImpl = createServiceInstance();
+
+ // How the implementation instance will tell us about property changes
+ propertyChangeSupport = createPropertyChangeSupport(serviceImpl);
+ propertyChangeSupport.addPropertyChangeListener(createPropertyChangeListener(serviceImpl));
+
+ } catch (Exception ex) {
+ throw new RuntimeException("Could not initialize implementation: " + ex, ex);
+ }
+ }
+
+ protected T createServiceInstance() throws Exception {
+ if (serviceClass == null) {
+ throw new IllegalStateException("Subclass has to provide service class or override createServiceInstance()");
+ }
+ try {
+ // Use this constructor if possible
+ return serviceClass.getConstructor(LocalService.class).newInstance(getService());
+ } catch (NoSuchMethodException ex) {
+ log.fine("Creating new service implementation instance with no-arg constructor: " + serviceClass.getName());
+ return serviceClass.newInstance();
+ }
+ }
+
+ protected PropertyChangeSupport createPropertyChangeSupport(T serviceImpl) throws Exception {
+ Method m;
+ if ((m = Reflections.getGetterMethod(serviceImpl.getClass(), "propertyChangeSupport")) != null &&
+ PropertyChangeSupport.class.isAssignableFrom(m.getReturnType())) {
+ log.fine("Service implementation instance offers PropertyChangeSupport, using that: " + serviceImpl.getClass().getName());
+ return (PropertyChangeSupport) m.invoke(serviceImpl);
+ }
+ log.fine("Creating new PropertyChangeSupport for service implementation: " + serviceImpl.getClass().getName());
+ return new PropertyChangeSupport(serviceImpl);
+ }
+
+ protected PropertyChangeListener createPropertyChangeListener(T serviceImpl) throws Exception {
+ return new DefaultPropertyChangeListener();
+ }
+
+ protected Collection readInitialEventedStateVariableValues() throws Exception {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + getClass().getSimpleName() + ") Implementation: " + serviceImpl;
+ }
+
+ protected class DefaultPropertyChangeListener implements PropertyChangeListener {
+
+ public void propertyChange(PropertyChangeEvent e) {
+ log.finer("Property change event on local service: " + e.getPropertyName());
+
+ // Prevent recursion
+ if (e.getPropertyName().equals(EVENTED_STATE_VARIABLES)) return;
+
+ String[] variableNames = ModelUtil.fromCommaSeparatedList(e.getPropertyName());
+ log.fine("Changed variable names: " + Arrays.toString(variableNames));
+
+ try {
+ Collection currentValues = getCurrentState(variableNames);
+
+ if (!currentValues.isEmpty()) {
+ getPropertyChangeSupport().firePropertyChange(
+ EVENTED_STATE_VARIABLES,
+ null,
+ currentValues
+ );
+ }
+
+ } catch (Exception ex) {
+ // TODO: Is it OK to only log this error? It means we keep running although we couldn't send events?
+ log.log(
+ Level.SEVERE,
+ "Error reading state of service after state variable update event: " + Exceptions.unwrap(ex),
+ ex
+ );
+ }
+ }
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/DiscoveryOptions.java b/clinglibrary/src/main/java/org/fourthline/cling/model/DiscoveryOptions.java
new file mode 100644
index 0000000000000000000000000000000000000000..54e68e05b8bd2e48c8d530b320be4dfa141d7e9e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/DiscoveryOptions.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Options for discovery processing by the {@link org.fourthline.cling.registry.Registry}.
+ *
+ * @author Christian Bauer
+ */
+public class DiscoveryOptions {
+
+ protected boolean advertised;
+ protected boolean byeByeBeforeFirstAlive;
+
+ /**
+ * @param advertised If false
, no alive notifications will be announced for
+ * this device and it will not appear in search responses.
+ */
+ public DiscoveryOptions(boolean advertised) {
+ this.advertised = advertised;
+ }
+
+ /**
+ *
+ * @param advertised If false
, no alive notifications will be announced for
+ * this device and it will not appear in search responses.
+ * @param byeByeBeforeFirstAlive If true
, a byebye NOTIFY message will be send before the
+ * first alive NOTIFY message.
+ */
+ public DiscoveryOptions(boolean advertised, boolean byeByeBeforeFirstAlive) {
+ this.advertised = advertised;
+ this.byeByeBeforeFirstAlive = byeByeBeforeFirstAlive;
+ }
+
+ /**
+ * @return true for regular advertisement with alive
+ * messages and in search responses.
+ */
+ public boolean isAdvertised() {
+ return advertised;
+ }
+
+ /**
+ * @return true if a byebye NOTIFY message will be send before the
+ * first alive NOTIFY message.
+ */
+ public boolean isByeByeBeforeFirstAlive() {
+ return byeByeBeforeFirstAlive;
+ }
+
+ // Performance optimization on Android
+ private static String simpleName = DiscoveryOptions.class.getSimpleName();
+ @Override
+ public String toString() {
+ return "(" + simpleName + ")" + " advertised: " + isAdvertised() + " byebyeBeforeFirstAlive: " + isByeByeBeforeFirstAlive();
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ExpirationDetails.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ExpirationDetails.java
new file mode 100644
index 0000000000000000000000000000000000000000..c29c1b6e254d6c888b63757064634526c0ff1431
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ExpirationDetails.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.util.Date;
+
+/**
+ * @author Christian Bauer
+ */
+public class ExpirationDetails {
+
+ public static final int UNLIMITED_AGE = 0;
+
+ private int maxAgeSeconds = UNLIMITED_AGE;
+ private long lastRefreshTimestampSeconds = getCurrentTimestampSeconds();
+
+ public ExpirationDetails() {
+ }
+
+ public ExpirationDetails(int maxAgeSeconds) {
+ this.maxAgeSeconds = maxAgeSeconds;
+ }
+
+ public int getMaxAgeSeconds() {
+ return maxAgeSeconds;
+ }
+
+ public long getLastRefreshTimestampSeconds() {
+ return lastRefreshTimestampSeconds;
+ }
+
+ public void setLastRefreshTimestampSeconds(long lastRefreshTimestampSeconds) {
+ this.lastRefreshTimestampSeconds = lastRefreshTimestampSeconds;
+ }
+
+ public void stampLastRefresh() {
+ setLastRefreshTimestampSeconds(getCurrentTimestampSeconds());
+ }
+
+ public boolean hasExpired() {
+ return hasExpired(false);
+ }
+
+ /**
+ * @param halfTime If true
then half maximum age is used to determine expiration.
+ * @return true
if the maximum age has been reached.
+ */
+ public boolean hasExpired(boolean halfTime) {
+ // Note: Uses direct field access for performance reasons on Android
+ return maxAgeSeconds != UNLIMITED_AGE &&
+ (lastRefreshTimestampSeconds + (maxAgeSeconds/(halfTime ? 2 : 1))) < getCurrentTimestampSeconds();
+ }
+
+ public long getSecondsUntilExpiration() {
+ // Note: Uses direct field access for performance reasons on Android
+ return maxAgeSeconds == UNLIMITED_AGE
+ ? Integer.MAX_VALUE
+ : (lastRefreshTimestampSeconds + maxAgeSeconds) - getCurrentTimestampSeconds();
+ }
+
+ protected long getCurrentTimestampSeconds() {
+ return new Date().getTime()/1000;
+ }
+
+ // Performance optimization on Android
+ private static String simpleName = ExpirationDetails.class.getSimpleName();
+ @Override
+ public String toString() {
+ return "(" + simpleName + ")" + " MAX AGE: " + maxAgeSeconds;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/Location.java b/clinglibrary/src/main/java/org/fourthline/cling/model/Location.java
new file mode 100644
index 0000000000000000000000000000000000000000..83f0227ef00ceddc345eee056726b65c02c03305
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/Location.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * The IP address/port, MAC address, and URI path of a (network) location.
+ *
+ * Used when sending messages about local devices and services to
+ * other UPnP participants on the network, such as where our device/service
+ * descriptors can be found or what callback address to use for event message
+ * delivery. We also let them know our MAC hardware address so they
+ * can wake us up from sleep with Wake-On-LAN if necessary.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class Location {
+
+ protected final NetworkAddress networkAddress;
+ protected final String path;
+ protected final URL url;
+
+ public Location(NetworkAddress networkAddress, String path) {
+ this.networkAddress = networkAddress;
+ this.path = path;
+ this.url = createAbsoluteURL(networkAddress.getAddress(), networkAddress.getPort(), path);
+ }
+
+ public NetworkAddress getNetworkAddress() {
+ return networkAddress;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Location location = (Location) o;
+
+ if (!networkAddress.equals(location.networkAddress)) return false;
+ if (!path.equals(location.path)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = networkAddress.hashCode();
+ result = 31 * result + path.hashCode();
+ return result;
+ }
+
+ /**
+ * @return An HTTP URL with the address, port, and path of this location.
+ */
+ public URL getURL() {
+ return url;
+ }
+
+ // Performance optimization on Android
+ private static URL createAbsoluteURL(InetAddress address, int localStreamPort, String path) {
+ try {
+ return new URL("http", address.getHostAddress(), localStreamPort, path);
+ } catch (Exception ex) {
+ throw new IllegalArgumentException("Address, port, and URI can not be converted to URL", ex);
+ }
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ModelUtil.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ModelUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..c45de7e0772de71a9404b17feecbdc98c5be1d5f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ModelUtil.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.Locale;
+
+/**
+ * Shared trivial procedures.
+ *
+ * @author Christian Bauer
+ */
+public class ModelUtil {
+
+ /**
+ * True if this class is executing on an Android runtime
+ */
+ final public static boolean ANDROID_RUNTIME;
+ static {
+ boolean foundAndroid = false;
+ try {
+ Class androidBuild = Thread.currentThread().getContextClassLoader().loadClass("android.os.Build");
+ foundAndroid = androidBuild.getField("ID").get(null) != null;
+ } catch (Exception ex) {
+ // Ignore
+ }
+ ANDROID_RUNTIME = foundAndroid;
+ }
+
+ /**
+ * True if this class is executing on an Android emulator runtime.
+ */
+ final public static boolean ANDROID_EMULATOR;
+ static {
+ boolean foundEmulator = false;
+ try {
+ Class androidBuild = Thread.currentThread().getContextClassLoader().loadClass("android.os.Build");
+ String product = (String)androidBuild.getField("PRODUCT").get(null);
+ if ("google_sdk".equals(product) || ("sdk".equals(product)))
+ foundEmulator = true;
+ } catch (Exception ex) {
+ // Ignore
+ }
+ ANDROID_EMULATOR = foundEmulator;
+ }
+
+ /**
+ * @param stringConvertibleTypes A collection of interfaces.
+ * @param clazz An interface to test.
+ * @return true
if the given interface is an Enum, or if the collection contains a super-interface.
+ */
+ public static boolean isStringConvertibleType(Set stringConvertibleTypes, Class clazz) {
+ if (clazz.isEnum()) return true;
+ for (Class toStringOutputType : stringConvertibleTypes) {
+ if (toStringOutputType.isAssignableFrom(clazz)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param name A UPnP device architecture "name" string.
+ * @return true
if the name is not empty, doesn't start with "xml", and
+ * matches {@link org.fourthline.cling.model.Constants#REGEX_UDA_NAME}.
+ */
+ public static boolean isValidUDAName(String name) {
+ if (ANDROID_RUNTIME) {
+ return name != null && name.length() != 0;
+ }
+ return name != null && name.length() != 0 && !name.toLowerCase(Locale.ROOT).startsWith("xml") && name.matches(Constants.REGEX_UDA_NAME);
+ }
+
+ /**
+ * Wraps the checked exception in a runtime exception.
+ */
+ public static InetAddress getInetAddressByName(String name) {
+ try {
+ return InetAddress.getByName(name);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Converts the given instances into comma-separated elements of a string,
+ * escaping commas with backslashes.
+ */
+ public static String toCommaSeparatedList(Object[] o) {
+ return toCommaSeparatedList(o, true, false);
+ }
+
+ /**
+ * Converts the given instances into comma-separated elements of a string,
+ * optionally escapes commas and double quotes with backslahses.
+ */
+ public static String toCommaSeparatedList(Object[] o, boolean escapeCommas, boolean escapeDoubleQuotes) {
+ if (o == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Object obj : o) {
+ String objString = obj.toString();
+ objString = objString.replaceAll("\\\\", "\\\\\\\\"); // Replace one backslash with two (nice, eh?)
+ if (escapeCommas) {
+ objString = objString.replaceAll(",", "\\\\,");
+ }
+ if (escapeDoubleQuotes) {
+ objString = objString.replaceAll("\"", "\\\"");
+ }
+ sb.append(objString).append(",");
+ }
+ if (sb.length() > 1) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+
+ }
+
+ /**
+ * Converts the comma-separated elements of a string into an array of strings,
+ * unescaping backslashed commas.
+ */
+ public static String[] fromCommaSeparatedList(String s) {
+ return fromCommaSeparatedList(s, true);
+ }
+
+ /**
+ * Converts the comma-separated elements of a string into an array of strings,
+ * optionally unescaping backslashed commas.
+ */
+ public static String[] fromCommaSeparatedList(String s, boolean unescapeCommas) {
+ if (s == null || s.length() == 0) {
+ return null;
+ }
+
+ final String QUOTED_COMMA_PLACEHOLDER = "XXX1122334455XXX";
+ if (unescapeCommas) {
+ s = s.replaceAll("\\\\,", QUOTED_COMMA_PLACEHOLDER);
+ }
+
+ String[] split = s.split(",");
+ for (int i = 0; i < split.length; i++) {
+ split[i] = split[i].replaceAll(QUOTED_COMMA_PLACEHOLDER, ",");
+ split[i] = split[i].replaceAll("\\\\\\\\", "\\\\");
+ }
+ return split;
+ }
+
+ /**
+ * @param seconds The number of seconds to convert.
+ * @return A string representing hours, minutes, seconds, e.g. 11:23:44
+ */
+ public static String toTimeString(long seconds) {
+ long hours = seconds / 3600,
+ remainder = seconds % 3600,
+ minutes = remainder / 60,
+ secs = remainder % 60;
+
+ return ((hours < 10 ? "0" : "") + hours
+ + ":" + (minutes < 10 ? "0" : "") + minutes
+ + ":" + (secs < 10 ? "0" : "") + secs);
+ }
+
+ /**
+ * @param s A string representing hours, minutes, seconds, e.g. 11:23:44
+ * @return The converted number of seconds.
+ */
+ public static long fromTimeString(String s) {
+ // Handle "00:00:00.000" pattern, drop the milliseconds
+ if (s.lastIndexOf(".") != -1)
+ s = s.substring(0, s.lastIndexOf("."));
+ String[] split = s.split(":");
+ if (split.length != 3)
+ throw new IllegalArgumentException("Can't parse time string: " + s);
+ return (Long.parseLong(split[0]) * 3600) +
+ (Long.parseLong(split[1]) * 60) +
+ (Long.parseLong(split[2]));
+ }
+
+ /**
+ * @param s A string with commas.
+ * @return The same string, a newline appended after every comma.
+ */
+ public static String commaToNewline(String s) {
+ StringBuilder sb = new StringBuilder();
+ String[] split = s.split(",");
+ for (String splitString : split) {
+ sb.append(splitString).append(",").append("\n");
+ }
+ if (sb.length() > 2) {
+ sb.deleteCharAt(sb.length() - 2);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * DNS reverse name lookup.
+ *
+ * @param includeDomain true
if the whole FQDN should be returned, instead of just the first (host) part.
+ * @return The resolved host (and domain-) name, or "UNKNOWN HOST" if resolution failed.
+ */
+ public static String getLocalHostName(boolean includeDomain) {
+ try {
+ String hostname = InetAddress.getLocalHost().getHostName();
+ return includeDomain
+ ? hostname
+ : hostname.indexOf(".") != -1 ? hostname.substring(0, hostname.indexOf(".")) : hostname;
+
+ } catch (Exception ex) {
+ // Return a dummy String
+ return "UNKNOWN HOST";
+ }
+ }
+
+ /**
+ * @return The MAC hardware address of the first network interface of this host.
+ */
+ public static byte[] getFirstNetworkInterfaceHardwareAddress() {
+ try {
+ Enumeration interfaceEnumeration = NetworkInterface.getNetworkInterfaces();
+ for (NetworkInterface iface : Collections.list(interfaceEnumeration)) {
+ if (!iface.isLoopback() && iface.isUp() && iface.getHardwareAddress() != null) {
+ return iface.getHardwareAddress();
+ }
+ }
+ } catch (Exception ex) {
+ throw new RuntimeException("Could not discover first network interface hardware address");
+ }
+ throw new RuntimeException("Could not discover first network interface hardware address");
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/Namespace.java b/clinglibrary/src/main/java/org/fourthline/cling/model/Namespace.java
new file mode 100644
index 0000000000000000000000000000000000000000..dab4df1eef3a524c85612b8a2a3afee643d141d2
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/Namespace.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.Icon;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.resource.Resource;
+import org.seamless.util.URIUtil;
+
+/**
+ * Enforces path conventions for all locally offered resources (descriptors, icons, etc.)
+ *
+ * Every descriptor, icon, event callback, or action message is send to a URL. This namespace
+ * defines how the path of this URL will look like and it will build the path for a given
+ * resource.
+ *
+ *
+ * By default, the namespace is organized as follows:
+ *
+ * {@code
+ * http://host:port/dev//desc.xml
+ * http://host:port/dev//svc///desc.xml
+ * http://host:port/dev//svc///action
+ * http://host:port/dev//svc///event
+ * http://host:port/dev//svc///event/cb.xml
+ * http://host:port/dev//svc///action
+ * ...
+ * }
+ *
+ * The namespace is also used to discover and create all {@link org.fourthline.cling.model.resource.Resource}s
+ * given a {@link org.fourthline.cling.model.meta.Device}'s metadata. This procedure is typically
+ * invoked once, when the device is added to the {@link org.fourthline.cling.registry.Registry}.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class Namespace {
+
+ final private static Logger log = Logger.getLogger(Namespace.class.getName());
+
+ public static final String DEVICE = "/dev";
+ public static final String SERVICE = "/svc";
+ public static final String CONTROL = "/action";
+ public static final String EVENTS = "/event";
+ public static final String DESCRIPTOR_FILE = "/desc";
+ public static final String CALLBACK_FILE = "/cb";
+
+ final protected URI basePath;
+ final protected String decodedPath;
+
+ public Namespace() {
+ this("");
+ }
+
+ public Namespace(String basePath) {
+ this(URI.create(basePath));
+ }
+
+ public Namespace(URI basePath) {
+ this.basePath = basePath;
+ this.decodedPath = basePath.getPath();
+ }
+
+ public URI getBasePath() {
+ return basePath;
+ }
+
+ public URI getPath(Device device) {
+ return appendPathToBaseURI(getDevicePath(device));
+ }
+
+ public URI getPath(Service service) {
+ return appendPathToBaseURI(getServicePath(service));
+ }
+
+ public URI getDescriptorPath(Device device) {
+ return appendPathToBaseURI(getDevicePath(device.getRoot()) + DESCRIPTOR_FILE);
+ }
+
+ /**
+ * Performance optimization, avoids URI manipulation.
+ */
+ public String getDescriptorPathString(Device device) {
+ return decodedPath + getDevicePath(device.getRoot()) + DESCRIPTOR_FILE;
+ }
+
+ public URI getDescriptorPath(Service service) {
+ return appendPathToBaseURI(getServicePath(service) + DESCRIPTOR_FILE);
+ }
+
+ public URI getControlPath(Service service) {
+ return appendPathToBaseURI(getServicePath(service) + CONTROL);
+ }
+
+ public URI getIconPath(Icon icon) {
+ return appendPathToBaseURI(getDevicePath(icon.getDevice()) + "/" + icon.getUri().toString());
+ }
+
+ public URI getEventSubscriptionPath(Service service) {
+ return appendPathToBaseURI(getServicePath(service) + EVENTS);
+ }
+
+ public URI getEventCallbackPath(Service service) {
+ return appendPathToBaseURI(getServicePath(service) + EVENTS + CALLBACK_FILE);
+ }
+
+ /**
+ * Performance optimization, avoids URI manipulation.
+ */
+ public String getEventCallbackPathString(Service service) {
+ return decodedPath + getServicePath(service) + EVENTS + CALLBACK_FILE;
+ }
+
+ public URI prefixIfRelative(Device device, URI uri) {
+ if (!uri.isAbsolute() && !uri.getPath().startsWith("/")) {
+ return appendPathToBaseURI(getDevicePath(device) + "/" + uri);
+ }
+ return uri;
+ }
+
+ public boolean isControlPath(URI uri) {
+ return uri.toString().endsWith(Namespace.CONTROL);
+ }
+
+ public boolean isEventSubscriptionPath(URI uri) {
+ return uri.toString().endsWith(Namespace.EVENTS);
+ }
+
+ public boolean isEventCallbackPath(URI uri) {
+ return uri.toString().endsWith(Namespace.CALLBACK_FILE);
+ }
+
+ public Resource[] getResources(Device device) throws ValidationException {
+ if (!device.isRoot()) return null;
+
+ Set resources = new HashSet<>();
+ List errors = new ArrayList<>();
+
+ log.fine("Discovering local resources of device graph");
+ Resource[] discoveredResources = device.discoverResources(this);
+ for (Resource resource : discoveredResources) {
+ log.finer("Discovered: " + resource);
+ if (!resources.add(resource)) {
+ log.finer("Local resource already exists, queueing validation error");
+ errors.add(new ValidationError(
+ getClass(),
+ "resources",
+ "Local URI namespace conflict between resources of device: " + resource
+ ));
+ }
+ }
+ if (errors.size() > 0) {
+ throw new ValidationException("Validation of device graph failed, call getErrors() on exception", errors);
+ }
+ return resources.toArray(new Resource[resources.size()]);
+ }
+
+ protected URI appendPathToBaseURI(String path) {
+ try {
+ // not calling getBasePath() on purpose since we're not sure if all DalvikVMs will inline it correctly
+ return
+ new URI(
+ basePath.getScheme(),
+ null,
+ basePath.getHost(),
+ basePath.getPort(),
+ decodedPath + path,
+ null,
+ null
+ );
+ } catch (URISyntaxException e) {
+ return URI.create(basePath + path);
+ }
+ }
+
+ protected String getDevicePath(Device device) {
+ if (device.getIdentity().getUdn() == null) {
+ throw new IllegalStateException("Can't generate local URI prefix without UDN");
+ }
+ StringBuilder s = new StringBuilder();
+ s.append(DEVICE).append("/");
+
+ s.append(URIUtil.encodePathSegment(device.getIdentity().getUdn().getIdentifierString()));
+ return s.toString();
+ }
+
+ protected String getServicePath(Service service) {
+ if (service.getServiceId() == null) {
+ throw new IllegalStateException("Can't generate local URI prefix without service ID");
+ }
+ StringBuilder s = new StringBuilder();
+ s.append(SERVICE);
+ s.append("/");
+ s.append(service.getServiceId().getNamespace());
+ s.append("/");
+ s.append(service.getServiceId().getId());
+ return getDevicePath(service.getDevice()) + s.toString();
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/NetworkAddress.java b/clinglibrary/src/main/java/org/fourthline/cling/model/NetworkAddress.java
new file mode 100644
index 0000000000000000000000000000000000000000..837d6aa3a64f42f4026d15a55063f1a4bf442858
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/NetworkAddress.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+
+/**
+ * IP address, port, and optional interface hardware address (MAC) of a network service.
+ *
+ * @author Christian Bauer
+ */
+public class NetworkAddress {
+
+ protected InetAddress address;
+ protected int port;
+ protected byte[] hardwareAddress;
+
+ public NetworkAddress(InetAddress address, int port) {
+ this(address, port, null);
+ }
+
+ public NetworkAddress(InetAddress address, int port, byte[] hardwareAddress) {
+ this.address = address;
+ this.port = port;
+ this.hardwareAddress = hardwareAddress;
+ }
+
+ public InetAddress getAddress() {
+ return address;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public byte[] getHardwareAddress() {
+ return hardwareAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NetworkAddress that = (NetworkAddress) o;
+
+ if (port != that.port) return false;
+ if (!address.equals(that.address)) return false;
+ if (!Arrays.equals(hardwareAddress, that.hardwareAddress)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = address.hashCode();
+ result = 31 * result + port;
+ result = 31 * result + (hardwareAddress != null ? Arrays.hashCode(hardwareAddress) : 0);
+ return result;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ServerClientTokens.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ServerClientTokens.java
new file mode 100644
index 0000000000000000000000000000000000000000..1779bef2b0e41ab6f399c27b16adf91767d5410f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ServerClientTokens.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * The agent string of the UPnP stack in network messages, either as a server or client.
+ *
+ * Tries to detect the operating system name and version, defaults to {@link UserConstants}
+ * for product name and version.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class ServerClientTokens {
+
+ public static final String UNKNOWN_PLACEHOLDER = "UNKNOWN";
+
+ // TODO: This means we default to UDA 1.0
+ private int majorVersion = 1;
+ private int minorVersion = 0;
+
+ private String osName = System.getProperty("os.name").replaceAll("[^a-zA-Z0-9\\.\\-_]", "");
+ private String osVersion = System.getProperty("os.version").replaceAll("[^a-zA-Z0-9\\.\\-_]", "");
+ private String productName = UserConstants.PRODUCT_TOKEN_NAME;
+ private String productVersion = UserConstants.PRODUCT_TOKEN_VERSION;
+
+ public ServerClientTokens() {
+ }
+
+ public ServerClientTokens(int majorVersion, int minorVersion) {
+ this.majorVersion = majorVersion;
+ this.minorVersion = minorVersion;
+ }
+
+ public ServerClientTokens(String productName, String productVersion) {
+ this.productName = productName;
+ this.productVersion = productVersion;
+ }
+
+ public ServerClientTokens(int majorVersion, int minorVersion, String osName, String osVersion, String productName, String productVersion) {
+ this.majorVersion = majorVersion;
+ this.minorVersion = minorVersion;
+ this.osName = osName;
+ this.osVersion = osVersion;
+ this.productName = productName;
+ this.productVersion = productVersion;
+ }
+
+ public int getMajorVersion() {
+ return majorVersion;
+ }
+
+ public void setMajorVersion(int majorVersion) {
+ this.majorVersion = majorVersion;
+ }
+
+ public int getMinorVersion() {
+ return minorVersion;
+ }
+
+ public void setMinorVersion(int minorVersion) {
+ this.minorVersion = minorVersion;
+ }
+
+ public String getOsName() {
+ return osName;
+ }
+
+ public void setOsName(String osName) {
+ this.osName = osName;
+ }
+
+ public String getOsVersion() {
+ return osVersion;
+ }
+
+ public void setOsVersion(String osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public String getProductVersion() {
+ return productVersion;
+ }
+
+ public void setProductVersion(String productVersion) {
+ this.productVersion = productVersion;
+ }
+
+ @Override
+ public String toString() {
+ return getOsName()+"/"+getOsVersion()
+ + " UPnP/" + getMajorVersion() + "." + getMinorVersion() + " "
+ + getProductName() + "/" + getProductVersion();
+ }
+
+ public String getHttpToken() {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append(osName.indexOf(' ') != -1 ? osName.replace(' ', '_') : osName);
+ sb.append('/');
+ sb.append(osVersion.indexOf(' ') != -1 ? osVersion.replace(' ', '_') : osVersion);
+ sb.append(" UPnP/");
+ sb.append(majorVersion);
+ sb.append('.');
+ sb.append(minorVersion);
+ sb.append(' ');
+ sb.append(productName.indexOf(' ') != -1 ? productName.replace(' ', '_') : productName);
+ sb.append('/');
+ sb.append(productVersion.indexOf(' ') != -1 ? productVersion.replace(' ', '_') : productVersion);
+
+ return sb.toString();
+ }
+
+ public String getOsToken() {
+ return getOsName().replaceAll(" ", "_")+"/"+getOsVersion().replaceAll(" ", "_");
+ }
+
+ public String getProductToken() {
+ return getProductName().replaceAll(" ", "_") + "/" + getProductVersion().replaceAll(" ", "_");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ServerClientTokens that = (ServerClientTokens) o;
+
+ if (majorVersion != that.majorVersion) return false;
+ if (minorVersion != that.minorVersion) return false;
+ if (!osName.equals(that.osName)) return false;
+ if (!osVersion.equals(that.osVersion)) return false;
+ if (!productName.equals(that.productName)) return false;
+ if (!productVersion.equals(that.productVersion)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = majorVersion;
+ result = 31 * result + minorVersion;
+ result = 31 * result + osName.hashCode();
+ result = 31 * result + osVersion.hashCode();
+ result = 31 * result + productName.hashCode();
+ result = 31 * result + productVersion.hashCode();
+ return result;
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceManager.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0941fc5ffa70047d6560ba7190df91e9c20ea4a
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.state.StateVariableValue;
+
+import java.beans.PropertyChangeSupport;
+import java.util.Collection;
+
+/**
+ * Binds the metadata of a service to a service implementation, unified interface for accessing local services.
+ *
+ * The UPnP core will always access a local service implementation through
+ * this manager, available with {@link org.fourthline.cling.model.meta.LocalService#getManager()}:
+ *
+ *
+ * -
+ * The {@link org.fourthline.cling.model.action.ActionExecutor}s use the manager to process
+ * UPnP control invocations. It's the service manager's job to translate
+ * such an action invocation into an actual method invocation, or any other procedure
+ * that satisfies the requirements. The {@link org.fourthline.cling.model.action.ActionExecutor}
+ * works together with the manager, for example, the
+ * {@link org.fourthline.cling.model.action.MethodActionExecutor} expects that an action
+ * method can be invoked through reflection on the instance returned by the manager's
+ * {@link #getImplementation()} method. This is possible with the
+ * the {@link org.fourthline.cling.model.DefaultServiceManager}. A different service manager
+ * might require a different set of action executors, and vice versa.
+ *
+ * -
+ * The {@link org.fourthline.cling.model.state.StateVariableAccessor}s use the manager
+ * to process UPnP state variable queries and GENA eventing. It's the service manager's
+ * job to return an actual value when a state variable has to be read. The
+ * {@link org.fourthline.cling.model.state.StateVariableAccessor} works together with
+ * the service manager, for example, the {@link org.fourthline.cling.model.state.FieldStateVariableAccessor}
+ * expects that a state variable value can be read through reflection on a field, of
+ * the instance returned by {@link #getImplementation()}. This is possible with the
+ * {@link org.fourthline.cling.model.DefaultServiceManager}. A different service manager
+ * might require a different set of state variable accessors, and vice versa.
+ *
+ * -
+ * A service manager has to notify the UPnP core, and especially the GENA eventing system,
+ * whenever the state of any evented UPnP state variable changes. For new subscriptions
+ * GENA also has to read the current state of the service manually, when the subscription
+ * has been established and an initial event message has to be send to the subscriber.
+ *
+ *
+ *
+ * A service manager can implement these concerns in any way imaginable. It has to
+ * be thread-safe.
+ *
+ *
+ * @param The interface expected by the
+ * bound {@link org.fourthline.cling.model.action.ActionExecutor}s
+ * and {@link org.fourthline.cling.model.state.StateVariableAccessor}s.
+ *
+ * @author Christian Bauer
+ */
+public interface ServiceManager {
+
+ /**
+ * Use this property name when propagating change events that affect any evented UPnP
+ * state variable. This name is detected by the GENA subsystem.
+ */
+ public static final String EVENTED_STATE_VARIABLES = "_EventedStateVariables";
+
+ /**
+ * @return The metadata of the service to which this manager is assigned.
+ */
+ public LocalService getService();
+
+ /**
+ * @return An instance with the interface expected by the
+ * bound {@link org.fourthline.cling.model.action.ActionExecutor}s
+ * and {@link org.fourthline.cling.model.state.StateVariableAccessor}s.
+ */
+ public T getImplementation();
+
+ /**
+ * Double-dispatch of arbitrary commands, used by action executors and state variable accessors.
+ *
+ * The service manager will execute the given {@link org.fourthline.cling.model.Command} and it
+ * might decorate the execution, for example, by locking/unlocking access to a shared service
+ * implementation before and after the execution.
+ *
+ * @param cmd The command to execute.
+ * @throws Exception Any exception, without wrapping, as thrown by {@link org.fourthline.cling.model.Command#execute(ServiceManager)}
+ */
+ public void execute(Command cmd) throws Exception;
+
+ /**
+ * Provides the capability to monitor the service for state changes.
+ *
+ * The GENA subsystem expects that this adapter will notify its listeners whenever
+ * any evented UPnP state variable of the service has changed its state. The
+ * following change event is expected:
+ *
+ *
+ * - The property name is the constant {@link #EVENTED_STATE_VARIABLES}.
+ * - The "old value" can be
null
, only the current state has to be included.
+ * - The "new value" is a
Collection
of {@link org.fourthline.cling.model.state.StateVariableValue},
+ * representing the current state of the service after the change.
+ *
+ *
+ * The collection has to include values for all state variables, no
+ * matter what state variable was updated. Any other event is ignored (e.g. individual property
+ * changes).
+ *
+ *
+ * @return An adapter that will notify its listeners whenever any evented state variable changes.
+ */
+ public PropertyChangeSupport getPropertyChangeSupport();
+
+ /**
+ * Reading the state of a service manually.
+ *
+ * @return A Collection
of {@link org.fourthline.cling.model.state.StateVariableValue}, representing
+ * the current state of the service, that is, all evented state variable values.
+ * @throws Exception Any error that occurred when the service's state was accessed.
+ */
+ public Collection getCurrentState() throws Exception;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceReference.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceReference.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4a7a8784f669b6dc779768a70408ccc93fecd51
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ServiceReference.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import org.fourthline.cling.model.types.ServiceId;
+import org.fourthline.cling.model.types.UDN;
+
+/**
+ * Combines a {@link org.fourthline.cling.model.types.UDN} and a {@link org.fourthline.cling.model.types.ServiceId}.
+ *
+ * A service reference is useful to remember a service. For example, if a control point has accessed
+ * a service once, it can remember the service with {@link org.fourthline.cling.model.meta.Service#getReference()}.
+ * Before every action invocation, it can now resolve the reference to an actually registered service with
+ * {@link org.fourthline.cling.registry.Registry#getService(ServiceReference)}. If the registry doesn't return
+ * a service for the given reference, the service is currently not available.
+ *
+ *
+ * This simplifies implementing disconnect/reconnect behavior in a control point.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class ServiceReference {
+
+ public static final String DELIMITER = "/";
+
+ final private UDN udn;
+ final private ServiceId serviceId;
+
+ public ServiceReference(String s) {
+ String[] split = s.split("/");
+ if (split.length == 2) {
+ this.udn = UDN.valueOf(split[0]);
+ this.serviceId = ServiceId.valueOf(split[1]);
+ } else {
+ this.udn = null;
+ this.serviceId = null;
+ }
+ }
+
+ public ServiceReference(UDN udn, ServiceId serviceId) {
+ this.udn = udn;
+ this.serviceId = serviceId;
+ }
+
+ public UDN getUdn() {
+ return udn;
+ }
+
+ public ServiceId getServiceId() {
+ return serviceId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ServiceReference that = (ServiceReference) o;
+
+ if (!serviceId.equals(that.serviceId)) return false;
+ if (!udn.equals(that.udn)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = udn.hashCode();
+ result = 31 * result + serviceId.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return udn == null || serviceId == null ? "" : udn.toString() + DELIMITER + serviceId.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/UnsupportedDataException.java b/clinglibrary/src/main/java/org/fourthline/cling/model/UnsupportedDataException.java
new file mode 100644
index 0000000000000000000000000000000000000000..2dfe0cc5987b2a994d5464fa475838fc4ae7dc6c
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/UnsupportedDataException.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Thrown by processors/converters when errors occurred.
+ *
+ * This exception indicates that received data was in an invalid format and/or could
+ * not be parsed or converted. You typically can recover from this failure after
+ * catching (and logging?) the exception.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class UnsupportedDataException extends RuntimeException {
+
+ private static final long serialVersionUID = 661795454401413339L;
+
+ protected Object data;
+
+ public UnsupportedDataException(String s) {
+ super(s);
+ }
+
+ public UnsupportedDataException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public UnsupportedDataException(String s, Throwable throwable, Object data) {
+ super(s, throwable);
+ this.data = data;
+ }
+
+ public Object getData() {
+ return data;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/UserConstants.java b/clinglibrary/src/main/java/org/fourthline/cling/model/UserConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..1120238e2c2a2e8b075ea6888847f2bad9a8ee02
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/UserConstants.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Settings you can modify if you know what you are doing.
+ *
+ * @author Michael Pujos
+ */
+public class UserConstants {
+
+ public static final String PRODUCT_TOKEN_NAME = "Cling";
+
+ public static final String PRODUCT_TOKEN_VERSION = "2.0";
+
+ public static final int DEFAULT_SUBSCRIPTION_DURATION_SECONDS = 1800;
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/Validatable.java b/clinglibrary/src/main/java/org/fourthline/cling/model/Validatable.java
new file mode 100644
index 0000000000000000000000000000000000000000..768ef4f936c12b444a4a877f2d442df4523d57d8
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/Validatable.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.util.List;
+
+/**
+ * Marker for types with integrity rules that require validation.
+ *
+ * @author Christian Bauer
+ */
+public interface Validatable {
+
+ /**
+ * @return An empty List
if all rules validated properly, otherwise, the detected errors.
+ */
+ public List validate();
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationError.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationError.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9870402fb4034a15a7d3e9acd6fcaa7b34f8cf7
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationError.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+/**
+ * Representing an integrity rule validation failure.
+ *
+ * @author Christian Bauer
+ */
+public class ValidationError {
+ private Class clazz;
+ private String propertyName;
+ private String message;
+
+ public ValidationError(Class clazz, String message) {
+ this.clazz = clazz;
+ this.message = message;
+ }
+
+ public ValidationError(Class clazz, String propertyName, String message) {
+ this.clazz = clazz;
+ this.propertyName = propertyName;
+ this.message = message;
+ }
+
+ public Class getClazz() {
+ return clazz;
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + " (Class: " + getClazz().getSimpleName()
+ + ", propertyName: " + getPropertyName() + "): "
+ + message;
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationException.java b/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..8636820e9085197eea4d23bfc02b709fd42b7034
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/ValidationException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+
+import java.util.List;
+
+/**
+ * Thrown if integrity rule violations are exceptional, encapsulating the errors.
+ *
+ * @author Christian Bauer
+ */
+public class ValidationException extends Exception {
+
+ public List errors;
+
+ public ValidationException(String s) {
+ super(s);
+ }
+
+ public ValidationException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public ValidationException(String s, List errors) {
+ super(s);
+ this.errors = errors;
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/VariableValue.java b/clinglibrary/src/main/java/org/fourthline/cling/model/VariableValue.java
new file mode 100644
index 0000000000000000000000000000000000000000..540a2e445ca7b7c964ad9bee32e6bb17399581d2
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/VariableValue.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import java.util.logging.Logger;
+
+import org.fourthline.cling.model.types.Datatype;
+import org.fourthline.cling.model.types.InvalidValueException;
+
+/**
+ * Encapsulates a variable or argument value, validates and transforms it from/to a string representaion.
+ *
+ * @author Christian Bauer
+ */
+public class VariableValue {
+
+ final private static Logger log = Logger.getLogger(VariableValue.class.getName());
+
+ final private Datatype datatype;
+ final private Object value;
+
+ /**
+ * Creates and validates a variable value.
+ *
+ * If the given value is a String
, it will be converted
+ * with {@link org.fourthline.cling.model.types.Datatype#valueOf(String)}. Any
+ * other value will be checked, whether it matches the datatype and if its
+ * string representation is valid in XML documents (unicode character test).
+ *
+ *
+ * Note that for performance reasons, validation of a non-string value
+ * argument is skipped if executed on an Android runtime!
+ *
+ *
+ * @param datatype The type of the variable.
+ * @param value The value of the variable.
+ * @throws InvalidValueException If the value is invalid for the given datatype, or if
+ * its string representation is invalid in XML.
+ */
+ public VariableValue(Datatype datatype, Object value) throws InvalidValueException {
+ this.datatype = datatype;
+ this.value = value instanceof String ? datatype.valueOf((String) value) : value;
+
+ if (ModelUtil.ANDROID_RUNTIME) return; // Skipping validation on Android
+
+ // We can skip this validation because we can catch invalid values
+ // of any remote service (action invocation, event value) before, they are
+ // strings. The datatype's valueOf() will take care of that. The validations
+ // are really only used when a developer prepares input arguments for an action
+ // invocation or when a local service returns a wrong value.
+
+ // In the first case the developer will get an exception when executing the
+ // action, if his action input argument value was of the wrong type. Or,
+ // an XML processing error will occur as soon as the SOAP message is handled,
+ // if the value contained invalid characters.
+
+ // The second case indicates a bug in the local service, either metadata (state
+ // variable type) or implementation (action method return value). This will
+ // most likely be caught by the metadata/annotation binder when the service is
+ // created.
+
+ if (!getDatatype().isValid(getValue()))
+ throw new InvalidValueException("Invalid value for " + getDatatype() +": " + getValue());
+
+ logInvalidXML(toString());
+ }
+
+ public Datatype getDatatype() {
+ return datatype;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ protected void logInvalidXML(String s) {
+ // Just display warnings. PS3 Media server sends null char in DIDL-Lite
+ // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
+ int cp;
+ int i = 0;
+ while (i < s.length()) {
+ cp = s.codePointAt(i);
+ if (!(cp == 0x9 || cp == 0xA || cp == 0xD ||
+ (cp >= 0x20 && cp <= 0xD7FF) ||
+ (cp >= 0xE000 && cp <= 0xFFFD) ||
+ (cp >= 0x10000 && cp <= 0x10FFFF))) {
+ log.warning("Found invalid XML char code: " + cp);
+ }
+ i += Character.charCount(cp);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getDatatype().getString(getValue());
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/XMLUtil.java b/clinglibrary/src/main/java/org/fourthline/cling/model/XMLUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..7abe7eca52a83a52c94bacb3ecf357a78efdb1a9
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/XMLUtil.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model;
+
+import org.w3c.dom.*;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * XML handling and printing shortcuts.
+ *
+ * This class exists because Android 2.1 does not offer any way to print an org.w3c.dom.Document
,
+ * and it also doesn't implement the most trivial methods to build a DOM (although the API is provided, they
+ * fail at runtime). We might be able to remove this class once compatibility for Android 2.1 can be
+ * dropped.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class XMLUtil {
+
+ /* TODO: How it should be done (nice API, eh?)
+ public static String documentToString(Document document) throws Exception {
+ TransformerFactory transFactory = TransformerFactory.newInstance();
+ transFactory.setAttribute("indent-number", 4);
+ Transformer transformer = transFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ StringWriter out = new StringWriter();
+ transformer.transform(new DOMSource(d), new StreamResult(out));
+ return out.toString();
+ }
+ */
+
+ // TODO: Evil methods to print XML on Android 2.1 (there is no TransformerFactory)
+
+ public static String documentToString(Document document) throws Exception {
+ return documentToString(document, true);
+ }
+
+ public static String documentToString(Document document, boolean standalone) throws Exception {
+ String prol = "";
+ return prol + nodeToString(document.getDocumentElement(), new HashSet(), document.getDocumentElement().getNamespaceURI());
+ }
+
+ public static String documentToFragmentString(Document document) throws Exception {
+ return nodeToString(document.getDocumentElement(), new HashSet(), document.getDocumentElement().getNamespaceURI());
+ }
+
+ protected static String nodeToString(Node node, Set parentPrefixes, String namespaceURI) throws Exception {
+ StringBuilder b = new StringBuilder();
+
+ if (node == null) {
+ return "";
+ }
+
+ if (node instanceof Element) {
+ Element element = (Element) node;
+ b.append("<");
+ b.append(element.getNodeName());
+
+ Map thisLevelPrefixes = new HashMap<>();
+ if (element.getPrefix() != null && !parentPrefixes.contains(element.getPrefix())) {
+ thisLevelPrefixes.put(element.getPrefix(), element.getNamespaceURI());
+ }
+
+ if (element.hasAttributes()) {
+ NamedNodeMap map = element.getAttributes();
+ for (int i = 0; i < map.getLength(); i++) {
+ Node attr = map.item(i);
+ if (attr.getNodeName().startsWith("xmlns")) continue;
+ if (attr.getPrefix() != null && !parentPrefixes.contains(attr.getPrefix())) {
+ thisLevelPrefixes.put(attr.getPrefix(), element.getNamespaceURI());
+ }
+ b.append(" ");
+ b.append(attr.getNodeName());
+ b.append("=\"");
+ b.append(attr.getNodeValue());
+ b.append("\"");
+ }
+ }
+
+ if (namespaceURI != null && !thisLevelPrefixes.containsValue(namespaceURI) &&
+ !namespaceURI.equals(element.getParentNode().getNamespaceURI())) {
+ b.append(" xmlns=\"").append(namespaceURI).append("\"");
+ }
+
+ for (Map.Entry entry : thisLevelPrefixes.entrySet()) {
+ b.append(" xmlns:").append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
+ parentPrefixes.add(entry.getKey());
+ }
+
+ NodeList children = element.getChildNodes();
+ boolean hasOnlyAttributes = true;
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() != Node.ATTRIBUTE_NODE) {
+ hasOnlyAttributes = false;
+ break;
+ }
+ }
+ if (!hasOnlyAttributes) {
+ b.append(">");
+ for (int i = 0; i < children.getLength(); i++) {
+ b.append(nodeToString(children.item(i), parentPrefixes, children.item(i).getNamespaceURI()));
+ }
+ b.append("");
+ b.append(element.getNodeName());
+ b.append(">");
+ } else {
+ b.append("/>");
+ }
+
+ for (String thisLevelPrefix : thisLevelPrefixes.keySet()) {
+ parentPrefixes.remove(thisLevelPrefix);
+ }
+
+ } else if (node.getNodeValue() != null) {
+ b.append(encodeText(node.getNodeValue(), node instanceof Attr));
+ }
+
+ return b.toString();
+ }
+
+ public static String encodeText(String s) {
+ return encodeText(s, true);
+ }
+
+ public static String encodeText(String s, boolean encodeQuotes) {
+ s = s.replaceAll("&", "&");
+ s = s.replaceAll("<", "<");
+ s = s.replaceAll(">", ">");
+ if(encodeQuotes) {
+ s = s.replaceAll("'", "'");
+ s = s.replaceAll("\"", """);
+ }
+ return s;
+ }
+
+ public static Element appendNewElement(Document document, Element parent, Enum el) {
+ return appendNewElement(document, parent, el.toString());
+ }
+
+ public static Element appendNewElement(Document document, Element parent, String element) {
+ Element child = document.createElement(element);
+ parent.appendChild(child);
+ return child;
+ }
+
+ public static Element appendNewElementIfNotNull(Document document, Element parent, Enum el, Object content) {
+ return appendNewElementIfNotNull(document, parent, el, content, null);
+ }
+
+ public static Element appendNewElementIfNotNull(Document document, Element parent, Enum el, Object content, String namespace) {
+ return appendNewElementIfNotNull(document, parent, el.toString(), content, namespace);
+ }
+
+ public static Element appendNewElementIfNotNull(Document document, Element parent, String element, Object content) {
+ return appendNewElementIfNotNull(document, parent, element, content, null);
+ }
+
+ public static Element appendNewElementIfNotNull(Document document, Element parent, String element, Object content, String namespace) {
+ if (content == null) return parent;
+ return appendNewElement(document, parent, element, content, namespace);
+ }
+
+ public static Element appendNewElement(Document document, Element parent, String element, Object content) {
+ return appendNewElement(document, parent, element, content, null);
+ }
+
+ public static Element appendNewElement(Document document, Element parent, String element, Object content, String namespace) {
+ Element childElement;
+ if (namespace != null) {
+ childElement = document.createElementNS(namespace, element);
+ } else {
+ childElement = document.createElement(element);
+ }
+
+ if (content != null) {
+ // TODO: We'll have that on Android 2.2:
+ // childElement.setTextContent(content.toString());
+ // Meanwhile:
+ childElement.appendChild(document.createTextNode(content.toString()));
+ }
+
+ parent.appendChild(childElement);
+ return childElement;
+ }
+
+ // TODO: Of course, there is no Element.getTextContent() either...
+ public static String getTextContent(Node node) {
+ StringBuffer buffer = new StringBuffer();
+ NodeList childList = node.getChildNodes();
+ for (int i = 0; i < childList.getLength(); i++) {
+ Node child = childList.item(i);
+ if (child.getNodeType() != Node.TEXT_NODE)
+ continue; // skip non-text nodes
+ buffer.append(child.getNodeValue());
+ }
+ return buffer.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..13af56f5019c557d95f3d8f837507532f54b000e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.Command;
+import org.fourthline.cling.model.ServiceManager;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.ErrorCode;
+import org.fourthline.cling.model.types.InvalidValueException;
+import org.seamless.util.Exceptions;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Shared procedures for action executors based on an actual service implementation instance.
+ *
+ * @author Christian Bauer
+ */
+public abstract class AbstractActionExecutor implements ActionExecutor {
+
+ private static Logger log = Logger.getLogger(AbstractActionExecutor.class.getName());
+
+ protected Map, StateVariableAccessor> outputArgumentAccessors =
+ new HashMap<>();
+
+ protected AbstractActionExecutor() {
+ }
+
+ protected AbstractActionExecutor(Map, StateVariableAccessor> outputArgumentAccessors) {
+ this.outputArgumentAccessors = outputArgumentAccessors;
+ }
+
+ public Map, StateVariableAccessor> getOutputArgumentAccessors() {
+ return outputArgumentAccessors;
+ }
+
+ /**
+ * Obtains the service implementation instance from the {@link org.fourthline.cling.model.ServiceManager}, handles exceptions.
+ */
+ public void execute(final ActionInvocation actionInvocation) {
+
+ log.fine("Invoking on local service: " + actionInvocation);
+
+ final LocalService service = actionInvocation.getAction().getService();
+
+ try {
+
+ if (service.getManager() == null) {
+ throw new IllegalStateException("Service has no implementation factory, can't get service instance");
+ }
+
+ service.getManager().execute(new Command() {
+ public void execute(ServiceManager serviceManager) throws Exception {
+ AbstractActionExecutor.this.execute(
+ actionInvocation,
+ serviceManager.getImplementation()
+ );
+ }
+
+ @Override
+ public String toString() {
+ return "Action invocation: " + actionInvocation.getAction();
+ }
+ });
+
+ } catch (ActionException ex) {
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("ActionException thrown by service, wrapping in invocation and returning: " + ex);
+ log.log(Level.FINE, "Exception root cause: ", Exceptions.unwrap(ex));
+ }
+ actionInvocation.setFailure(ex);
+ } catch (InterruptedException ex) {
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("InterruptedException thrown by service, wrapping in invocation and returning: " + ex);
+ log.log(Level.FINE, "Exception root cause: ", Exceptions.unwrap(ex));
+ }
+ actionInvocation.setFailure(new ActionCancelledException(ex));
+ } catch (Throwable t) {
+ Throwable rootCause = Exceptions.unwrap(t);
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("Execution has thrown, wrapping root cause in ActionException and returning: " + t);
+ log.log(Level.FINE, "Exception root cause: ", rootCause);
+ }
+ actionInvocation.setFailure(
+ new ActionException(
+ ErrorCode.ACTION_FAILED,
+ (rootCause.getMessage() != null ? rootCause.getMessage() : rootCause.toString()),
+ rootCause
+ )
+ );
+ }
+ }
+
+ protected abstract void execute(ActionInvocation actionInvocation, Object serviceImpl) throws Exception;
+
+ /**
+ * Reads the output arguments after an action execution using accessors.
+ *
+ * @param action The action of which the output arguments are read.
+ * @param instance The instance on which the accessors will be invoked.
+ * @return null
if the action has no output arguments, a single instance if it has one, an
+ * Object[]
otherwise.
+ * @throws Exception
+ */
+ protected Object readOutputArgumentValues(Action action, Object instance) throws Exception {
+ Object[] results = new Object[action.getOutputArguments().length];
+ log.fine("Attempting to retrieve output argument values using accessor: " + results.length);
+
+ int i = 0;
+ for (ActionArgument outputArgument : action.getOutputArguments()) {
+ log.finer("Calling acccessor method for: " + outputArgument);
+
+ StateVariableAccessor accessor = getOutputArgumentAccessors().get(outputArgument);
+ if (accessor != null) {
+ log.fine("Calling accessor to read output argument value: " + accessor);
+ results[i++] = accessor.read(instance);
+ } else {
+ throw new IllegalStateException("No accessor bound for: " + outputArgument);
+ }
+ }
+
+ if (results.length == 1) {
+ return results[0];
+ }
+ return results.length > 0 ? results : null;
+ }
+
+ /**
+ * Sets the output argument value on the {@link org.fourthline.cling.model.action.ActionInvocation}, considers string conversion.
+ */
+ protected void setOutputArgumentValue(ActionInvocation actionInvocation, ActionArgument argument, Object result)
+ throws ActionException {
+
+ LocalService service = actionInvocation.getAction().getService();
+
+ if (result != null) {
+ try {
+ if (service.isStringConvertibleType(result)) {
+ log.fine("Result of invocation matches convertible type, setting toString() single output argument value");
+ actionInvocation.setOutput(new ActionArgumentValue(argument, result.toString()));
+ } else {
+ log.fine("Result of invocation is Object, setting single output argument value");
+ actionInvocation.setOutput(new ActionArgumentValue(argument, result));
+ }
+ } catch (InvalidValueException ex) {
+ throw new ActionException(
+ ErrorCode.ARGUMENT_VALUE_INVALID,
+ "Wrong type or invalid value for '" + argument.getName() + "': " + ex.getMessage(),
+ ex
+ );
+ }
+ } else {
+
+ log.fine("Result of invocation is null, not setting any output argument value(s)");
+ }
+
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionArgumentValue.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionArgumentValue.java
new file mode 100644
index 0000000000000000000000000000000000000000..41d412225221920dce53d29237012c4fddb7845c
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionArgumentValue.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.VariableValue;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.types.InvalidValueException;
+
+/**
+ * Represents the value of an action input or output argument.
+ *
+ * @author Christian Bauer
+ */
+public class ActionArgumentValue extends VariableValue {
+
+ final private ActionArgument argument;
+
+ public ActionArgumentValue(ActionArgument argument, Object value) throws InvalidValueException {
+ super(argument.getDatatype(), value != null && value.getClass().isEnum() ? value.toString() : value);
+ this.argument = argument;
+ }
+
+ public ActionArgument getArgument() {
+ return argument;
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionCancelledException.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionCancelledException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9774f0cfb7fee6427a227601652cc9cd9439dae
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionCancelledException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.types.ErrorCode;
+
+/**
+ * @author Christian Bauer
+ */
+public class ActionCancelledException extends ActionException {
+
+ public ActionCancelledException(InterruptedException cause) {
+ super(ErrorCode.ACTION_FAILED, "Action execution interrupted", cause);
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionException.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionException.java
new file mode 100644
index 0000000000000000000000000000000000000000..e03e9d3b68044f55b77feb0eee971c7a7a26cefd
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.types.ErrorCode;
+
+/**
+ * Thrown (or encapsulated in {@link org.fourthline.cling.model.action.ActionInvocation} when an action execution failed.
+ *
+ * @author Christian Bauer
+ */
+public class ActionException extends Exception {
+
+ private int errorCode;
+
+ public ActionException(int errorCode, String message) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public ActionException(int errorCode, String message, Throwable cause) {
+ super(message, cause);
+ this.errorCode = errorCode;
+ }
+
+ public ActionException(ErrorCode errorCode) {
+ this(errorCode.getCode(), errorCode.getDescription());
+ }
+
+ public ActionException(ErrorCode errorCode, String message) {
+ this(errorCode, message, true);
+ }
+
+ public ActionException(ErrorCode errorCode, String message, boolean concatMessages) {
+ this(errorCode.getCode(), concatMessages ? (errorCode.getDescription() + ". " + message + ".") : message);
+ }
+
+ public ActionException(ErrorCode errorCode, String message, Throwable cause) {
+ this(errorCode.getCode(), errorCode.getDescription() + ". " + message + ".", cause);
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionExecutor.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c89861f8d2529b84e84f0fd9bb21e6ec5ec4f61
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionExecutor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.meta.LocalService;
+
+/**
+ * Executes an {@link org.fourthline.cling.model.action.ActionInvocation}.
+ *
+ * @author Christian Bauer
+ */
+public interface ActionExecutor {
+
+ public void execute(final ActionInvocation actionInvocation);
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionInvocation.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionInvocation.java
new file mode 100644
index 0000000000000000000000000000000000000000..7be49085086a49f07807f1f4034c84168c8874c1
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/ActionInvocation.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.profile.ClientInfo;
+import org.fourthline.cling.model.types.InvalidValueException;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The input, output, and failure values of an action invocation.
+ *
+ * @author Christian Bauer
+ */
+public class ActionInvocation {
+
+ final protected Action action;
+ final protected ClientInfo clientInfo;
+
+ // We don't necessarily have to preserve insertion order but it's nicer if the arrays returned
+ // by the getters are reliable
+ protected Map> input = new LinkedHashMap<>();
+ protected Map> output = new LinkedHashMap<>();
+
+ protected ActionException failure = null;
+
+ public ActionInvocation(Action action) {
+ this(action, null, null, null);
+ }
+
+ public ActionInvocation(Action action,
+ ClientInfo clientInfo) {
+ this(action, null, null, clientInfo);
+ }
+
+ public ActionInvocation(Action action,
+ ActionArgumentValue[] input) {
+ this(action, input, null, null);
+ }
+
+ public ActionInvocation(Action action,
+ ActionArgumentValue[] input,
+ ClientInfo clientInfo) {
+ this(action, input, null, clientInfo);
+ }
+
+ public ActionInvocation(Action action,
+ ActionArgumentValue[] input,
+ ActionArgumentValue[] output) {
+ this(action, input, output, null);
+ }
+
+ public ActionInvocation(Action action,
+ ActionArgumentValue[] input,
+ ActionArgumentValue[] output,
+ ClientInfo clientInfo) {
+ if (action == null) {
+ throw new IllegalArgumentException("Action can not be null");
+ }
+ this.action = action;
+
+ setInput(input);
+ setOutput(output);
+
+ this.clientInfo = clientInfo;
+ }
+
+ public ActionInvocation(ActionException failure) {
+ this.action = null;
+ this.input = null;
+ this.output = null;
+ this.failure = failure;
+ this.clientInfo = null;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public ActionArgumentValue[] getInput() {
+ return input.values().toArray(new ActionArgumentValue[input.size()]);
+ }
+
+ public ActionArgumentValue getInput(String argumentName) {
+ return getInput(getInputArgument(argumentName));
+ }
+
+ public ActionArgumentValue getInput(ActionArgument argument) {
+ return input.get(argument.getName());
+ }
+
+ public Map> getInputMap() {
+ return Collections.unmodifiableMap(input);
+ }
+
+ public ActionArgumentValue[] getOutput() {
+ return output.values().toArray(new ActionArgumentValue[output.size()]);
+ }
+
+ public ActionArgumentValue getOutput(String argumentName) {
+ return getOutput(getOutputArgument(argumentName));
+ }
+
+ public Map> getOutputMap() {
+ return Collections.unmodifiableMap(output);
+ }
+
+ public ActionArgumentValue getOutput(ActionArgument argument) {
+ return output.get(argument.getName());
+ }
+
+ public void setInput(String argumentName, Object value) throws InvalidValueException {
+ setInput(new ActionArgumentValue(getInputArgument(argumentName), value));
+ }
+
+ public void setInput(ActionArgumentValue value) {
+ input.put(value.getArgument().getName(), value);
+ }
+
+ public void setInput(ActionArgumentValue[] input) {
+ if (input == null) return;
+ for (ActionArgumentValue argumentValue : input) {
+ this.input.put(argumentValue.getArgument().getName(), argumentValue);
+ }
+ }
+
+ public void setOutput(String argumentName, Object value) throws InvalidValueException {
+ setOutput(new ActionArgumentValue(getOutputArgument(argumentName), value));
+ }
+
+ public void setOutput(ActionArgumentValue value){
+ output.put(value.getArgument().getName(), value);
+ }
+
+ public void setOutput(ActionArgumentValue[] output) {
+ if (output == null) return;
+ for (ActionArgumentValue argumentValue : output) {
+ this.output.put(argumentValue.getArgument().getName(), argumentValue);
+ }
+ }
+
+ protected ActionArgument getInputArgument(String name) {
+ ActionArgument argument = getAction().getInputArgument(name);
+ if (argument == null) throw new IllegalArgumentException("Argument not found: " + name);
+ return argument;
+ }
+
+ protected ActionArgument getOutputArgument(String name) {
+ ActionArgument argument = getAction().getOutputArgument(name);
+ if (argument == null) throw new IllegalArgumentException("Argument not found: " + name);
+ return argument;
+ }
+
+ /**
+ * @return null
if execution was successful, failure details otherwise.
+ */
+ public ActionException getFailure() {
+ return failure;
+ }
+
+ public void setFailure(ActionException failure) {
+ this.failure = failure;
+ }
+
+ /**
+ * @return null
if no info was provided for a local invocation.
+ */
+ public ClientInfo getClientInfo() {
+ return clientInfo;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + getClass().getSimpleName() + ") " + getAction();
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fadfe1171d7d9ee2568714f1efc170efa810c6b
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.meta.ActionArgument;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.profile.RemoteClientInfo;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.ErrorCode;
+import org.seamless.util.Reflections;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * Invokes methods on a service implementation instance with reflection.
+ *
+ *
+ * If the method has an additional last parameter of type
+ * {@link org.fourthline.cling.model.profile.RemoteClientInfo}, the details
+ * of the control point client will be provided to the action method. You can use this
+ * to get the client's address and request headers, and to provide extra response headers.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class MethodActionExecutor extends AbstractActionExecutor {
+
+ private static Logger log = Logger.getLogger(MethodActionExecutor.class.getName());
+
+ protected Method method;
+
+ public MethodActionExecutor(Method method) {
+ this.method = method;
+ }
+
+ public MethodActionExecutor(Map, StateVariableAccessor> outputArgumentAccessors, Method method) {
+ super(outputArgumentAccessors);
+ this.method = method;
+ }
+
+ public Method getMethod() {
+ return method;
+ }
+
+ @Override
+ protected void execute(ActionInvocation actionInvocation, Object serviceImpl) throws Exception {
+
+ // Find the "real" parameters of the method we want to call, and create arguments
+ Object[] inputArgumentValues = createInputArgumentValues(actionInvocation, method);
+
+ // Simple case: no output arguments
+ if (!actionInvocation.getAction().hasOutputArguments()) {
+ log.fine("Calling local service method with no output arguments: " + method);
+ Reflections.invoke(method, serviceImpl, inputArgumentValues);
+ return;
+ }
+
+ boolean isVoid = method.getReturnType().equals(Void.TYPE);
+
+ log.fine("Calling local service method with output arguments: " + method);
+ Object result;
+ boolean isArrayResultProcessed = true;
+ if (isVoid) {
+
+ log.fine("Action method is void, calling declared accessors(s) on service instance to retrieve ouput argument(s)");
+ Reflections.invoke(method, serviceImpl, inputArgumentValues);
+ result = readOutputArgumentValues(actionInvocation.getAction(), serviceImpl);
+
+ } else if (isUseOutputArgumentAccessors(actionInvocation)) {
+
+ log.fine("Action method is not void, calling declared accessor(s) on returned instance to retrieve ouput argument(s)");
+ Object returnedInstance = Reflections.invoke(method, serviceImpl, inputArgumentValues);
+ result = readOutputArgumentValues(actionInvocation.getAction(), returnedInstance);
+
+ } else {
+
+ log.fine("Action method is not void, using returned value as (single) output argument");
+ result = Reflections.invoke(method, serviceImpl, inputArgumentValues);
+ isArrayResultProcessed = false; // We never want to process e.g. byte[] as individual variable values
+ }
+
+ ActionArgument[] outputArgs = actionInvocation.getAction().getOutputArguments();
+
+ if (isArrayResultProcessed && result instanceof Object[]) {
+ Object[] results = (Object[]) result;
+ log.fine("Accessors returned Object[], setting output argument values: " + results.length);
+ for (int i = 0; i < outputArgs.length; i++) {
+ setOutputArgumentValue(actionInvocation, outputArgs[i], results[i]);
+ }
+ } else if (outputArgs.length == 1) {
+ setOutputArgumentValue(actionInvocation, outputArgs[0], result);
+ } else {
+ throw new ActionException(
+ ErrorCode.ACTION_FAILED,
+ "Method return does not match required number of output arguments: " + outputArgs.length
+ );
+ }
+
+ }
+
+ protected boolean isUseOutputArgumentAccessors(ActionInvocation actionInvocation) {
+ for (ActionArgument argument : actionInvocation.getAction().getOutputArguments()) {
+ // If there is one output argument for which we have an accessor, all arguments need accessors
+ if (getOutputArgumentAccessors().get(argument) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected Object[] createInputArgumentValues(ActionInvocation actionInvocation, Method method) throws ActionException {
+
+ LocalService service = actionInvocation.getAction().getService();
+
+ List values = new ArrayList<>();
+ int i = 0;
+ for (ActionArgument argument : actionInvocation.getAction().getInputArguments()) {
+
+ Class methodParameterType = method.getParameterTypes()[i];
+
+ ActionArgumentValue inputValue = actionInvocation.getInput(argument);
+
+ // If it's a primitive argument, we need a value
+ if (methodParameterType.isPrimitive() && (inputValue == null || inputValue.toString().length() == 0))
+ throw new ActionException(
+ ErrorCode.ARGUMENT_VALUE_INVALID,
+ "Primitive action method argument '" + argument.getName() + "' requires input value, can't be null or empty string"
+ );
+
+ // It's not primitive and we have no value, that's fine too
+ if (inputValue == null) {
+ values.add(i++, null);
+ continue;
+ }
+
+ // If it's not null, maybe it was a string-convertible type, if so, try to instantiate it
+ String inputCallValueString = inputValue.toString();
+ // Empty string means null and we can't instantiate Enums!
+ if (inputCallValueString.length() > 0 && service.isStringConvertibleType(methodParameterType) && !methodParameterType.isEnum()) {
+ try {
+ Constructor ctor = methodParameterType.getConstructor(String.class);
+ log.finer("Creating new input argument value instance with String.class constructor of type: " + methodParameterType);
+ Object o = ctor.newInstance(inputCallValueString);
+ values.add(i++, o);
+ } catch (Exception ex) {
+ log.warning("Error preparing action method call: " + method);
+ log.warning("Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex);
+ throw new ActionException(
+ ErrorCode.ARGUMENT_VALUE_INVALID, "Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex
+ );
+ }
+ } else {
+ // Or if it wasn't, just use the value without any conversion
+ values.add(i++, inputValue.getValue());
+ }
+ }
+
+ if (method.getParameterTypes().length > 0
+ && RemoteClientInfo.class.isAssignableFrom(method.getParameterTypes()[method.getParameterTypes().length-1])) {
+ if (actionInvocation instanceof RemoteActionInvocation &&
+ ((RemoteActionInvocation)actionInvocation).getRemoteClientInfo() != null) {
+ log.finer("Providing remote client info as last action method input argument: " + method);
+ values.add(i, ((RemoteActionInvocation)actionInvocation).getRemoteClientInfo());
+ } else {
+ // Local call, no client info available
+ values.add(i, null);
+ }
+ }
+
+ return values.toArray(new Object[values.size()]);
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/QueryStateVariableExecutor.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/QueryStateVariableExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ea6b747c81a007f5c936e8a4fd892f247b8db0e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/QueryStateVariableExecutor.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.QueryStateVariableAction;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.StateVariableAccessor;
+import org.fourthline.cling.model.types.ErrorCode;
+
+/**
+ * Special executor for one action, the deprecated "query the value of the any state variable" action.
+ *
+ * @author Christian Bauer
+ */
+public class QueryStateVariableExecutor extends AbstractActionExecutor {
+
+ @Override
+ protected void execute(ActionInvocation actionInvocation, Object serviceImpl) throws Exception {
+
+ // Querying a state variable doesn't mean an actual "action" method on this instance gets invoked
+ if (actionInvocation.getAction() instanceof QueryStateVariableAction) {
+ if (!actionInvocation.getAction().getService().isSupportsQueryStateVariables()) {
+ actionInvocation.setFailure(
+ new ActionException(ErrorCode.INVALID_ACTION, "This service does not support querying state variables")
+ );
+ } else {
+ executeQueryStateVariable(actionInvocation, serviceImpl);
+ }
+ } else {
+ throw new IllegalStateException(
+ "This class can only execute QueryStateVariableAction's, not: " + actionInvocation.getAction()
+ );
+ }
+ }
+
+ protected void executeQueryStateVariable(ActionInvocation actionInvocation, Object serviceImpl) throws Exception {
+
+ LocalService service = actionInvocation.getAction().getService();
+
+ String stateVariableName = actionInvocation.getInput("varName").toString();
+ StateVariable stateVariable = service.getStateVariable(stateVariableName);
+
+ if (stateVariable == null) {
+ throw new ActionException(
+ ErrorCode.ARGUMENT_VALUE_INVALID, "No state variable found: " + stateVariableName
+ );
+ }
+
+ StateVariableAccessor accessor;
+ if ((accessor = service.getAccessor(stateVariable.getName())) == null) {
+ throw new ActionException(
+ ErrorCode.ARGUMENT_VALUE_INVALID, "No accessor for state variable, can't read state: " + stateVariableName
+ );
+ }
+
+ try {
+ setOutputArgumentValue(
+ actionInvocation,
+ actionInvocation.getAction().getOutputArgument("return"),
+ accessor.read(stateVariable, serviceImpl).toString()
+ );
+ } catch (Exception ex) {
+ throw new ActionException(ErrorCode.ACTION_FAILED, ex.getMessage());
+ }
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/action/RemoteActionInvocation.java b/clinglibrary/src/main/java/org/fourthline/cling/model/action/RemoteActionInvocation.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a031c6e4c072acc002eac823994d8ba0cea18c8
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/action/RemoteActionInvocation.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.action;
+
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.profile.RemoteClientInfo;
+
+/**
+ * An action invocation by a remote control point.
+ *
+ * @author Christian Bauer
+ */
+public class RemoteActionInvocation extends ActionInvocation {
+
+ final protected RemoteClientInfo remoteClientInfo;
+
+ public RemoteActionInvocation(Action action,
+ ActionArgumentValue[] input,
+ ActionArgumentValue[] output,
+ RemoteClientInfo remoteClientInfo) {
+ super(action, input, output, null);
+ this.remoteClientInfo = remoteClientInfo;
+ }
+
+ public RemoteActionInvocation(Action action,
+ RemoteClientInfo remoteClientInfo) {
+ super(action);
+ this.remoteClientInfo = remoteClientInfo;
+ }
+
+ public RemoteActionInvocation(ActionException failure,
+ RemoteClientInfo remoteClientInfo) {
+ super(failure);
+ this.remoteClientInfo = remoteClientInfo;
+ }
+
+ public RemoteClientInfo getRemoteClientInfo() {
+ return remoteClientInfo;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/gena/CancelReason.java b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/CancelReason.java
new file mode 100644
index 0000000000000000000000000000000000000000..34200cac90a0498b5a7c90efe943c2bbbbdcfa75
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/CancelReason.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.gena;
+
+/**
+ * The reason why a {@link org.fourthline.cling.model.gena.GENASubscription} has ended unexpectedly.
+ *
+ * @author Christian Bauer
+ */
+public enum CancelReason {
+
+ RENEWAL_FAILED,
+ DEVICE_WAS_REMOVED,
+ UNSUBSCRIBE_FAILED,
+ EXPIRED
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/gena/GENASubscription.java b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/GENASubscription.java
new file mode 100644
index 0000000000000000000000000000000000000000..ade3ea42e44b535335911ef546dd4806a5e3a24f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/GENASubscription.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.gena;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.fourthline.cling.model.UserConstants;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
+
+/**
+ * An established subscription, with identifer, expiration duration, sequence handling, and state variable values.
+ *
+ * For every subscription, no matter if it's an incoming subscription to a local service,
+ * or a local control point subscribing to a remote servce, an instance is maintained by
+ * the {@link org.fourthline.cling.registry.Registry}.
+ *
+ *
+ * @author Christian Bauer
+ */
+public abstract class GENASubscription {
+
+ protected S service;
+ protected String subscriptionId;
+ protected int requestedDurationSeconds = UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS;
+ protected int actualDurationSeconds;
+ protected UnsignedIntegerFourBytes currentSequence;
+ protected Map> currentValues = new LinkedHashMap<>();
+
+ /**
+ * Defaults to {@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS}.
+ */
+ protected GENASubscription(S service) {
+ this.service = service;
+ }
+
+ public GENASubscription(S service, int requestedDurationSeconds) {
+ this(service);
+ this.requestedDurationSeconds = requestedDurationSeconds;
+ }
+
+ synchronized public S getService() {
+ return service;
+ }
+
+ synchronized public String getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ synchronized public void setSubscriptionId(String subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ }
+
+ synchronized public int getRequestedDurationSeconds() {
+ return requestedDurationSeconds;
+ }
+
+ synchronized public int getActualDurationSeconds() {
+ return actualDurationSeconds;
+ }
+
+ synchronized public void setActualSubscriptionDurationSeconds(int seconds) {
+ this.actualDurationSeconds = seconds;
+ }
+
+ synchronized public UnsignedIntegerFourBytes getCurrentSequence() {
+ return currentSequence;
+ }
+
+ synchronized public Map> getCurrentValues() {
+ return currentValues;
+ }
+
+ public abstract void established();
+ public abstract void eventReceived();
+
+ @Override
+ public String toString() {
+ return "(GENASubscription, SID: " + getSubscriptionId() + ", SEQUENCE: " + getCurrentSequence() + ")";
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b8a811530f9716894c06c44f4fd2347a96993ea
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.gena;
+
+import org.fourthline.cling.model.ServiceManager;
+import org.fourthline.cling.model.UserConstants;
+import org.fourthline.cling.model.message.header.SubscriptionIdHeader;
+import org.fourthline.cling.model.meta.LocalService;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
+import org.seamless.util.Exceptions;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An incoming subscription to a local service.
+ *
+ * Uses the {@link org.fourthline.cling.model.ServiceManager} to read the initial state of
+ * the {@link org.fourthline.cling.model.meta.LocalService} on instantation. Typically, the
+ * {@link #registerOnService()} method is called next, and from this point forward all
+ * {@link org.fourthline.cling.model.ServiceManager#EVENTED_STATE_VARIABLES} property change
+ * events are detected by this subscription. After moderation of state variable values
+ * (frequency and range of changes), the {@link #eventReceived()} method is called.
+ * Delivery of the event message to the subscriber is not part of this class, but the
+ * implementor of {@link #eventReceived()}.
+ *
+ *
+ * @author Christian Bauer
+ */
+public abstract class LocalGENASubscription extends GENASubscription implements PropertyChangeListener {
+
+ private static Logger log = Logger.getLogger(LocalGENASubscription.class.getName());
+
+ final List callbackURLs;
+
+ // Moderation history
+ final Map lastSentTimestamp = new HashMap<>();
+ final Map lastSentNumericValue = new HashMap<>();
+
+ protected LocalGENASubscription(LocalService service, List callbackURLs) throws Exception {
+ super(service);
+ this.callbackURLs = callbackURLs;
+ }
+
+ public LocalGENASubscription(LocalService service,
+ Integer requestedDurationSeconds, List callbackURLs) throws Exception {
+ super(service);
+
+ setSubscriptionDuration(requestedDurationSeconds);
+
+ log.fine("Reading initial state of local service at subscription time");
+ long currentTime = new Date().getTime();
+ this.currentValues.clear();
+
+ Collection values = getService().getManager().getCurrentState();
+
+ log.finer("Got evented state variable values: " + values.size());
+
+ for (StateVariableValue value : values) {
+ this.currentValues.put(value.getStateVariable().getName(), value);
+
+ if (log.isLoggable(Level.FINEST)) {
+ log.finer("Read state variable value '" + value.getStateVariable().getName() + "': " + value.toString());
+ }
+
+ // Preserve "last sent" state for future moderation
+ lastSentTimestamp.put(value.getStateVariable().getName(), currentTime);
+ if (value.getStateVariable().isModeratedNumericType()) {
+ lastSentNumericValue.put(value.getStateVariable().getName(), Long.valueOf(value.toString()));
+ }
+ }
+
+ this.subscriptionId = SubscriptionIdHeader.PREFIX + UUID.randomUUID();
+ this.currentSequence = new UnsignedIntegerFourBytes(0);
+ this.callbackURLs = callbackURLs;
+ }
+
+ synchronized public List getCallbackURLs() {
+ return callbackURLs;
+ }
+
+ /**
+ * Adds a property change listener on the {@link org.fourthline.cling.model.ServiceManager}.
+ */
+ synchronized public void registerOnService() {
+ getService().getManager().getPropertyChangeSupport().addPropertyChangeListener(this);
+ }
+
+ synchronized public void establish() {
+ established();
+ }
+
+ /**
+ * Removes a property change listener on the {@link org.fourthline.cling.model.ServiceManager}.
+ */
+ synchronized public void end(CancelReason reason) {
+ try {
+ getService().getManager().getPropertyChangeSupport().removePropertyChangeListener(this);
+ } catch (Exception ex) {
+ log.warning("Removal of local service property change listener failed: " + Exceptions.unwrap(ex));
+ }
+ ended(reason);
+ }
+
+ /**
+ * Moderates {@link org.fourthline.cling.model.ServiceManager#EVENTED_STATE_VARIABLES} events and state variable
+ * values, calls {@link #eventReceived()}.
+ */
+ synchronized public void propertyChange(PropertyChangeEvent e) {
+ if (!e.getPropertyName().equals(ServiceManager.EVENTED_STATE_VARIABLES)) return;
+
+ log.fine("Eventing triggered, getting state for subscription: " + getSubscriptionId());
+
+ long currentTime = new Date().getTime();
+
+ Collection newValues = (Collection) e.getNewValue();
+ Set excludedVariables = moderateStateVariables(currentTime, newValues);
+
+ currentValues.clear();
+ for (StateVariableValue newValue : newValues) {
+ String name = newValue.getStateVariable().getName();
+ if (!excludedVariables.contains(name)) {
+ log.fine("Adding state variable value to current values of event: " + newValue.getStateVariable() + " = " + newValue);
+ currentValues.put(newValue.getStateVariable().getName(), newValue);
+
+ // Preserve "last sent" state for future moderation
+ lastSentTimestamp.put(name, currentTime);
+ if (newValue.getStateVariable().isModeratedNumericType()) {
+ lastSentNumericValue.put(name, Long.valueOf(newValue.toString()));
+ }
+ }
+ }
+
+ if (currentValues.size() > 0) {
+ log.fine("Propagating new state variable values to subscription: " + this);
+ // TODO: I'm not happy with this design, this dispatches to a separate thread which _then_
+ // is supposed to lock and read the values off this instance. That obviously doesn't work
+ // so it's currently a hack in SendingEvent.java
+ eventReceived();
+ } else {
+ log.fine("No state variable values for event (all moderated out?), not triggering event");
+ }
+ }
+
+ /**
+ * Checks whether a state variable is moderated, and if this change is within the maximum rate and range limits.
+ *
+ * @param currentTime The current unix time.
+ * @param values The state variable values to moderate.
+ * @return A collection of state variable values that although they might have changed, are excluded from the event.
+ */
+ synchronized protected Set moderateStateVariables(long currentTime, Collection values) {
+
+ Set excludedVariables = new HashSet<>();
+
+ // Moderate event variables that have a maximum rate or minimum delta
+ for (StateVariableValue stateVariableValue : values) {
+
+ StateVariable stateVariable = stateVariableValue.getStateVariable();
+ String stateVariableName = stateVariableValue.getStateVariable().getName();
+
+ if (stateVariable.getEventDetails().getEventMaximumRateMilliseconds() == 0 &&
+ stateVariable.getEventDetails().getEventMinimumDelta() == 0) {
+ log.finer("Variable is not moderated: " + stateVariable);
+ continue;
+ }
+
+ // That should actually never happen, because we always "send" it as the initial state/event
+ if (!lastSentTimestamp.containsKey(stateVariableName)) {
+ log.finer("Variable is moderated but was never sent before: " + stateVariable);
+ continue;
+ }
+
+ if (stateVariable.getEventDetails().getEventMaximumRateMilliseconds() > 0) {
+ long timestampLastSent = lastSentTimestamp.get(stateVariableName);
+ long timestampNextSend = timestampLastSent + (stateVariable.getEventDetails().getEventMaximumRateMilliseconds());
+ if (currentTime <= timestampNextSend) {
+ log.finer("Excluding state variable with maximum rate: " + stateVariable);
+ excludedVariables.add(stateVariableName);
+ continue;
+ }
+ }
+
+ if (stateVariable.isModeratedNumericType() && lastSentNumericValue.get(stateVariableName) != null) {
+
+ long oldValue = Long.valueOf(lastSentNumericValue.get(stateVariableName));
+ long newValue = Long.valueOf(stateVariableValue.toString());
+ long minDelta = stateVariable.getEventDetails().getEventMinimumDelta();
+
+ if (newValue > oldValue && newValue - oldValue < minDelta) {
+ log.finer("Excluding state variable with minimum delta: " + stateVariable);
+ excludedVariables.add(stateVariableName);
+ continue;
+ }
+
+ if (newValue < oldValue && oldValue - newValue < minDelta) {
+ log.finer("Excluding state variable with minimum delta: " + stateVariable);
+ excludedVariables.add(stateVariableName);
+ }
+ }
+
+ }
+ return excludedVariables;
+ }
+
+ synchronized public void incrementSequence() {
+ this.currentSequence.increment(true);
+ }
+
+ /**
+ * @param requestedDurationSeconds If null
defaults to
+ * {@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS}
+ */
+ synchronized public void setSubscriptionDuration(Integer requestedDurationSeconds) {
+ this.requestedDurationSeconds =
+ requestedDurationSeconds == null
+ ? UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS
+ : requestedDurationSeconds;
+
+ setActualSubscriptionDurationSeconds(this.requestedDurationSeconds);
+ }
+
+ public abstract void ended(CancelReason reason);
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/gena/RemoteGENASubscription.java b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/RemoteGENASubscription.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a5520dad50c901c770ecd2cedfa4d85cc092aad
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/gena/RemoteGENASubscription.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.gena;
+
+import org.fourthline.cling.model.Location;
+import org.fourthline.cling.model.Namespace;
+import org.fourthline.cling.model.NetworkAddress;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.RemoteService;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
+import org.fourthline.cling.model.UnsupportedDataException;
+
+import java.beans.PropertyChangeSupport;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An outgoing subscription to a remote service.
+ *
+ * Once established, calls its {@link #eventReceived()} method whenever an event has
+ * been received from the remote service.
+ *
+ *
+ * @author Christian Bauer
+ */
+public abstract class RemoteGENASubscription extends GENASubscription {
+
+ protected PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
+
+ protected RemoteGENASubscription(RemoteService service,
+ int requestedDurationSeconds) {
+ super(service, requestedDurationSeconds);
+ }
+
+ synchronized public URL getEventSubscriptionURL() {
+ return getService().getDevice().normalizeURI(
+ getService().getEventSubscriptionURI()
+ );
+ }
+
+ synchronized public List getEventCallbackURLs(List activeStreamServers, Namespace namespace) {
+ List callbackURLs = new ArrayList<>();
+ for (NetworkAddress activeStreamServer : activeStreamServers) {
+ callbackURLs.add(
+ new Location(
+ activeStreamServer,
+ namespace.getEventCallbackPathString(getService())
+ ).getURL());
+ }
+ return callbackURLs;
+ }
+
+ /* The following four methods should always be called in an independent thread, not within the
+ message receiving thread. Otherwise the user who implements the abstract delegate methods can
+ block the network communication.
+ */
+
+ synchronized public void establish() {
+ established();
+ }
+
+ synchronized public void fail(UpnpResponse responseStatus) {
+ failed(responseStatus);
+ }
+
+ synchronized public void end(CancelReason reason, UpnpResponse response) {
+ ended(reason, response);
+ }
+
+ synchronized public void receive(UnsignedIntegerFourBytes sequence, Collection newValues) {
+
+ if (this.currentSequence != null) {
+
+ // TODO: Handle rollover to 1!
+ if (this.currentSequence.getValue().equals(this.currentSequence.getBits().getMaxValue()) && sequence.getValue() == 1) {
+ System.err.println("TODO: HANDLE ROLLOVER");
+ return;
+ }
+
+ if (this.currentSequence.getValue() >= sequence.getValue()) {
+ return;
+ }
+
+ int difference;
+ long expectedValue = currentSequence.getValue() + 1;
+ if ((difference = (int) (sequence.getValue() - expectedValue)) != 0) {
+ eventsMissed(difference);
+ }
+
+ }
+
+ this.currentSequence = sequence;
+
+ for (StateVariableValue newValue : newValues) {
+ currentValues.put(newValue.getStateVariable().getName(), newValue);
+ }
+
+ eventReceived();
+ }
+
+ public abstract void invalidMessage(UnsupportedDataException ex);
+
+ public abstract void failed(UpnpResponse responseStatus);
+
+ public abstract void ended(CancelReason reason, UpnpResponse responseStatus);
+
+ public abstract void eventsMissed(int numberOfMissedEvents);
+
+ @Override
+ public String toString() {
+ return "(SID: " + getSubscriptionId() + ") " + getService();
+ }
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/Connection.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/Connection.java
new file mode 100644
index 0000000000000000000000000000000000000000..7415edf108a911a26dcbbcf4e3caefa01d28adad
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/Connection.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import java.net.InetAddress;
+
+/**
+ * An API for the Cling protocol layer to access some transport layer details.
+ *
+ * @author Christian Bauer
+ */
+public interface Connection {
+
+ boolean isOpen();
+
+ InetAddress getRemoteAddress();
+
+ InetAddress getLocalAddress();
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/IncomingDatagramMessage.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/IncomingDatagramMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d0ccf5192c29f6e6bd4a36da944fa8ad1fd882f
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/IncomingDatagramMessage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import java.net.InetAddress;
+
+/**
+ * A received UDP datagram request or response message, with source address and port.
+ *
+ * Additionally, holds a local address that is reachable from the source
+ * address (in the same subnet):
+ *
+ *
+ * - When an M-SEARCH is received, we send a LOCATION header back with a
+ * reachable (by the remote control point) local address.
+ * - When a NOTIFY discovery message (can be a search response) is received we
+ need to memorize on which local address it was received, so that the we can
+ later give the remote device a reachable (from its point of view) local
+ GENA callback address.
+ *
+ *
+ * @author Christian Bauer
+ */
+public class IncomingDatagramMessage extends UpnpMessage {
+
+ private InetAddress sourceAddress;
+ private int sourcePort;
+ private InetAddress localAddress;
+
+ public IncomingDatagramMessage(O operation, InetAddress sourceAddress, int sourcePort, InetAddress localAddress) {
+ super(operation);
+ this.sourceAddress = sourceAddress;
+ this.sourcePort = sourcePort;
+ this.localAddress = localAddress;
+ }
+
+ protected IncomingDatagramMessage(IncomingDatagramMessage source) {
+ super(source);
+ this.sourceAddress = source.getSourceAddress();
+ this.sourcePort = source.getSourcePort();
+ this.localAddress = source.getLocalAddress();
+ }
+
+ public InetAddress getSourceAddress() {
+ return sourceAddress;
+ }
+
+ public int getSourcePort() {
+ return sourcePort;
+ }
+
+ public InetAddress getLocalAddress() {
+ return localAddress;
+ }
+
+}
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/OutgoingDatagramMessage.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/OutgoingDatagramMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff5f154ef9a6af8eb99c06c8b93da9c74e7527d5
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/OutgoingDatagramMessage.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import java.net.InetAddress;
+
+/**
+ * A UDP datagram request or response message for sending, with destination address and port.
+ *
+ * @author Christian Bauer
+ */
+public abstract class OutgoingDatagramMessage extends UpnpMessage {
+
+ private InetAddress destinationAddress;
+ private int destinationPort;
+ // For performance reasons, headers of this message are not normalized
+ private UpnpHeaders headers = new UpnpHeaders(false);
+
+ protected OutgoingDatagramMessage(O operation, InetAddress destinationAddress, int destinationPort) {
+ super(operation);
+ this.destinationAddress = destinationAddress;
+ this.destinationPort = destinationPort;
+ }
+
+ protected OutgoingDatagramMessage(O operation, BodyType bodyType, Object body, InetAddress destinationAddress, int destinationPort) {
+ super(operation, bodyType, body);
+ this.destinationAddress = destinationAddress;
+ this.destinationPort = destinationPort;
+ }
+
+ public InetAddress getDestinationAddress() {
+ return destinationAddress;
+ }
+
+ public int getDestinationPort() {
+ return destinationPort;
+ }
+
+ @Override
+ public UpnpHeaders getHeaders() {
+ return this.headers;
+ }
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamRequestMessage.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamRequestMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..237462dd07cc67a82bcc14ccc598be95211f4761
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamRequestMessage.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * A TCP (HTTP) stream request message.
+ *
+ * @author Christian Bauer
+ */
+public class StreamRequestMessage extends UpnpMessage {
+
+ protected Connection connection;
+
+ public StreamRequestMessage(StreamRequestMessage source) {
+ super(source);
+ this.connection = source.getConnection();
+ }
+
+ public StreamRequestMessage(UpnpRequest operation) {
+ super(operation);
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URI uri) {
+ super(new UpnpRequest(method, uri));
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URL url) {
+ super(new UpnpRequest(method, url));
+ }
+
+ public StreamRequestMessage(UpnpRequest operation, String body) {
+ super(operation, BodyType.STRING, body);
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URI uri, String body) {
+ super(new UpnpRequest(method, uri), BodyType.STRING, body);
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URL url, String body) {
+ super(new UpnpRequest(method, url), BodyType.STRING, body);
+ }
+
+
+ public StreamRequestMessage(UpnpRequest operation, byte[] body) {
+ super(operation, BodyType.BYTES, body);
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URI uri, byte[] body) {
+ super(new UpnpRequest(method, uri), BodyType.BYTES, body);
+ }
+
+ public StreamRequestMessage(UpnpRequest.Method method, URL url, byte[] body) {
+ super(new UpnpRequest(method, url), BodyType.BYTES, body);
+ }
+
+ public URI getUri() {
+ return getOperation().getURI();
+ }
+
+ public void setUri(URI uri) {
+ getOperation().setUri(uri);
+ }
+
+ public void setConnection(Connection connection) {
+ this.connection = connection;
+ }
+
+ public Connection getConnection() {
+ return connection;
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamResponseMessage.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamResponseMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..740b552d66a8316a08961d6844c2f2e58dd0d359
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/StreamResponseMessage.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import org.fourthline.cling.model.message.header.ContentTypeHeader;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+import org.seamless.util.MimeType;
+
+/**
+ * A TCP (HTTP) stream response message.
+ *
+ * @author Christian Bauer
+ */
+public class StreamResponseMessage extends UpnpMessage {
+
+ public StreamResponseMessage(StreamResponseMessage source) {
+ super(source);
+ }
+
+ public StreamResponseMessage(UpnpResponse.Status status) {
+ super(new UpnpResponse(status));
+ }
+
+ public StreamResponseMessage(UpnpResponse operation) {
+ super(operation);
+ }
+
+
+ public StreamResponseMessage(UpnpResponse operation, String body) {
+ super(operation, BodyType.STRING, body);
+ }
+
+ public StreamResponseMessage(String body) {
+ super(new UpnpResponse(UpnpResponse.Status.OK),BodyType.STRING, body);
+ }
+
+
+ public StreamResponseMessage(UpnpResponse operation, byte[] body) {
+ super(operation, BodyType.BYTES, body);
+ }
+
+ public StreamResponseMessage(byte[] body) {
+ super(new UpnpResponse(UpnpResponse.Status.OK),BodyType.BYTES, body);
+ }
+
+
+ public StreamResponseMessage(String body, ContentTypeHeader contentType) {
+ this(body);
+ getHeaders().add(UpnpHeader.Type.CONTENT_TYPE, contentType);
+ }
+
+ public StreamResponseMessage(String body, MimeType mimeType) {
+ this(body, new ContentTypeHeader(mimeType));
+ }
+
+ public StreamResponseMessage(byte[] body, ContentTypeHeader contentType) {
+ this(body);
+ getHeaders().add(UpnpHeader.Type.CONTENT_TYPE, contentType);
+ }
+
+ public StreamResponseMessage(byte[] body, MimeType mimeType) {
+ this(body, new ContentTypeHeader(mimeType));
+ }
+
+}
\ No newline at end of file
diff --git a/clinglibrary/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java b/clinglibrary/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java
new file mode 100644
index 0000000000000000000000000000000000000000..953c938f8c2a05da6182eedf291ce7c0d5f9a03e
--- /dev/null
+++ b/clinglibrary/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 4th Line GmbH, Switzerland
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * Lesser General Public License Version 2 or later ("LGPL") or the
+ * Common Development and Distribution License Version 1 or later
+ * ("CDDL") (collectively, the "License"). You may not use this file
+ * except in compliance with the License. See LICENSE.txt for more
+ * information.
+ *
+ * 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.
+ */
+
+package org.fourthline.cling.model.message;
+
+import org.seamless.http.Headers;
+import org.fourthline.cling.model.message.header.UpnpHeader;
+
+import java.io.ByteArrayInputStream;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Provides UPnP header API in addition to plain multi-map HTTP header access.
+ *
+ * @author Christian Bauer
+ */
+public class UpnpHeaders extends Headers {
+
+ private static final Logger log = Logger.getLogger(UpnpHeaders.class.getName());
+
+ protected Map> parsedHeaders;
+
+ public UpnpHeaders() {
+ }
+
+ public UpnpHeaders(Map> headers) {
+ super(headers);
+ }
+
+ public UpnpHeaders(ByteArrayInputStream inputStream) {
+ super(inputStream);
+ }
+
+ public UpnpHeaders(boolean normalizeHeaders) {
+ super(normalizeHeaders);
+ }
+
+ protected void parseHeaders() {
+ // This runs as late as possible and only when necessary (getter called and map is dirty)
+ parsedHeaders = new LinkedHashMap<>();
+ if (log.isLoggable(Level.FINE))
+ log.fine("Parsing all HTTP headers for known UPnP headers: " + size());
+ for (Entry> entry : entrySet()) {
+
+ if (entry.getKey() == null) continue; // Oh yes, the JDK has 'null' HTTP headers
+
+ UpnpHeader.Type type = UpnpHeader.Type.getByHttpName(entry.getKey());
+ if (type == null) {
+ if (log.isLoggable(Level.FINE))
+ log.fine("Ignoring non-UPNP HTTP header: " + entry.getKey());
+ continue;
+ }
+
+ for (String value : entry.getValue()) {
+ UpnpHeader upnpHeader = UpnpHeader.newInstance(type, value);
+ if (upnpHeader == null || upnpHeader.getValue() == null) {
+ if (log.isLoggable(Level.FINE))
+ log.fine(
+ "Ignoring known but irrelevant header (value violates the UDA specification?) '"
+ + type.getHttpName()
+ + "': "
+ + value
+ );
+ } else {
+ addParsedValue(type, upnpHeader);
+ }
+ }
+ }
+ }
+
+ protected void addParsedValue(UpnpHeader.Type type, UpnpHeader value) {
+ if (log.isLoggable(Level.FINE))
+ log.fine("Adding parsed header: " + value);
+ List list = parsedHeaders.get(type);
+ if (list == null) {
+ list = new LinkedList<>();
+ parsedHeaders.put(type, list);
+ }
+ list.add(value);
+ }
+
+ @Override
+ public List put(String key, List values) {
+ parsedHeaders = null;
+ return super.put(key, values);
+ }
+
+ @Override
+ public void add(String key, String value) {
+ parsedHeaders = null;
+ super.add(key, value);
+ }
+
+ @Override
+ public List remove(Object key) {
+ parsedHeaders = null;
+ return super.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ parsedHeaders = null;
+ super.clear();
+ }
+
+ public boolean containsKey(UpnpHeader.Type type) {
+ if (parsedHeaders == null) parseHeaders();
+ return parsedHeaders.containsKey(type);
+ }
+
+ public List