/* * 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. * <p> * Usage example, establishing a subscription with a {@link org.fourthline.cling.model.meta.Service}: * </p> * <pre> * 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); * </pre> * * @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 <tt>createDefaultFailureMessage()</tt>. * * @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 <tt>null</tt>. * @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 <tt>null</tt>. * @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 <tt>end()</tt>), this is <tt>null</tt>. * @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. * <p> * Use the {@link org.fourthline.cling.model.gena.GENASubscription#getCurrentValues()} method to obtain * the evented state variable values. * </p> * * @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. * <p> * It's up to you if you want to react to missed events or if you (can) silently ignore them. * </p> * @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 <code>null</code> if there was no response. * @param exception The exception or <code>null</code> 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. * <p> * 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. * </p> * <p> * The default implementation will log the exception at <code>INFO</code> level, and * the invalid XML at <code>FINE</code> level. * </p> * * @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().toString()); log.fine("------------------------------------------------------------------------------"); } } @Override public String toString() { return "(SubscriptionCallback) " + getService(); } }