/*
* Copyright 2014-2016 CyberVision, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kaaproject.kaa.client.channel.impl.channels;
import org.kaaproject.kaa.client.AbstractKaaClient;
import org.kaaproject.kaa.client.channel.ChannelDirection;
import org.kaaproject.kaa.client.channel.IpTransportInfo;
import org.kaaproject.kaa.client.channel.KaaDataChannel;
import org.kaaproject.kaa.client.channel.KaaDataDemultiplexer;
import org.kaaproject.kaa.client.channel.KaaDataMultiplexer;
import org.kaaproject.kaa.client.channel.ServerType;
import org.kaaproject.kaa.client.channel.TransportConnectionInfo;
import org.kaaproject.kaa.client.channel.TransportProtocolId;
import org.kaaproject.kaa.client.channel.TransportProtocolIdConstants;
import org.kaaproject.kaa.client.channel.connectivity.ConnectivityChecker;
import org.kaaproject.kaa.client.channel.failover.FailoverManager;
import org.kaaproject.kaa.client.channel.failover.FailoverStatus;
import org.kaaproject.kaa.client.channel.impl.channels.polling.CancelableCommandRunnable;
import org.kaaproject.kaa.client.channel.impl.channels.polling.CancelableRunnable;
import org.kaaproject.kaa.client.channel.impl.channels.polling.CancelableScheduledFuture;
import org.kaaproject.kaa.client.channel.impl.channels.polling.PollCommand;
import org.kaaproject.kaa.client.channel.impl.channels.polling.RawDataProcessor;
import org.kaaproject.kaa.client.persistence.KaaClientState;
import org.kaaproject.kaa.client.transport.AbstractHttpClient;
import org.kaaproject.kaa.common.TransportType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public class DefaultOperationsChannel implements KaaDataChannel, RawDataProcessor {
public static final Logger LOG = LoggerFactory // NOSONAR
.getLogger(DefaultOperationsChannel.class);
private static final Map<TransportType, ChannelDirection> SUPPORTED_TYPES = new HashMap<>();
private static final String CHANNEL_ID = "default_operations_long_poll_channel";
static {
SUPPORTED_TYPES.put(TransportType.PROFILE, ChannelDirection.BIDIRECTIONAL);
SUPPORTED_TYPES.put(TransportType.CONFIGURATION, ChannelDirection.BIDIRECTIONAL);
SUPPORTED_TYPES.put(TransportType.NOTIFICATION, ChannelDirection.BIDIRECTIONAL);
SUPPORTED_TYPES.put(TransportType.USER, ChannelDirection.BIDIRECTIONAL);
SUPPORTED_TYPES.put(TransportType.EVENT, ChannelDirection.DOWN);
}
private final Object httpClientLock = new Object();
private final Object httpClientSetLock = new Object();
private final AbstractKaaClient client;
private final KaaClientState state;
private final FailoverManager failoverManager;
private AbstractHttpClient httpClient;
private KaaDataDemultiplexer demultiplexer;
private KaaDataMultiplexer multiplexer;
private IpTransportInfo currentServer;
private ScheduledExecutorService scheduler;
private volatile Future<?> pollFuture;
private volatile boolean stopped = true;
private volatile boolean processingResponse = false;
private volatile boolean taskPosted = false;
private final CancelableCommandRunnable task = new CancelableCommandRunnable() {
@Override
protected void executeCommand() {
if (!stopped) {
taskPosted = false;
synchronized (httpClientSetLock) {
while (httpClient == null && !stopped && !Thread.currentThread().isInterrupted()) {
try {
httpClientSetLock.wait();
} catch (InterruptedException ex) {
break;
}
}
}
if (!stopped) {
currentCommand = new PollCommand(httpClient, DefaultOperationsChannel.this,
getSupportedTransportTypes(), currentServer);
if (!Thread.currentThread().isInterrupted()) {
currentCommand.execute();
}
currentCommand = null;
if (!taskPosted && !stopped && !Thread.currentThread().isInterrupted()) {
taskPosted = true;
pollFuture = scheduler.submit(task);
}
}
}
}
};
private volatile boolean isShutdown = false;
private volatile boolean isPaused = false;
/**
* All-args constructor.
*/
public DefaultOperationsChannel(AbstractKaaClient client, KaaClientState state,
FailoverManager failoverManager) {
this.client = client;
this.state = state;
this.failoverManager = failoverManager;
}
protected ScheduledExecutorService createExecutor() {
LOG.info("Creating a new executor for channel [{}]", getId());
return new ScheduledThreadPoolExecutor(1) {
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable,
RunnableScheduledFuture<V> task) {
if (runnable instanceof CancelableRunnable) {
return new CancelableScheduledFuture<V>((CancelableRunnable) runnable, task);
}
return super.decorateTask(runnable, task);
}
};
}
private void stopPollScheduler(boolean forced) {
if (!stopped) {
stopped = true;
if (!processingResponse && pollFuture != null) {
LOG.info("Stopping poll future..");
pollFuture.cancel(forced);
if (forced) {
task.waitUntilExecuted();
}
LOG.info("Poll scheduler stopped");
}
}
}
private void startPoll() {
if (!stopped) {
stopPollScheduler(true);
}
if (scheduler == null) {
scheduler = createExecutor();
}
stopped = false;
LOG.info("Starting poll scheduler..");
taskPosted = true;
pollFuture = scheduler.submit(task);
LOG.info("Poll scheduler started");
}
private void stopPoll() {
stopPollScheduler(true);
}
@Override
public LinkedHashMap<String, byte[]> createRequest(
Map<TransportType, ChannelDirection> types) { // NOSONAR
LinkedHashMap<String, byte[]> request = null;
try {
byte[] requestBodyRaw = multiplexer.compileRequest(types);
synchronized (httpClientLock) {
request = HttpRequestCreator.createOperationHttpRequest(requestBodyRaw,
httpClient.getEncoderDecoder());
}
} catch (Exception ex) {
LOG.error("Failed to create request {}", ex);
}
return request;
}
@Override
public void onResponse(byte[] response) {
LOG.debug("Response for channel [{}] received", getId());
byte[] decodedResponse;
try {
processingResponse = true;
synchronized (httpClientLock) {
decodedResponse = httpClient.getEncoderDecoder().decodeData(response);
}
demultiplexer.processResponse(decodedResponse);
processingResponse = false;
failoverManager.onServerConnected(currentServer);
} catch (Exception ex) {
LOG.error("Failed to process response {}", Arrays.toString(response));
LOG.error("Exception stack trace: ", ex);
}
}
@Override
public void onServerError(TransportConnectionInfo info) {
if (!stopped) {
LOG.debug("Channel [{}] connection failed", getId());
synchronized (this) {
stopPollScheduler(false);
}
failoverManager.onServerFailed(info, FailoverStatus.NO_CONNECTIVITY);
} else {
LOG.debug("Channel [{}] connection aborted", getId());
}
}
@Override
public synchronized void sync(TransportType type) {
sync(Collections.singleton(type));
}
@Override
public synchronized void sync(Set<TransportType> types) {
if (isShutdown) {
LOG.info("Can't sync. Channel [{}] is down", getId());
return;
}
if (isPaused) {
LOG.info("Can't sync. Channel [{}] is paused", getId());
return;
}
if (multiplexer == null) {
LOG.warn("Can't sync. Channel {} multiplexer is not set", getId());
return;
}
if (demultiplexer == null) {
LOG.warn("Can't sync. Channel {} demultiplexer is not set", getId());
return;
}
if (currentServer == null) {
LOG.warn("Can't sync. Server is null");
}
for (TransportType type : types) {
LOG.info("Processing sync {} for channel [{}]", type, getId());
if (getSupportedTransportTypes().get(type) == null) {
LOG.error("Unsupported type {} for channel [{}]", type, getId());
return;
}
}
stopPoll();
startPoll();
}
@Override
public synchronized void syncAll() {
if (isShutdown) {
LOG.info("Can't sync. Channel [{}] is down", getId());
return;
}
if (isPaused) {
LOG.info("Can't sync. Channel [{}] is paused", getId());
return;
}
LOG.info("Processing sync all for channel [{}]", getId());
if (multiplexer != null && demultiplexer != null) {
if (currentServer != null) {
stopPoll();
startPoll();
} else {
LOG.warn("Can't sync. Server is null");
}
}
}
@Override
public void syncAck(TransportType type) {
syncAck(Collections.singleton(type));
}
@Override
public void syncAck(Set<TransportType> types) {
LOG.info("Sync ack message is ignored for Channel {}", getId());
}
@Override
public String getId() {
return CHANNEL_ID;
}
@Override
public TransportProtocolId getTransportProtocolId() {
return TransportProtocolIdConstants.HTTP_TRANSPORT_ID;
}
@Override
public ServerType getServerType() {
return ServerType.OPERATIONS;
}
@Override
public synchronized void setDemultiplexer(KaaDataDemultiplexer demultiplexer) {
if (demultiplexer != null) {
this.demultiplexer = demultiplexer;
}
}
@Override
public synchronized void setMultiplexer(KaaDataMultiplexer multiplexer) {
if (multiplexer != null) {
this.multiplexer = multiplexer;
}
}
@Override
public TransportConnectionInfo getServer() {
return currentServer;
}
// TODO: refactor this as part of KAA-126
@Override
public synchronized void setServer(TransportConnectionInfo server) {
if (isShutdown) {
LOG.info("Can't set server. Channel [{}] is down", getId());
return;
}
if (server != null) {
if (!isPaused) {
stopPoll();
}
this.currentServer = new IpTransportInfo(server);
synchronized (httpClientLock) {
LOG.debug("Channel [{}]: creating HTTP client..", getId());
this.httpClient = client.createHttpClient(currentServer.getUrl() + "/EP/LongSync",
state.getPrivateKey(), state.getPublicKey(), currentServer.getPublicKey());
synchronized (httpClientSetLock) {
httpClientSetLock.notifyAll();
}
LOG.debug("Channel [{}]: HTTP client created", getId());
}
if (!isPaused) {
startPoll();
}
}
}
@Override
public void setConnectivityChecker(ConnectivityChecker checker) {
// Do nothing
}
@Override
public synchronized void shutdown() {
if (!isShutdown) {
isShutdown = true;
stopPoll();
if (scheduler != null) {
scheduler.shutdownNow();
}
}
}
@Override
public synchronized void pause() {
if (isShutdown) {
LOG.info("Can't pause channel. Channel [{}] is down", getId());
return;
}
if (!isPaused) {
isPaused = true;
stopPoll();
if (scheduler != null) {
scheduler.shutdownNow();
scheduler = null;
}
}
}
@Override
public synchronized void resume() {
if (isShutdown) {
LOG.info("Can't resume channel. Channel [{}] is down", getId());
return;
}
if (isPaused) {
isPaused = false;
startPoll();
}
}
@Override
public Map<TransportType, ChannelDirection> getSupportedTransportTypes() {
return SUPPORTED_TYPES;
}
}