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  
16  package org.fourthline.cling.controlpoint;
17  
18  import org.fourthline.cling.model.UnsupportedDataException;
19  import org.fourthline.cling.model.UserConstants;
20  import org.fourthline.cling.model.gena.CancelReason;
21  import org.fourthline.cling.model.gena.GENASubscription;
22  import org.fourthline.cling.model.gena.LocalGENASubscription;
23  import org.fourthline.cling.model.gena.RemoteGENASubscription;
24  import org.fourthline.cling.model.message.UpnpResponse;
25  import org.fourthline.cling.model.meta.LocalService;
26  import org.fourthline.cling.model.meta.RemoteService;
27  import org.fourthline.cling.model.meta.Service;
28  import org.fourthline.cling.protocol.ProtocolCreationException;
29  import org.fourthline.cling.protocol.sync.SendingSubscribe;
30  import org.seamless.util.Exceptions;
31  
32  import java.util.Collections;
33  import java.util.logging.Level;
34  import java.util.logging.Logger;
35  
36  /**
37   * Subscribe and receive events from a service through GENA.
38   * <p>
39   * Usage example, establishing a subscription with a {@link org.fourthline.cling.model.meta.Service}:
40   * </p>
41   * <pre>
42   * SubscriptionCallback callback = new SubscriptionCallback(service, 600) { // Timeout in seconds
43   *
44   *      public void established(GENASubscription sub) {
45   *          System.out.println("Established: " + sub.getSubscriptionId());
46   *      }
47   *
48   *      public void failed(GENASubscription sub, UpnpResponse response, Exception ex) {
49   *          System.err.println(
50   *              createDefaultFailureMessage(response, ex)
51   *          );
52   *      }
53   *
54   *      public void ended(GENASubscription sub, CancelReason reason, UpnpResponse response) {
55   *          // Reason should be null, or it didn't end regularly
56   *      }
57   *
58   *      public void eventReceived(GENASubscription sub) {
59   *          System.out.println("Event: " + sub.getCurrentSequence().getValue());
60   *          Map&lt;String, StateVariableValue> values = sub.getCurrentValues();
61   *          StateVariableValue status = values.get("Status");
62   *          System.out.println("Status is: " + status.toString());
63   *      }
64   *
65   *      public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
66   *          System.out.println("Missed events: " + numberOfMissedEvents);
67   *      }
68   * };
69   *
70   * upnpService.getControlPoint().execute(callback);
71   * </pre>
72   *
73   * @author Christian Bauer
74   */
75  public abstract class SubscriptionCallback implements Runnable {
76  
77      protected static Logger log = Logger.getLogger(SubscriptionCallback.class.getName());
78  
79      protected final Service service;
80      protected final Integer requestedDurationSeconds;
81  
82      private ControlPoint controlPoint;
83      private GENASubscription subscription;
84  
85      protected SubscriptionCallback(Service service) {
86          this.service = service;
87          this.requestedDurationSeconds = UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS;
88      }
89  
90      protected SubscriptionCallback(Service service, int requestedDurationSeconds) {
91          this.service = service;
92          this.requestedDurationSeconds = requestedDurationSeconds;
93      }
94  
95      public Service getService() {
96          return service;
97      }
98  
99      synchronized public ControlPoint getControlPoint() {
100         return controlPoint;
101     }
102 
103     synchronized public void setControlPoint(ControlPoint controlPoint) {
104         this.controlPoint = controlPoint;
105     }
106 
107     synchronized public GENASubscription getSubscription() {
108         return subscription;
109     }
110 
111     synchronized public void setSubscription(GENASubscription subscription) {
112         this.subscription = subscription;
113     }
114 
115     synchronized public void run() {
116         if (getControlPoint()  == null) {
117             throw new IllegalStateException("Callback must be executed through ControlPoint");
118         }
119 
120         if (getService() instanceof LocalService) {
121             establishLocalSubscription((LocalService) service);
122         } else if (getService() instanceof RemoteService) {
123             establishRemoteSubscription((RemoteService) service);
124         }
125     }
126 
127     private void establishLocalSubscription(LocalService service) {
128 
129         if (getControlPoint().getRegistry().getLocalDevice(service.getDevice().getIdentity().getUdn(), false) == null) {
130             log.fine("Local device service is currently not registered, failing subscription immediately");
131             failed(null, null, new IllegalStateException("Local device is not registered"));
132             return;
133         }
134 
135         // Local execution of subscription on local service re-uses the procedure and lifecycle that is
136         // used for inbound subscriptions from remote control points on local services!
137         // Except that it doesn't ever expire, we override the requested duration with Integer.MAX_VALUE!
138 
139         LocalGENASubscription localSubscription = null;
140         try {
141             localSubscription =
142                     new LocalGENASubscription(service, Integer.MAX_VALUE, Collections.EMPTY_LIST) {
143 
144                         public void failed(Exception ex) {
145                             synchronized (SubscriptionCallback.this) {
146                                 SubscriptionCallback.this.setSubscription(null);
147                                 SubscriptionCallback.this.failed(null, null, ex);
148                             }
149                         }
150 
151                         public void established() {
152                             synchronized (SubscriptionCallback.this) {
153                                 SubscriptionCallback.this.setSubscription(this);
154                                 SubscriptionCallback.this.established(this);
155                             }
156                         }
157 
158                         public void ended(CancelReason reason) {
159                             synchronized (SubscriptionCallback.this) {
160                                 SubscriptionCallback.this.setSubscription(null);
161                                 SubscriptionCallback.this.ended(this, reason, null);
162                             }
163                         }
164 
165                         public void eventReceived() {
166                             synchronized (SubscriptionCallback.this) {
167                                 log.fine("Local service state updated, notifying callback, sequence is: " + getCurrentSequence());
168                                 SubscriptionCallback.this.eventReceived(this);
169                                 incrementSequence();
170                             }
171                         }
172                     };
173 
174             log.fine("Local device service is currently registered, also registering subscription");
175             getControlPoint().getRegistry().addLocalSubscription(localSubscription);
176 
177             log.fine("Notifying subscription callback of local subscription availablity");
178             localSubscription.establish();
179 
180             log.fine("Simulating first initial event for local subscription callback, sequence: " + localSubscription.getCurrentSequence());
181             eventReceived(localSubscription);
182             localSubscription.incrementSequence();
183 
184             log.fine("Starting to monitor state changes of local service");
185             localSubscription.registerOnService();
186 
187         } catch (Exception ex) {
188             log.fine("Local callback creation failed: " + ex.toString());
189             log.log(Level.FINE, "Exception root cause: ", Exceptions.unwrap(ex));
190             if (localSubscription != null)
191                 getControlPoint().getRegistry().removeLocalSubscription(localSubscription);
192             failed(localSubscription, null, ex);
193         }
194     }
195 
196     private void establishRemoteSubscription(RemoteService service) {
197         RemoteGENASubscription remoteSubscription =
198                 new RemoteGENASubscription(service, requestedDurationSeconds) {
199 
200                     public void failed(UpnpResponse responseStatus) {
201                         synchronized (SubscriptionCallback.this) {
202                             SubscriptionCallback.this.setSubscription(null);
203                             SubscriptionCallback.this.failed(this, responseStatus, null);
204                         }
205                     }
206 
207                     public void established() {
208                         synchronized (SubscriptionCallback.this) {
209                             SubscriptionCallback.this.setSubscription(this);
210                             SubscriptionCallback.this.established(this);
211                         }
212                     }
213 
214                     public void ended(CancelReason reason, UpnpResponse responseStatus) {
215                         synchronized (SubscriptionCallback.this) {
216                             SubscriptionCallback.this.setSubscription(null);
217                             SubscriptionCallback.this.ended(this, reason, responseStatus);
218                         }
219                     }
220 
221                     public void eventReceived() {
222                         synchronized (SubscriptionCallback.this) {
223                             SubscriptionCallback.this.eventReceived(this);
224                         }
225                     }
226 
227                     public void eventsMissed(int numberOfMissedEvents) {
228                         synchronized (SubscriptionCallback.this) {
229                             SubscriptionCallback.this.eventsMissed(this, numberOfMissedEvents);
230                         }
231                     }
232 
233 					public void invalidMessage(UnsupportedDataException ex) {
234 						synchronized (SubscriptionCallback.this) {
235 							SubscriptionCallback.this.invalidMessage(this, ex);
236 						}
237 					}
238                 };
239 
240         SendingSubscribe protocol;
241         try {
242             protocol = getControlPoint().getProtocolFactory().createSendingSubscribe(remoteSubscription);
243         } catch (ProtocolCreationException ex) {
244             failed(subscription, null, ex);
245             return;
246         }
247         protocol.run();
248     }
249 
250     synchronized public void end() {
251         if (subscription == null) return;
252         if (subscription instanceof LocalGENASubscription) {
253             endLocalSubscription((LocalGENASubscription)subscription);
254         } else if (subscription instanceof RemoteGENASubscription) {
255             endRemoteSubscription((RemoteGENASubscription)subscription);
256         }
257     }
258 
259     private void endLocalSubscription(LocalGENASubscription subscription) {
260         log.fine("Removing local subscription and ending it in callback: " + subscription);
261         getControlPoint().getRegistry().removeLocalSubscription(subscription);
262         subscription.end(null); // No reason, on controlpoint request
263     }
264 
265     private void endRemoteSubscription(RemoteGENASubscription subscription) {
266         log.fine("Ending remote subscription: " + subscription);
267         getControlPoint().getConfiguration().getSyncProtocolExecutorService().execute(
268                 getControlPoint().getProtocolFactory().createSendingUnsubscribe(subscription)
269         );
270     }
271 
272     protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception) {
273         failed(subscription, responseStatus, exception, createDefaultFailureMessage(responseStatus, exception));
274     }
275 
276     /**
277      * Called when establishing a local or remote subscription failed. To get a nice error message that
278      * transparently detects local or remote errors use <tt>createDefaultFailureMessage()</tt>.
279      *
280      * @param subscription   The failed subscription object, not very useful at this point.
281      * @param responseStatus For a remote subscription, if a response was received at all, this is it, otherwise <tt>null</tt>.
282      * @param exception      For a local subscription and failed creation of a remote subscription protocol (before
283      *                       sending the subscribe request), any exception that caused the failure, otherwise <tt>null</tt>.
284      * @param defaultMsg     A user-friendly error message.
285      * @see #createDefaultFailureMessage
286      */
287     protected abstract void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg);
288 
289     /**
290      * Called when a local or remote subscription was successfully established.
291      *
292      * @param subscription The successful subscription.
293      */
294     protected abstract void established(GENASubscription subscription);
295 
296     /**
297      * Called when a local or remote subscription ended, either on user request or because of a failure.
298      *
299      * @param subscription   The ended subscription instance.
300      * @param reason         If the subscription ended regularly (through <tt>end()</tt>), this is <tt>null</tt>.
301      * @param responseStatus For a remote subscription, if the cause implies a remopte response and it was
302      *                       received, this is it (e.g. renewal failure response).
303      */
304     protected abstract void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus);
305 
306     /**
307      * Called when an event for an established subscription has been received.
308      * <p>
309      * Use the {@link org.fourthline.cling.model.gena.GENASubscription#getCurrentValues()} method to obtain
310      * the evented state variable values.
311      * </p>
312      *
313      * @param subscription The established subscription with fresh state variable values.
314      */
315     protected abstract void eventReceived(GENASubscription subscription);
316 
317     /**
318      * Called when a received event was out of sequence, indicating that events have been missed.
319      * <p>
320      * It's up to you if you want to react to missed events or if you (can) silently ignore them.
321      * </p>
322      * @param subscription The established subscription.
323      * @param numberOfMissedEvents The number of missed events.
324      */
325     protected abstract void eventsMissed(GENASubscription subscription, int numberOfMissedEvents);
326 
327     /**
328      * @param responseStatus The (HTTP) response or <code>null</code> if there was no response.
329      * @param exception The exception or <code>null</code> if there was no exception.
330      * @return A human-friendly error message.
331      */
332     public static String createDefaultFailureMessage(UpnpResponse responseStatus, Exception exception) {
333         String message = "Subscription failed: ";
334         if (responseStatus != null) {
335             message = message + " HTTP response was: " + responseStatus.getResponseDetails();
336         } else if (exception != null) {
337             message = message + " Exception occured: " + exception;
338         } else {
339             message = message + " No response received.";
340         }
341         return message;
342     }
343 
344     /**
345      * Called when a received event message could not be parsed successfully.
346      * <p>
347      * This typically indicates a broken device which is not UPnP compliant. You can
348      * react to this failure in any way you like, for example, you could terminate
349      * the subscription or simply create an error report/log.
350      * </p>
351      * <p>
352      * The default implementation will log the exception at <code>INFO</code> level, and
353      * the invalid XML at <code>FINE</code> level.
354      * </p>
355      *
356      * @param remoteGENASubscription The established subscription.
357      * @param ex Call {@link org.fourthline.cling.model.UnsupportedDataException#getData()} to access the invalid XML.
358      */
359 	protected void invalidMessage(RemoteGENASubscription remoteGENASubscription,
360                                   UnsupportedDataException ex) {
361         log.info("Invalid event message received, causing: " + ex);
362         if (log.isLoggable(Level.FINE)) {
363             log.fine("------------------------------------------------------------------------------");
364             log.fine(ex.getData() != null ? ex.getData().toString() : "null");
365             log.fine("------------------------------------------------------------------------------");
366         }
367     }
368 
369     @Override
370     public String toString() {
371         return "(SubscriptionCallback) " + getService();
372     }
373 
374 }