1 /*
2 * Copyright (C) 2013 4th Line GmbH, Switzerland
3 *
4 * The contents of this file are subject to the terms of either the GNU
5 * Lesser General Public License Version 2 or later ("LGPL") or the
6 * Common Development and Distribution License Version 1 or later
7 * ("CDDL") (collectively, the "License"). You may not use this file
8 * except in compliance with the License. See LICENSE.txt for more
9 * information.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14 */
15 package example.controlpoint;
16
17 import example.binarylight.BinaryLightSampleData;
18 import example.binarylight.SwitchPower;
19 import org.fourthline.cling.controlpoint.SubscriptionCallback;
20 import org.fourthline.cling.mock.MockRouter;
21 import org.fourthline.cling.mock.MockUpnpService;
22 import org.fourthline.cling.model.UnsupportedDataException;
23 import org.fourthline.cling.model.gena.CancelReason;
24 import org.fourthline.cling.model.gena.GENASubscription;
25 import org.fourthline.cling.model.gena.RemoteGENASubscription;
26 import org.fourthline.cling.model.message.StreamResponseMessage;
27 import org.fourthline.cling.model.message.UpnpResponse;
28 import org.fourthline.cling.model.message.header.SubscriptionIdHeader;
29 import org.fourthline.cling.model.message.header.TimeoutHeader;
30 import org.fourthline.cling.model.message.header.UpnpHeader;
31 import org.fourthline.cling.model.meta.LocalDevice;
32 import org.fourthline.cling.model.meta.LocalService;
33 import org.fourthline.cling.model.state.StateVariableValue;
34 import org.fourthline.cling.model.types.BooleanDatatype;
35 import org.fourthline.cling.model.types.Datatype;
36 import org.seamless.util.Reflections;
37 import org.testng.annotations.Test;
38
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.Map;
42
43 import static org.testng.Assert.*;
44
45 /**
46 * Receiving events from services
47 * <p>
48 * The UPnP specification defines a general event notification architecture (GENA) which is based
49 * on a publish/subscribe paradigm. Your control point subscribes with a service in order to receive
50 * events. When the service state changes, an event message will be delivered to the callback
51 * of your control point. Subscriptions are periodically refreshed until you unsubscribe from
52 * the service. If you do not unsubscribe and if a refresh of the subscription fails, maybe
53 * because the control point was turned off without proper shutdown, the subscription will
54 * timeout on the publishing service's side.
55 * </p>
56 * <p>
57 * This is an example subscription on a service that sends events for a state variable named
58 * <code>Status</code> (e.g. the previously shown <a href="#section.SwitchPower">SwitchPower</a>
59 * service). The subscription's refresh and timeout period is 600 seconds:
60 * </p>
61 * <a class="citation" href="javacode://this#subscriptionLifecycle" style="include: SUBSCRIBE; exclude: EXC1, EXC2, EXC3, EXC4, EXC5;"/>
62 * <p>
63 * The <code>SubscriptionCallback</code> offers the methods <code>failed()</code>,
64 * <code>established()</code>, and <code>ended()</code> which are called during a subscription's lifecycle.
65 * When a subscription ends you will be notified with a <code>CancelReason</code> whenever the termination
66 * of the subscription was irregular. See the Javadoc of these methods for more details.
67 * </p>
68 * <p>
69 * Every event message from the service will be passed to the <code>eventReceived()</code> method,
70 * and every message will carry a sequence number. You can access the changed state variable values
71 * in this method, note that only state variables which changed are included in the event messages.
72 * A special event message called the "initial event" will be send by the service once, when you
73 * subscribe. This message contains values for <em>all</em> evented state variables of the service;
74 * you'll receive an initial snapshot of the state of the service at subscription time.
75 * </p>
76 * <p>
77 * Whenever the receiving UPnP stack detects an event message that is out of sequence, e.g. because
78 * some messages were lost during transport, the <code>eventsMissed()</code> method will be called
79 * before you receive the event. You then decide if missing events is important for the correct
80 * behavior of your application, or if you can silently ignore it and continue processing events
81 * with non-consecutive sequence numbers.
82 * </p>
83 * <p>
84 * You can optionally override the <code>invalidMessage()</code> method and react to message parsing
85 * errors, if your subscription is with a remote service. Most of the time all you can do here is
86 * log or report an error to developers, so they can work around the broken remote service (UPnP
87 * interoperability is frequently very poor).
88 * </p>
89 * <p>
90 * You end a subscription regularly by calling <code>callback.end()</code>, which will unsubscribe
91 * your control point from the service.
92 * </p>
93 */
94 public class EventSubscriptionTest {
95
96 @Test
97 public void subscriptionLifecycle() throws Exception {
98
99 MockUpnpService upnpService = createMockUpnpService();
100
101 final List<Boolean> testAssertions = new ArrayList<>();
102
103 // Register local device and its service
104 LocalDevice device = BinaryLightSampleData.createDevice(SwitchPower.class);
105 upnpService.getRegistry().addDevice(device);
106
107 LocalService service = device.getServices()[0];
108
109 SubscriptionCallback callback = new SubscriptionCallback(service, 600) { // DOC: SUBSCRIBE
110
111 @Override
112 public void established(GENASubscription sub) {
113 System.out.println("Established: " + sub.getSubscriptionId());
114 testAssertions.add(true); // DOC: EXC2
115 }
116
117 @Override
118 protected void failed(GENASubscription subscription,
119 UpnpResponse responseStatus,
120 Exception exception,
121 String defaultMsg) {
122 System.err.println(defaultMsg);
123 testAssertions.add(false); // DOC: EXC1
124 }
125
126 @Override
127 public void ended(GENASubscription sub,
128 CancelReason reason,
129 UpnpResponse response) {
130 assertNull(reason);
131 assertNotNull(sub); // DOC: EXC3
132 assertNull(response);
133 testAssertions.add(true); // DOC: EXC3
134 }
135
136 @Override
137 public void eventReceived(GENASubscription sub) {
138
139 System.out.println("Event: " + sub.getCurrentSequence().getValue());
140
141 Map<String, StateVariableValue> values = sub.getCurrentValues();
142 StateVariableValue status = values.get("Status");
143
144 assertEquals(status.getDatatype().getClass(), BooleanDatatype.class);
145 assertEquals(status.getDatatype().getBuiltin(), Datatype.Builtin.BOOLEAN);
146
147 System.out.println("Status is: " + status.toString());
148
149 if (sub.getCurrentSequence().getValue() == 0) { // DOC: EXC4
150 assertEquals(sub.getCurrentValues().get("Status").toString(), "0");
151 testAssertions.add(true);
152 } else if (sub.getCurrentSequence().getValue() == 1) {
153 assertEquals(sub.getCurrentValues().get("Status").toString(), "1");
154 testAssertions.add(true);
155 } else {
156 testAssertions.add(false);
157 } // DOC: EXC4
158 }
159
160 @Override
161 public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
162 System.out.println("Missed events: " + numberOfMissedEvents);
163 testAssertions.add(false); // DOC: EXC5
164 }
165
166 @Override
167 protected void invalidMessage(RemoteGENASubscription sub,
168 UnsupportedDataException ex) {
169 // Log/send an error report?
170 }
171 };
172
173 upnpService.getControlPoint().execute(callback); // DOC: SUBSCRIBE
174
175 // Modify the state of the service and trigger event
176 Object serviceImpl = service.getManager().getImplementation();
177 Reflections.set(Reflections.getField(serviceImpl.getClass(), "status"), serviceImpl, true);
178 service.getManager().getPropertyChangeSupport().firePropertyChange("Status", false, true);
179
180 assertEquals(callback.getSubscription().getCurrentSequence().getValue(), Long.valueOf(2)); // It's the NEXT sequence!
181 assert callback.getSubscription().getSubscriptionId().startsWith("uuid:");
182
183 // Actually, the local subscription we are testing here has an "unlimited" duration
184 assertEquals(callback.getSubscription().getActualDurationSeconds(), Integer.MAX_VALUE);
185
186 callback.end();
187
188 assertEquals(testAssertions.size(), 4);
189 for (Boolean testAssertion : testAssertions) {
190 assert testAssertion;
191 }
192
193 assertEquals(upnpService.getRouter().getSentStreamRequestMessages().size(), 0);
194 }
195
196 protected MockUpnpService createMockUpnpService() {
197 return new MockUpnpService() {
198 @Override
199 protected MockRouter createRouter() {
200 return new MockRouter(getConfiguration(), getProtocolFactory()) {
201 @Override
202 public StreamResponseMessage[] getStreamResponseMessages() {
203 return new StreamResponseMessage[]{
204 createSubscribeResponseMessage(),
205 createUnsubscribeResponseMessage()
206 };
207 }
208 };
209 }
210 };
211 }
212
213 protected StreamResponseMessage createSubscribeResponseMessage() {
214 StreamResponseMessage msg = new StreamResponseMessage(new UpnpResponse(UpnpResponse.Status.OK));
215 msg.getHeaders().add(
216 UpnpHeader.Type.SID, new SubscriptionIdHeader("uuid:1234")
217 );
218 msg.getHeaders().add(
219 UpnpHeader.Type.TIMEOUT, new TimeoutHeader(180)
220 );
221 return msg;
222 }
223
224 protected StreamResponseMessage createUnsubscribeResponseMessage() {
225 return new StreamResponseMessage(new UpnpResponse(UpnpResponse.Status.OK));
226 }
227
228
229 }