View Javadoc
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 }