/*******************************************************************************
* Copyright (c) 2016 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.openshift.internal.restclient.okhttp;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang.StringUtils;
import org.jboss.dmr.ModelNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.openshift.internal.restclient.DefaultClient;
import com.openshift.internal.restclient.URLBuilder;
import com.openshift.internal.restclient.model.properties.ResourcePropertyKeys;
import com.openshift.restclient.IApiTypeMapper;
import com.openshift.restclient.IClient;
import com.openshift.restclient.IOpenShiftWatchListener;
import com.openshift.restclient.IOpenShiftWatchListener.ChangeType;
import com.openshift.restclient.IWatcher;
import com.openshift.restclient.OpenShiftException;
import com.openshift.restclient.http.IHttpConstants;
import com.openshift.restclient.model.IList;
import com.openshift.restclient.model.IResource;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;
public class WatchClient implements IWatcher, IHttpConstants {
private static final Logger LOGGER = LoggerFactory.getLogger(WatchClient.class);
private DefaultClient client;
private OkHttpClient okClient;
private AtomicReference<Status> status = new AtomicReference<>(Status.Stopped);
private IApiTypeMapper typeMappings;
private Map<String, WatchEndpoint> endpointMap = Collections.synchronizedMap(new HashMap<>());
private enum Status {
Started,
Starting,
Stopped,
Stopping
}
public WatchClient(DefaultClient client, IApiTypeMapper typeMapper, OkHttpClient okClient) {
this.client = client;
this.typeMappings = typeMapper;
this.okClient = okClient;
}
@Override
public void stop() {
if(status.compareAndSet(Status.Started, Status.Stopping)) {
Map<String, WatchEndpoint> endpoints = new HashMap<>(endpointMap);
endpointMap.clear();
endpoints.values().forEach(w->w.close());
status.set(Status.Stopped);
}
}
public IWatcher watch(Collection<String> kinds, String namespace, IOpenShiftWatchListener listener) {
if(status.compareAndSet(Status.Stopped, Status.Starting)) {
try {
for (String kind : kinds) {
WatchEndpoint socket = new WatchEndpoint(client, listener, kind);
final String resourceVersion = getResourceVersion(kind, namespace, socket);
final String endpoint = new URLBuilder(client.getBaseURL(), this.typeMappings)
.kind(kind)
.namespace(namespace)
.watch()
.addParmeter(ResourcePropertyKeys.RESOURCE_VERSION, resourceVersion)
.websocket();
Request request = client.newRequestBuilderTo(endpoint)
.header(PROPERTY_ORIGIN, client.getBaseURL().toString())
.header(PROPERTY_USER_AGENT, "openshift-restclient-java")
.build();
WebSocketCall call = WebSocketCall.create(okClient.newBuilder().build(), request);
socket.setCall(call);
endpointMap.put(kind, socket);
call.enqueue(socket);
}
status.set(Status.Started);
} catch (Exception e) {
endpointMap.clear();
status.set(Status.Stopped);
try {
throw ResponseCodeInterceptor.createOpenShiftException(client, 0, String.format("Could not watch resources in namespace %s: %s", namespace, e.getMessage()), null, e);
} catch (IOException e1) {
throw new OpenShiftException(e1, "IOException trying to create an OpenShift specific exception");
}
}
}
return this;
}
private String getResourceVersion(String kind, String namespace, WatchEndpoint endpoint) throws Exception{
IList list = client.get(kind, namespace);
Collection<IResource> items = list.getItems();
List<IResource> resources = new ArrayList<>(items.size());
resources.addAll(items);
endpoint.setResources(resources);
return list.getResourceVersion();
}
static class WatchEndpoint implements WebSocketListener{
private IOpenShiftWatchListener listener;
private List<IResource> resources;
private final String kind;
private final IClient client;
private WebSocket wsClient;
private WebSocketCall call;
public WatchEndpoint(IClient client, IOpenShiftWatchListener listener, String kind) {
this.listener = listener;
this.kind = kind;
this.client = client;
}
public void setCall(WebSocketCall call) {
this.call = call;
}
void close() {
try {
if(wsClient != null) {
wsClient.close(STATUS_NORMAL_STOP, "Client was asked to stop.");
wsClient = null;
}
if(call != null) {
call.cancel();
}
listener.disconnected();
} catch (Exception e) {
LOGGER.debug("Unable to stop the watch client",e);
}finally {
wsClient = null;
}
}
public void setResources(List<IResource> resources) {
this.resources = resources;
}
@Override
public void onClose(int statusCode, String reason) {
LOGGER.debug("WatchSocket closed for kind: {}, code: {}, reason: {}", new Object[]{kind, statusCode, reason});
listener.disconnected();
}
@Override
public void onFailure(IOException err, Response response) {
LOGGER.debug("WatchSocket Error for kind {}: {}", kind, err);
try {
if (response == null) {
listener.error(ResponseCodeInterceptor.createOpenShiftException(client, 0, "", "", err));
} else if (response.code() == IHttpConstants.STATUS_OK && err instanceof ProtocolException) {
// Just swallow it. Means the feature isn't supported in this OS server version yet.
// WebSocket creates error "Expected HTTP 101 response but was '200 OK'"
// This is described in the web socket specification.
LOGGER.debug("The feature isn't supported", err);
} else {
listener.error(ResponseCodeInterceptor.createOpenShiftException(client, response.code(),
response.body().string(), response.request().url().toString(), err));
}
} catch (IOException e) {
LOGGER.error("IOException trying to notify listener of specific OpenShiftException", err);
listener.error(err);
}
}
@Override
public void onMessage(ResponseBody body) throws IOException {
String message = body.string();
LOGGER.debug(message);
ModelNode node = ModelNode.fromJSONString(message);
IOpenShiftWatchListener.ChangeType event = new ChangeType(node.get("type").asString());
IResource resource = client.getResourceFactory().create(node.get("object").toJSONString(true));
if(StringUtils.isEmpty(resource.getKind())) {
LOGGER.error("Unable to determine resource kind from: " + node.get("object").toJSONString(false));
}
listener.received(resource, event);
}
@Override
public void onOpen(WebSocket socket, Response response) {
LOGGER.debug("WatchSocket connected for {}", kind);
wsClient = socket;
listener.connected(resources);
}
@Override
public void onPong(Buffer buffer) {
}
}
}