com.digitalpetri.opcua.sdk.server.subscriptions.Subscription.java Source code

Java tutorial

Introduction

Here is the source code for com.digitalpetri.opcua.sdk.server.subscriptions.Subscription.java

Source

/*
 * digitalpetri OPC-UA SDK
 *
 * Copyright (C) 2015 Kevin Herron
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.digitalpetri.opcua.sdk.server.subscriptions;

import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import com.digitalpetri.opcua.sdk.server.Session;
import com.digitalpetri.opcua.sdk.server.items.BaseMonitoredItem;
import com.digitalpetri.opcua.stack.core.StatusCodes;
import com.digitalpetri.opcua.stack.core.application.services.ServiceRequest;
import com.digitalpetri.opcua.stack.core.serialization.UaStructure;
import com.digitalpetri.opcua.stack.core.types.builtin.DateTime;
import com.digitalpetri.opcua.stack.core.types.builtin.DiagnosticInfo;
import com.digitalpetri.opcua.stack.core.types.builtin.ExtensionObject;
import com.digitalpetri.opcua.stack.core.types.builtin.StatusCode;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UInteger;
import com.digitalpetri.opcua.stack.core.types.structured.DataChangeNotification;
import com.digitalpetri.opcua.stack.core.types.structured.EventFieldList;
import com.digitalpetri.opcua.stack.core.types.structured.EventNotificationList;
import com.digitalpetri.opcua.stack.core.types.structured.ModifySubscriptionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.MonitoredItemNotification;
import com.digitalpetri.opcua.stack.core.types.structured.NotificationMessage;
import com.digitalpetri.opcua.stack.core.types.structured.PublishRequest;
import com.digitalpetri.opcua.stack.core.types.structured.PublishResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ResponseHeader;
import com.digitalpetri.opcua.stack.core.types.structured.SetPublishingModeRequest;
import com.digitalpetri.opcua.stack.core.types.structured.StatusChangeNotification;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.PeekingIterator;
import com.google.common.math.DoubleMath;
import com.google.common.primitives.Ints;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;

public class Subscription {

    private static final double MIN_LIFETIME = 10 * 1000.0;
    private static final double MAX_LIFETIME = 60 * 60 * 1000.0;

    private static final double MIN_PUBLISHING_INTERVAL = 100.0;
    private static final double MAX_PUBLISHING_INTERVAL = 60 * 1000.0;

    private static final int MAX_NOTIFICATIONS = 0xFFFF;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private volatile Iterator<BaseMonitoredItem<?>> lastIterator = Iterators.emptyIterator();

    private final AtomicLong itemIds = new AtomicLong(1L);
    private final Map<UInteger, BaseMonitoredItem<?>> itemsById = Maps.newConcurrentMap();

    private final AtomicReference<State> state = new AtomicReference<>(State.Normal);
    private final AtomicReference<StateListener> stateListener = new AtomicReference<>();

    private final AtomicLong sequenceNumber = new AtomicLong(1L);

    private final Map<UInteger, NotificationMessage> availableMessages = Maps.newConcurrentMap();

    private final PublishHandler publishHandler = new PublishHandler();
    private final TimerHandler timerHandler = new TimerHandler();

    private volatile boolean messageSent = false;
    private volatile boolean moreNotifications = false;
    private volatile long keepAliveCounter;
    private volatile long lifetimeCounter;

    private volatile double publishingInterval;
    private volatile long lifetimeCount;
    private volatile long maxKeepAliveCount;
    private volatile int maxNotificationsPerPublish;
    private volatile boolean publishingEnabled;
    private volatile int priority;

    private volatile SubscriptionManager subscriptionManager;

    private final UInteger subscriptionId;

    public Subscription(SubscriptionManager subscriptionManager, UInteger subscriptionId, double publishingInterval,
            long maxKeepAliveCount, long lifetimeCount, long maxNotificationsPerPublish, boolean publishingEnabled,
            int priority) {

        this.subscriptionManager = subscriptionManager;
        this.subscriptionId = subscriptionId;

        setPublishingInterval(publishingInterval);
        setMaxKeepAliveCount(maxKeepAliveCount);
        setLifetimeCount(lifetimeCount);
        setMaxNotificationsPerPublish(maxNotificationsPerPublish);

        this.publishingEnabled = publishingEnabled;
        this.priority = priority;

        resetKeepAliveCounter();
        resetLifetimeCounter();

        logger.debug("[id={}] subscription created, interval={}, keep-alive={}, lifetime={}", subscriptionId,
                publishingInterval, maxKeepAliveCount, lifetimeCount);
    }

    public synchronized void modifySubscription(ModifySubscriptionRequest request) {
        setPublishingInterval(request.getRequestedPublishingInterval());
        setMaxKeepAliveCount(request.getRequestedMaxKeepAliveCount().longValue());
        setLifetimeCount(request.getRequestedLifetimeCount().longValue());
        setMaxNotificationsPerPublish(request.getMaxNotificationsPerPublish().longValue());

        this.priority = request.getPriority().intValue();

        resetLifetimeCounter();

        logger.debug("[id={}] subscription modified, interval={}, keep-alive={}, lifetime={}", subscriptionId,
                publishingInterval, maxKeepAliveCount, lifetimeCount);
    }

    public synchronized List<BaseMonitoredItem<?>> deleteSubscription() {
        setState(State.Closed);

        logger.debug("[id={}] subscription deleted.", subscriptionId);

        return Lists.newArrayList(itemsById.values());
    }

    public synchronized void setPublishingMode(SetPublishingModeRequest request) {
        this.publishingEnabled = request.getPublishingEnabled();

        resetLifetimeCounter();

        logger.debug("[id={}] {}.", subscriptionId,
                publishingEnabled ? "publishing enabled." : "publishing disabled.");
    }

    public synchronized void addMonitoredItems(List<BaseMonitoredItem<?>> createdItems) {
        for (BaseMonitoredItem<?> item : createdItems) {
            itemsById.put(item.getId(), item);
        }

        resetLifetimeCounter();

        logger.debug("[id={}] created {} MonitoredItems.", subscriptionId, createdItems.size());
    }

    public synchronized void removeMonitoredItems(List<BaseMonitoredItem<?>> deletedItems) {
        for (BaseMonitoredItem<?> item : deletedItems) {
            itemsById.remove(item.getId());
        }

        resetLifetimeCounter();

        logger.debug("[id={}] deleted {} MonitoredItems.", subscriptionId, deletedItems.size());
    }

    public synchronized Map<UInteger, BaseMonitoredItem<?>> getMonitoredItems() {
        return itemsById;
    }

    /**
     * Given the requested publishing interval, set it to something reasonable.
     *
     * @param requestedPublishingInterval the requested publishing interval.
     */
    private void setPublishingInterval(double requestedPublishingInterval) {
        if (requestedPublishingInterval < MIN_PUBLISHING_INTERVAL || Double.isNaN(requestedPublishingInterval)
                || Double.isInfinite(requestedPublishingInterval)) {
            requestedPublishingInterval = MIN_PUBLISHING_INTERVAL;
        }

        if (requestedPublishingInterval > MAX_PUBLISHING_INTERVAL) {
            requestedPublishingInterval = MAX_PUBLISHING_INTERVAL;
        }

        this.publishingInterval = requestedPublishingInterval;
    }

    private void setMaxKeepAliveCount(long maxKeepAliveCount) {
        if (maxKeepAliveCount == 0)
            maxKeepAliveCount = 3;

        double keepAliveInterval = maxKeepAliveCount * publishingInterval;

        // keep alive interval cannot be longer than the max subscription lifetime.
        if (keepAliveInterval > MAX_LIFETIME) {
            maxKeepAliveCount = (long) (MAX_LIFETIME / publishingInterval);

            if (maxKeepAliveCount < UInteger.MAX_VALUE) {
                if (MAX_LIFETIME % publishingInterval != 0) {
                    maxKeepAliveCount++;
                }
            }

            keepAliveInterval = maxKeepAliveCount * publishingInterval;
        }

        // the time between publishes cannot exceed the max publishing interval.
        if (keepAliveInterval > MAX_PUBLISHING_INTERVAL) {
            maxKeepAliveCount = (long) (MAX_PUBLISHING_INTERVAL / publishingInterval);

            if (maxKeepAliveCount < UInteger.MAX_VALUE) {
                if (MAX_PUBLISHING_INTERVAL % publishingInterval != 0) {
                    maxKeepAliveCount++;
                }
            }
        }

        this.maxKeepAliveCount = maxKeepAliveCount;
    }

    private void setLifetimeCount(long lifetimeCount) {
        double lifetimeInterval = lifetimeCount * publishingInterval;

        // lifetime cannot be longer than the max subscription lifetime.
        if (lifetimeInterval > MAX_LIFETIME) {
            lifetimeCount = (long) (MAX_LIFETIME / publishingInterval);

            if (lifetimeCount < UInteger.MAX_VALUE) {
                if (MAX_LIFETIME % publishingInterval != 0) {
                    lifetimeCount++;
                }
            }
        }

        // the lifetime must be greater than the keepalive.
        if (maxKeepAliveCount < UInteger.MAX_VALUE / 3) {
            if (maxKeepAliveCount * 3 > lifetimeCount) {
                lifetimeCount = maxKeepAliveCount * 3;
            }

            lifetimeInterval = lifetimeCount * publishingInterval;
        } else {
            lifetimeCount = UInteger.MAX_VALUE;
            lifetimeInterval = Double.MAX_VALUE;
        }

        // apply the minimum.
        if (MIN_LIFETIME > publishingInterval && MIN_LIFETIME > lifetimeInterval) {
            lifetimeCount = (long) (MIN_LIFETIME / publishingInterval);

            if (lifetimeCount < UInteger.MAX_VALUE) {
                if (MIN_LIFETIME % publishingInterval != 0) {
                    lifetimeCount++;
                }
            }
        }

        this.lifetimeCount = lifetimeCount;
    }

    private void setMaxNotificationsPerPublish(long maxNotificationsPerPublish) {
        if (maxNotificationsPerPublish <= 0 || maxNotificationsPerPublish > MAX_NOTIFICATIONS) {
            maxNotificationsPerPublish = MAX_NOTIFICATIONS;
        }
        this.maxNotificationsPerPublish = Ints.saturatedCast(maxNotificationsPerPublish);
    }

    private synchronized PublishQueue publishQueue() {
        return subscriptionManager.getPublishQueue();
    }

    private long currentSequenceNumber() {
        return sequenceNumber.get();
    }

    private long nextSequenceNumber() {
        return sequenceNumber.getAndIncrement();
    }

    void resetLifetimeCounter() {
        lifetimeCounter = lifetimeCount;

        logger.debug("[id={}] lifetime counter reset to {}", subscriptionId, lifetimeCounter);
    }

    private void resetKeepAliveCounter() {
        keepAliveCounter = maxKeepAliveCount;

        logger.debug("[id={}] keep-alive counter reset to {}", subscriptionId, maxKeepAliveCount);
    }

    private void returnKeepAlive(ServiceRequest<PublishRequest, PublishResponse> service) {
        ResponseHeader header = service.createResponseHeader();

        UInteger sequenceNumber = uint(currentSequenceNumber());

        NotificationMessage notificationMessage = new NotificationMessage(sequenceNumber, DateTime.now(),
                new ExtensionObject[0]);

        UInteger[] available = getAvailableSequenceNumbers();

        UInteger requestHandle = service.getRequest().getRequestHeader().getRequestHandle();
        StatusCode[] acknowledgeResults = subscriptionManager.getAcknowledgeResults(requestHandle);

        PublishResponse response = new PublishResponse(header, subscriptionId, available, moreNotifications,
                notificationMessage, acknowledgeResults, new DiagnosticInfo[0]);

        service.setResponse(response);

        logger.debug("[id={}] returned keep-alive NotificationMessage sequenceNumber={}.", subscriptionId,
                sequenceNumber);
    }

    void returnStatusChangeNotification(ServiceRequest<PublishRequest, PublishResponse> service) {
        StatusChangeNotification statusChange = new StatusChangeNotification(
                new StatusCode(StatusCodes.Bad_Timeout), null);

        UInteger sequenceNumber = uint(nextSequenceNumber());

        NotificationMessage notificationMessage = new NotificationMessage(sequenceNumber, new DateTime(),
                new ExtensionObject[] { ExtensionObject.encode(statusChange) });

        ResponseHeader header = service.createResponseHeader();

        PublishResponse response = new PublishResponse(header, subscriptionId, new UInteger[0], false,
                notificationMessage, new StatusCode[0], new DiagnosticInfo[0]);

        service.setResponse(response);

        logger.debug("[id={}] returned StatusChangeNotification sequenceNumber={}.", subscriptionId,
                sequenceNumber);
    }

    private void returnNotifications(ServiceRequest<PublishRequest, PublishResponse> service) {
        LinkedHashSet<BaseMonitoredItem<?>> items = new LinkedHashSet<>();

        lastIterator.forEachRemaining(items::add);

        itemsById.values().stream().filter(item -> item.hasNotifications() || item.isTriggered())
                .forEach(items::add);

        PeekingIterator<BaseMonitoredItem<?>> iterator = Iterators.peekingIterator(items.iterator());

        gatherAndSend(iterator, Optional.of(service));

        lastIterator = iterator.hasNext() ? iterator : Iterators.emptyIterator();
    }

    /**
     * Gather {@link MonitoredItemNotification}s and send them using {@code service}, if present.
     *
     * @param iterator a {@link PeekingIterator} over the current {@link BaseMonitoredItem}s.
     * @param service  a {@link ServiceRequest}, if available.
     */
    private void gatherAndSend(PeekingIterator<BaseMonitoredItem<?>> iterator,
            Optional<ServiceRequest<PublishRequest, PublishResponse>> service) {

        if (service.isPresent()) {
            List<UaStructure> notifications = Lists.newArrayList();

            while (notifications.size() < maxNotificationsPerPublish && iterator.hasNext()) {
                BaseMonitoredItem<?> item = iterator.peek();

                boolean gatheredAllForItem = gather(item, notifications, maxNotificationsPerPublish);

                if (gatheredAllForItem && iterator.hasNext()) {
                    iterator.next();
                }
            }

            moreNotifications = iterator.hasNext();

            sendNotifications(service.get(), notifications);

            if (moreNotifications) {
                gatherAndSend(iterator, Optional.ofNullable(publishQueue().poll()));
            }
        } else {
            if (moreNotifications) {
                publishQueue().addSubscription(this);
            }
        }
    }

    private boolean gather(BaseMonitoredItem<?> item, List<UaStructure> notifications, int maxNotifications) {
        int max = maxNotifications - notifications.size();

        return item.getNotifications(notifications, max);
    }

    private void sendNotifications(ServiceRequest<PublishRequest, PublishResponse> service,
            List<UaStructure> notifications) {

        List<MonitoredItemNotification> dataNotifications = Lists.newArrayList();
        List<EventFieldList> eventNotifications = Lists.newArrayList();

        notifications.forEach(notification -> {
            if (notification instanceof MonitoredItemNotification) {
                dataNotifications.add((MonitoredItemNotification) notification);
            } else if (notification instanceof EventFieldList) {
                eventNotifications.add((EventFieldList) notification);
            }
        });

        List<ExtensionObject> notificationData = Lists.newArrayList();

        if (dataNotifications.size() > 0) {
            DataChangeNotification dataChange = new DataChangeNotification(
                    dataNotifications.toArray(new MonitoredItemNotification[dataNotifications.size()]),
                    new DiagnosticInfo[0]);

            notificationData.add(ExtensionObject.encode(dataChange));
        }

        if (eventNotifications.size() > 0) {
            EventNotificationList eventChange = new EventNotificationList(
                    eventNotifications.toArray(new EventFieldList[eventNotifications.size()]));

            notificationData.add(ExtensionObject.encode(eventChange));
        }

        UInteger sequenceNumber = uint(nextSequenceNumber());

        NotificationMessage notificationMessage = new NotificationMessage(sequenceNumber, new DateTime(),
                notificationData.toArray(new ExtensionObject[notificationData.size()]));

        availableMessages.put(notificationMessage.getSequenceNumber(), notificationMessage);
        UInteger[] available = getAvailableSequenceNumbers();

        UInteger requestHandle = service.getRequest().getRequestHeader().getRequestHandle();
        StatusCode[] acknowledgeResults = subscriptionManager.getAcknowledgeResults(requestHandle);

        ResponseHeader header = service.createResponseHeader();

        PublishResponse response = new PublishResponse(header, subscriptionId, available, moreNotifications,
                notificationMessage, acknowledgeResults, new DiagnosticInfo[0]);

        service.setResponse(response);

        logger.debug(
                "[id={}] returning {} DataChangeNotification(s) and {} EventNotificationList(s) sequenceNumber={}.",
                subscriptionId, dataNotifications.size(), eventNotifications.size(), sequenceNumber);
    }

    private boolean notificationsAvailable() {
        return itemsById.values().stream().anyMatch(item -> item.hasNotifications() || item.isTriggered());
    }

    private void setState(State state) {
        State previousState = this.state.getAndSet(state);

        logger.debug("[id={}] {} -> {}", subscriptionId, previousState, state);

        StateListener listener = stateListener.get();

        if (listener != null) {
            listener.onStateChange(this, previousState, state);
        }
    }

    public UInteger getId() {
        return subscriptionId;
    }

    public double getPublishingInterval() {
        return publishingInterval;
    }

    public long getMaxKeepAliveCount() {
        return maxKeepAliveCount;
    }

    public long getLifetimeCount() {
        return lifetimeCount;
    }

    public int getMaxNotificationsPerPublish() {
        return maxNotificationsPerPublish;
    }

    public boolean isPublishingEnabled() {
        return publishingEnabled;
    }

    public int getPriority() {
        return priority;
    }

    public synchronized UInteger[] getAvailableSequenceNumbers() {
        Set<UInteger> uIntegers = availableMessages.keySet();
        UInteger[] available = uIntegers.toArray(new UInteger[uIntegers.size()]);
        Arrays.sort(available);
        return available;
    }

    public synchronized SubscriptionManager getSubscriptionManager() {
        return subscriptionManager;
    }

    public synchronized void setSubscriptionManager(SubscriptionManager subscriptionManager) {
        this.subscriptionManager = subscriptionManager;
    }

    public Session getSession() {
        return subscriptionManager.getSession();
    }

    public long nextItemId() {
        return itemIds.getAndIncrement();
    }

    public void setStateListener(StateListener listener) {
        stateListener.set(listener);
    }

    /**
     * Handle an incoming {@link PublishRequest}.
     *
     * @param service The service request that contains the {@link PublishRequest}.
     */
    synchronized void onPublish(ServiceRequest<PublishRequest, PublishResponse> service) {
        State state = this.state.get();

        logger.trace("[id={}] onPublish(), state={}, keep-alive={}, lifetime={}", subscriptionId, state,
                keepAliveCounter, lifetimeCounter);

        if (state == State.Normal)
            publishHandler.whenNormal(service);
        else if (state == State.KeepAlive)
            publishHandler.whenKeepAlive(service);
        else if (state == State.Late)
            publishHandler.whenLate(service);
        else if (state == State.Closing)
            publishHandler.whenClosing(service);
        else if (state == State.Closed)
            publishHandler.whenClosed(service);
        else
            throw new RuntimeException("Unhandled subscription state: " + state);
    }

    /**
     * The publishing timer has elapsed.
     */
    synchronized void onPublishingTimer() {
        State state = this.state.get();

        logger.trace("[id={}] onPublishingTimer(), state={}, keep-alive={}, lifetime={}", subscriptionId, state,
                keepAliveCounter, lifetimeCounter);

        if (state == State.Normal)
            timerHandler.whenNormal();
        else if (state == State.KeepAlive)
            timerHandler.whenKeepAlive();
        else if (state == State.Late)
            timerHandler.whenLate();
        else if (state == State.Closed)
            logger.debug("[id={}] onPublish(), state={}", subscriptionId, state); // No-op.
        else
            throw new RuntimeException("unhandled subscription state: " + state);
    }

    synchronized void startPublishingTimer() {
        if (state.get() == State.Closed)
            return;

        lifetimeCounter--;

        if (lifetimeCounter < 1) {
            logger.debug("[id={}] lifetime expired.", subscriptionId);

            setState(State.Closing);
        } else {
            long interval = DoubleMath.roundToLong(publishingInterval, RoundingMode.UP);

            subscriptionManager.getServer().getScheduledExecutorService().schedule(this::onPublishingTimer,
                    interval, TimeUnit.MILLISECONDS);
        }
    }

    public synchronized StatusCode acknowledge(UInteger sequenceNumber) {
        if (availableMessages.remove(sequenceNumber) != null) {
            logger.debug("[id={}] sequence number acknowledged: {}", subscriptionId, sequenceNumber);

            return StatusCode.GOOD;
        } else {
            logger.debug("[id={}] sequence number unknown: {}", subscriptionId, sequenceNumber);

            return new StatusCode(StatusCodes.Bad_SequenceNumberUnknown);
        }
    }

    public synchronized NotificationMessage republish(UInteger sequenceNumber) {
        resetLifetimeCounter();

        return availableMessages.get(sequenceNumber);
    }

    private class PublishHandler {
        private void whenNormal(ServiceRequest<PublishRequest, PublishResponse> service) {
            boolean publishingEnabled = Subscription.this.publishingEnabled;

            /* Subscription State Table Row 4 */
            if (!publishingEnabled || (publishingEnabled && !moreNotifications)) {
                publishQueue().addRequest(service);
            }
            /* Subscription State Table Row 5 */
            else if (publishingEnabled && moreNotifications) {
                resetLifetimeCounter();
                returnNotifications(service);
                messageSent = true;
            } else {
                throw new IllegalStateException("unhandled subscription state");
            }
        }

        private void whenLate(ServiceRequest<PublishRequest, PublishResponse> service) {
            boolean publishingEnabled = Subscription.this.publishingEnabled;
            boolean notificationsAvailable = notificationsAvailable();

            /* Subscription State Table Row 10 */
            if (publishingEnabled && (notificationsAvailable || moreNotifications)) {
                setState(State.Normal);
                resetLifetimeCounter();
                returnNotifications(service);
                messageSent = true;
            }
            /* Subscription State Table Row 11 */
            else if (!publishingEnabled || (publishingEnabled && !notificationsAvailable && !moreNotifications)) {
                setState(State.KeepAlive);
                resetLifetimeCounter();
                returnKeepAlive(service);
                messageSent = true;
            } else {
                throw new IllegalStateException("unhandled subscription state");
            }
        }

        private void whenKeepAlive(ServiceRequest<PublishRequest, PublishResponse> service) {
            /* Subscription State Table Row 13 */
            publishQueue().addRequest(service);
        }

        private void whenClosing(ServiceRequest<PublishRequest, PublishResponse> service) {
            returnStatusChangeNotification(service);

            setState(State.Closed);
        }

        private void whenClosed(ServiceRequest<PublishRequest, PublishResponse> service) {
            publishQueue().addRequest(service);
        }
    }

    private class TimerHandler {
        private void whenNormal() {
            boolean publishRequestQueued = publishQueue().isNotEmpty();
            boolean publishingEnabled = Subscription.this.publishingEnabled;
            boolean notificationsAvailable = notificationsAvailable();

            /* Subscription State Table Row 6 */
            if (publishRequestQueued && publishingEnabled && notificationsAvailable) {
                Optional<ServiceRequest<PublishRequest, PublishResponse>> service = Optional
                        .ofNullable(publishQueue().poll());

                if (service.isPresent()) {
                    resetLifetimeCounter();
                    returnNotifications(service.get());
                    messageSent = true;
                    startPublishingTimer();
                } else {
                    whenNormal();
                }
            }
            /* Subscription State Table Row 7 */
            else if (publishRequestQueued && !messageSent
                    && (!publishingEnabled || (publishingEnabled && !notificationsAvailable))) {
                Optional<ServiceRequest<PublishRequest, PublishResponse>> service = Optional
                        .ofNullable(publishQueue().poll());

                if (service.isPresent()) {
                    resetLifetimeCounter();
                    returnKeepAlive(service.get());
                    messageSent = true;
                    startPublishingTimer();
                } else {
                    whenNormal();
                }
            }
            /* Subscription State Table Row 8 */
            else if (!publishRequestQueued && (!messageSent || (publishingEnabled && notificationsAvailable))) {
                setState(State.Late);
                startPublishingTimer();

                publishQueue().addSubscription(Subscription.this);
            }
            /* Subscription State Table Row 9 */
            else if (messageSent && (!publishingEnabled || (publishingEnabled && !notificationsAvailable))) {
                setState(State.KeepAlive);
                resetKeepAliveCounter();
                startPublishingTimer();
            } else {
                throw new IllegalStateException("unhandled subscription state");
            }
        }

        private void whenLate() {
            /* Subscription State Table Row 12 */
            startPublishingTimer();
        }

        private void whenKeepAlive() {
            boolean publishingEnabled = Subscription.this.publishingEnabled;
            boolean notificationsAvailable = notificationsAvailable();
            boolean publishRequestQueued = publishQueue().isNotEmpty();

            /* Subscription State Table Row 14 */
            if (publishingEnabled && notificationsAvailable && publishRequestQueued) {
                Optional<ServiceRequest<PublishRequest, PublishResponse>> service = Optional
                        .ofNullable(publishQueue().poll());

                if (service.isPresent()) {
                    setState(State.Normal);
                    resetLifetimeCounter();
                    returnNotifications(service.get());
                    messageSent = true;
                    startPublishingTimer();
                } else {
                    whenKeepAlive();
                }
            }
            /* Subscription State Table Row 15 */
            else if (publishRequestQueued && keepAliveCounter == 1
                    && (!publishingEnabled || (publishingEnabled && !notificationsAvailable))) {

                Optional<ServiceRequest<PublishRequest, PublishResponse>> service = Optional
                        .ofNullable(publishQueue().poll());

                if (service.isPresent()) {
                    returnKeepAlive(service.get());
                    resetLifetimeCounter();
                    resetKeepAliveCounter();
                    startPublishingTimer();
                } else {
                    whenKeepAlive();
                }
            }
            /* Subscription State Table Row 16 */
            else if (keepAliveCounter > 1
                    && (!publishingEnabled || (publishingEnabled && !notificationsAvailable))) {

                keepAliveCounter--;
                startPublishingTimer();
            }
            /* Subscription State Table Row 17 */
            else if (!publishRequestQueued && (keepAliveCounter == 1
                    || (keepAliveCounter > 1 && publishingEnabled && notificationsAvailable))) {

                setState(State.Late);
                startPublishingTimer();

                publishQueue().addSubscription(Subscription.this);
            }
        }
    }

    public static enum State {
        Closing, Closed, Normal, KeepAlive, Late
    }

    public static interface StateListener {
        void onStateChange(Subscription subscription, State previousState, State currentState);
    }

}