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 org.fourthline.cling.binding.LocalServiceBinder;
19  import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder;
20  import org.fourthline.cling.binding.annotations.UpnpAction;
21  import org.fourthline.cling.binding.annotations.UpnpInputArgument;
22  import org.fourthline.cling.binding.annotations.UpnpOutputArgument;
23  import org.fourthline.cling.binding.annotations.UpnpStateVariable;
24  import org.fourthline.cling.controlpoint.ActionCallback;
25  import org.fourthline.cling.mock.MockUpnpService;
26  import org.fourthline.cling.model.DefaultServiceManager;
27  import org.fourthline.cling.model.action.ActionArgumentValue;
28  import org.fourthline.cling.model.action.ActionInvocation;
29  import org.fourthline.cling.model.message.UpnpResponse;
30  import org.fourthline.cling.model.meta.Action;
31  import org.fourthline.cling.model.meta.LocalDevice;
32  import org.fourthline.cling.model.meta.LocalService;
33  import org.fourthline.cling.model.meta.Service;
34  import org.fourthline.cling.model.types.BooleanDatatype;
35  import org.fourthline.cling.model.types.Datatype;
36  import org.fourthline.cling.model.types.UDAServiceId;
37  import org.fourthline.cling.model.types.UDAServiceType;
38  import org.testng.annotations.DataProvider;
39  import org.testng.annotations.Test;
40  
41  import static org.testng.Assert.assertEquals;
42  
43  /**
44   * Invoking an action
45   * <p>
46   * UPnP services expose state variables and actions. While the state variables represent the
47   * current state of the service, actions are the operations used to query or maniuplate the
48   * service's state. You have to obtain a <code>Service</code> instance from a
49   * <code>Device</code> to access any <code>Action</code>. The target device can be local
50   * to the same UPnP stack as your control point, or it can be remote of another device anywhere
51   * on the network. We'll discuss later in this chapter how to access devices through the
52   * local stack's <code>Registry</code>.
53   * </p>
54   * <p>
55   * Once you have the device, access the <code>Service</code> through the metadata model, for example:
56   * </p>
57   * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_findservice" style="include: FINDSERVICE"/>
58   * <p>
59   * This method will search the device and all its embedded devices for a service with the given
60   * identifier and returns either the found <code>Service</code> or <code>null</code>. The Cling
61   * metamodel is thread-safe, so you can share an instance of <code>Service</code> or
62   * <code>Action</code> and access it concurrently.
63   * </p>
64   * <p>
65   * Invoking an action is the job of an instance of <code>ActionInvocation</code>, note that this
66   * instance is <em>NOT</em> thread-safe and each thread that wishes to execute an action has to
67   * obtain its own invocation from the <code>Action</code> metamodel:
68   * </p>
69   * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_getstatus" style="include: GETSTATUS; exclude: EXC1"/>
70   * <p>
71   * Execution is asynchronous, your <code>ActionCallback</code> has two methods which will be called
72   * by the UPnP stack when the execution completes. If the action is successful, you can obtain any
73   * output argument values from the invocation instance, which is conveniently passed into the
74   * <code>success()</code> method. You can inspect the named output argument values and their datatypes to
75   * continue processing the result.
76   * </p>
77   * <p>
78   * Action execution doesn't have to be processed asynchronously, after all, the underlying HTTP/SOAP protocol
79   * is a request waiting for a response. The callback programming model however fits nicely into a typical
80   * UPnP client, which also has to process event notifications and device registrations asynchronously. If
81   * you want to execute an <code>ActionInvocation</code> directly, within the current thread, use the empty
82   * <code>ActionCallback.Default</code> implementation:
83   * </p>
84   * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_synchronous" style="include: SYNCHRONOUS"/>
85   * <p>
86   * When invocation fails you can access the failure details through
87   * <code>invocation.getFailure()</code>, or use the shown convenience method to create a simple error
88   * message. See the Javadoc of <code>ActionCallback</code> for more details.
89   * </p>
90   * <p>
91   * When an action requires input argument values, you have to provide them. Like output arguments, any
92   * input arguments of actions are also named, so you can set them by calling <code>setInput("MyArgumentName", value)</code>:
93   * </p>
94   * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_settarget" style="include: SETTARGET; exclude: EXC2"/>
95   * <p>
96   * This action has one input argument of UPnP type "boolean". You can set a Java <code>boolean</code>
97   * primitive or <code>Boolean</code> instance and it will be automatically converted. If you set an
98   * invalid value for a particular argument, such as an instance with the wrong type,
99   * an <code>InvalidValueException</code> will be thrown immediately.
100  * </p>
101  * <div class="note">
102  * <div class="title">Empty values and null in Cling</div>
103  * There is no difference between empty string <code>""</code> and <code>null</code> in Cling,
104  * because the UPnP specification does not address this issue. The SOAP  message of an action call
105  * or an event message must contain an element {@code <SomeVar></SomeVar>} for all arguments, even if
106  * it is an empty XML element. If you provide  an empty string or a null value when preparing a message,
107  * it will always be a <code>null</code> on the receiving end because we can only transmit one
108  * thing, an empty XML element. If you forget to set an input argument's value, it will be null/empty element.
109  * </div>
110  */
111 public class ActionInvocationTest {
112 
113     protected LocalService bindService(Class<?> clazz) throws Exception {
114         LocalServiceBinder binder = new AnnotationLocalServiceBinder();
115         // Let's also test the overloaded reader
116         LocalService svc = binder.read(
117                 clazz,
118                 new UDAServiceId("SwitchPower"),
119                 new UDAServiceType("SwitchPower", 1),
120                 true,
121                 new Class[]{MyString.class}
122         );
123         svc.setManager(
124                 new DefaultServiceManager(svc, clazz)
125         );
126         return svc;
127     }
128 
129     @DataProvider(name = "devices")
130     public Object[][] getDevices() throws Exception {
131         return new LocalDevice[][]{
132                 {BinaryLightSampleData.createDevice(bindService(TestServiceOne.class))},
133                 {BinaryLightSampleData.createDevice(bindService(TestServiceTwo.class))},
134                 {BinaryLightSampleData.createDevice(bindService(TestServiceThree.class))},
135         };
136     }
137 
138     @Test(dataProvider = "devices")
139     public void invokeActions(LocalDevice device) throws Exception {
140 
141         MockUpnpService upnpService = new MockUpnpService();
142 
143         Service service = device.findService(new UDAServiceId("SwitchPower")); // DOC: FINDSERVICE
144         Action getStatusAction = service.getAction("GetStatus");               // DOC: FINDSERVICE
145 
146         final boolean[] tests = new boolean[3];
147 
148         ActionInvocation getStatusInvocation = new ActionInvocation(getStatusAction);   // DOC: GETSTATUS
149 
150         ActionCallback getStatusCallback = new ActionCallback(getStatusInvocation) {
151 
152             @Override
153             public void success(ActionInvocation invocation) {
154                 ActionArgumentValue status  = invocation.getOutput("ResultStatus");
155 
156                 assert status != null;
157 
158                 assertEquals(status.getArgument().getName(), "ResultStatus");
159 
160                 assertEquals(status.getDatatype().getClass(), BooleanDatatype.class);
161                 assertEquals(status.getDatatype().getBuiltin(), Datatype.Builtin.BOOLEAN);
162 
163                 assertEquals((Boolean) status.getValue(), Boolean.valueOf(false));
164                 assertEquals(status.toString(), "0"); // '0' is 'false' in UPnP
165                 tests[0] = true; // DOC: EXC1
166             }
167 
168             @Override
169             public void failure(ActionInvocation invocation,
170                                 UpnpResponse operation,
171                                 String defaultMsg) {
172                 System.err.println(defaultMsg);
173             }
174         };
175 
176         upnpService.getControlPoint().execute(getStatusCallback);                       // DOC: GETSTATUS
177 
178 
179         Action action = service.getAction("SetTarget");                                 // DOC: SETTARGET
180 
181         ActionInvocation setTargetInvocation = new ActionInvocation(action);
182 
183         setTargetInvocation.setInput("NewTargetValue", true); // Can throw InvalidValueException
184 
185         // Alternative:
186         //
187         // setTargetInvocation.setInput(
188         //         new ActionArgumentValue(
189         //                 action.getInputArgument("NewTargetValue"),
190         //                 true
191         //         )
192         // );
193 
194         ActionCallback setTargetCallback = new ActionCallback(setTargetInvocation) {
195 
196             @Override
197             public void success(ActionInvocation invocation) {
198                 ActionArgumentValue[] output = invocation.getOutput();
199                 assertEquals(output.length, 0);
200                 tests[1] = true; // DOC: EXC2
201             }
202 
203             @Override
204             public void failure(ActionInvocation invocation,
205                                 UpnpResponse operation,
206                                 String defaultMsg) {
207                 System.err.println(defaultMsg);
208             }
209         };
210 
211         upnpService.getControlPoint().execute(setTargetCallback);                       // DOC: SETTARGET
212 
213         getStatusInvocation = new ActionInvocation(getStatusAction);
214         new ActionCallback.Default(getStatusInvocation, upnpService.getControlPoint()).run(); // DOC: SYNCHRONOUS
215         ActionArgumentValue status  = getStatusInvocation.getOutput("ResultStatus");
216         if (Boolean.valueOf(true).equals(status.getValue())) {
217             tests[2] = true;
218         }
219 
220         for (boolean test : tests) {
221             assertEquals(test, true);
222         }
223 
224 
225         LocalService svc = (LocalService) service;
226 
227         ActionInvocation getTargetInvocation = new ActionInvocation(svc.getAction("GetTarget"));
228         svc.getExecutor(getTargetInvocation.getAction()).execute(getTargetInvocation);
229         assertEquals(getTargetInvocation.getFailure(), null);
230         assertEquals(getTargetInvocation.getOutput().length, 1);
231         assertEquals(getTargetInvocation.getOutput()[0].toString(), "1");
232 
233         ActionInvocation setMyStringInvocation = new ActionInvocation(svc.getAction("SetMyString"));
234         setMyStringInvocation.setInput("MyString", "foo");
235         svc.getExecutor(setMyStringInvocation.getAction()).execute(setMyStringInvocation);
236         assertEquals(setMyStringInvocation.getFailure(), null);
237         assertEquals(setMyStringInvocation.getOutput().length, 0);
238 
239         ActionInvocation getMyStringInvocation = new ActionInvocation(svc.getAction("GetMyString"));
240         svc.getExecutor(getMyStringInvocation.getAction()).execute(getMyStringInvocation);
241         assertEquals(getTargetInvocation.getFailure(), null);
242         assertEquals(getMyStringInvocation.getOutput().length, 1);
243         assertEquals(getMyStringInvocation.getOutput()[0].toString(), "foo");
244 
245     }
246 
247     @Test(dataProvider = "devices")
248     public void invokeActionsWithAlias(LocalDevice device) throws Exception {
249 
250         MockUpnpService upnpService = new MockUpnpService();
251 
252         Service service = device.findService(new UDAServiceId("SwitchPower"));
253         Action getStatusAction = service.getAction("GetStatus");
254 
255         final boolean[] tests = new boolean[1];
256 
257         Action action = service.getAction("SetTarget");
258         ActionInvocation setTargetInvocation = new ActionInvocation(action);
259         setTargetInvocation.setInput("NewTargetValue1", true);
260         ActionCallback setTargetCallback = new ActionCallback(setTargetInvocation) {
261 
262             @Override
263             public void success(ActionInvocation invocation) {
264                 ActionArgumentValue[] output = invocation.getOutput();
265                 assertEquals(output.length, 0);
266                 tests[0] = true;
267             }
268 
269             @Override
270             public void failure(ActionInvocation invocation,
271                                 UpnpResponse operation,
272                                 String defaultMsg) {
273                 System.err.println(defaultMsg);
274             }
275         };
276         upnpService.getControlPoint().execute(setTargetCallback);
277 
278         for (boolean test : tests) {
279             assertEquals(test, true);
280         }
281 
282         LocalService svc = (LocalService) service;
283 
284         ActionInvocation getTargetInvocation = new ActionInvocation(svc.getAction("GetTarget"));
285         svc.getExecutor(getTargetInvocation.getAction()).execute(getTargetInvocation);
286         assertEquals(getTargetInvocation.getFailure(), null);
287         assertEquals(getTargetInvocation.getOutput().length, 1);
288         assertEquals(getTargetInvocation.getOutput()[0].toString(), "1");
289 
290         ActionInvocation setMyStringInvocation = new ActionInvocation(svc.getAction("SetMyString"));
291         setMyStringInvocation.setInput("MyString1", "foo");
292         svc.getExecutor(setMyStringInvocation.getAction()).execute(setMyStringInvocation);
293         assertEquals(setMyStringInvocation.getFailure(), null);
294         assertEquals(setMyStringInvocation.getOutput().length, 0);
295 
296         ActionInvocation getMyStringInvocation = new ActionInvocation(svc.getAction("GetMyString"));
297         svc.getExecutor(getMyStringInvocation.getAction()).execute(getMyStringInvocation);
298         assertEquals(getTargetInvocation.getFailure(), null);
299         assertEquals(getMyStringInvocation.getOutput().length, 1);
300         assertEquals(getMyStringInvocation.getOutput()[0].toString(), "foo");
301 
302     }
303 
304     /* ####################################################################################################### */
305 
306     public static class TestServiceOne {
307 
308         @UpnpStateVariable(sendEvents = false)
309         private boolean target = false;
310 
311         @UpnpStateVariable
312         private boolean status = false;
313 
314         @UpnpStateVariable(sendEvents = false)
315         private MyString myString;
316 
317         @UpnpAction
318         public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
319             target = newTargetValue;
320             status = newTargetValue;
321         }
322 
323         @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
324         public boolean getTarget() {
325             return target;
326         }
327 
328         @UpnpAction(name = "GetStatus", out = @UpnpOutputArgument(name = "ResultStatus", getterName = "getStatus"))
329         public void dummyStatus() {
330             // NOOP
331         }
332 
333         public boolean getStatus() {
334             return status;
335         }
336 
337         @UpnpAction
338         public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
339             this.myString = myString;
340         }
341 
342         @UpnpAction(name = "GetMyString", out = @UpnpOutputArgument(name = "MyString", getterName = "getMyString"))
343         public void getMyStringDummy() {
344         }
345 
346         public MyString getMyString() {
347             return myString;
348         }
349     }
350 
351     public static class TestServiceTwo {
352 
353         @UpnpStateVariable(sendEvents = false)
354         private boolean target = false;
355 
356         @UpnpStateVariable
357         private boolean status = false;
358 
359         @UpnpStateVariable(sendEvents = false)
360         private MyString myString;
361 
362         @UpnpAction
363         public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
364             target = newTargetValue;
365             status = newTargetValue;
366         }
367 
368         @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
369         public boolean getTarget() {
370             return target;
371         }
372 
373         @UpnpAction(name = "GetStatus", out = @UpnpOutputArgument(name = "ResultStatus", getterName = "getStatus"))
374         public StatusHolder dummyStatus() {
375             return new StatusHolder(status);
376         }
377 
378         @UpnpAction
379         public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
380             this.myString = myString;
381         }
382 
383         @UpnpAction(out = @UpnpOutputArgument(name = "MyString", getterName = "getMyString"))
384         public MyStringHolder getMyString() {
385             return new MyStringHolder(myString);
386         }
387 
388         public class StatusHolder {
389             boolean st;
390 
391             public StatusHolder(boolean st) {
392                 this.st = st;
393             }
394 
395             public boolean getStatus() {
396                 return st;
397             }
398         }
399 
400         public class MyStringHolder {
401             MyString myString;
402 
403             public MyStringHolder(MyString myString) {
404                 this.myString = myString;
405             }
406 
407             public MyString getMyString() {
408                 return myString;
409             }
410         }
411 
412     }
413 
414     public static class TestServiceThree {
415 
416         @UpnpStateVariable(sendEvents = false)
417         private boolean target = false;
418 
419         @UpnpStateVariable
420         private boolean status = false;
421 
422         @UpnpStateVariable(sendEvents = false)
423         private MyString myString;
424 
425         @UpnpAction
426         public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
427             target = newTargetValue;
428             status = newTargetValue;
429         }
430 
431         @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
432         public boolean getTarget() {
433             return target;
434         }
435 
436         @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
437         public boolean getStatus() {
438             return status;
439         }
440 
441         @UpnpAction
442         public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
443             this.myString = myString;
444         }
445 
446         @UpnpAction(out = @UpnpOutputArgument(name = "MyString"))
447         public MyString getMyString() {
448             return myString;
449         }
450     }
451 
452     public static class MyString {
453         private String s;
454 
455         public MyString(String s) {
456             this.s = s;
457         }
458 
459         public String getS() {
460             return s;
461         }
462 
463         @Override
464         public String toString() {
465             return s;
466         }
467     }
468 
469 }