/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.zeppelin.notebook.repo.zeppelinhub;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.Note;
import org.apache.zeppelin.notebook.NoteInfo;
import org.apache.zeppelin.notebook.repo.NotebookRepo;
import org.apache.zeppelin.notebook.repo.NotebookRepoSettingsInfo;
import org.apache.zeppelin.notebook.repo.zeppelinhub.model.Instance;
import org.apache.zeppelin.notebook.repo.zeppelinhub.model.UserTokenContainer;
import org.apache.zeppelin.notebook.repo.zeppelinhub.model.UserSessionContainer;
import org.apache.zeppelin.notebook.repo.zeppelinhub.rest.ZeppelinhubRestApiHandler;
import org.apache.zeppelin.notebook.repo.zeppelinhub.websocket.Client;
import org.apache.zeppelin.notebook.repo.zeppelinhub.websocket.utils.ZeppelinhubUtils;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
/**
* ZeppelinHub repo class.
*/
public class ZeppelinHubRepo implements NotebookRepo {
private static final Logger LOG = LoggerFactory.getLogger(ZeppelinHubRepo.class);
private static final String DEFAULT_SERVER = "https://www.zeppelinhub.com";
static final String ZEPPELIN_CONF_PROP_NAME_SERVER = "zeppelinhub.api.address";
static final String ZEPPELIN_CONF_PROP_NAME_TOKEN = "zeppelinhub.api.token";
public static final String TOKEN_HEADER = "X-Zeppelin-Token";
private static final Gson GSON = new Gson();
private static final Note EMPTY_NOTE = new Note();
private final Client websocketClient;
private final UserTokenContainer tokenManager;
private String token;
private ZeppelinhubRestApiHandler restApiClient;
private final ZeppelinConfiguration conf;
public ZeppelinHubRepo(ZeppelinConfiguration conf) {
this.conf = conf;
String zeppelinHubUrl = getZeppelinHubUrl(conf);
LOG.info("Initializing ZeppelinHub integration module");
token = conf.getString("ZEPPELINHUB_API_TOKEN", ZEPPELIN_CONF_PROP_NAME_TOKEN, "");
restApiClient = ZeppelinhubRestApiHandler.newInstance(zeppelinHubUrl);
//TODO(khalid): check which realm for authentication, pass to token manager
tokenManager = UserTokenContainer.init(restApiClient, token);
websocketClient = Client.initialize(getZeppelinWebsocketUri(conf),
getZeppelinhubWebsocketUri(conf), token, conf);
websocketClient.start();
}
private String getZeppelinHubWsUri(URI api) throws URISyntaxException {
URI apiRoot = api;
String scheme = apiRoot.getScheme();
int port = apiRoot.getPort();
if (port <= 0) {
port = (scheme != null && scheme.equals("https")) ? 443 : 80;
}
if (scheme == null) {
LOG.info("{} is not a valid zeppelinhub server address. proceed with default address {}",
apiRoot, DEFAULT_SERVER);
apiRoot = new URI(DEFAULT_SERVER);
scheme = apiRoot.getScheme();
port = apiRoot.getPort();
if (port <= 0) {
port = (scheme != null && scheme.equals("https")) ? 443 : 80;
}
}
String ws = scheme.equals("https") ? "wss://" : "ws://";
return ws + apiRoot.getHost() + ":" + port + "/async";
}
String getZeppelinhubWebsocketUri(ZeppelinConfiguration conf) {
String zeppelinHubUri = StringUtils.EMPTY;
try {
zeppelinHubUri = getZeppelinHubWsUri(new URI(conf.getString("ZEPPELINHUB_API_ADDRESS",
ZEPPELIN_CONF_PROP_NAME_SERVER, DEFAULT_SERVER)));
} catch (URISyntaxException e) {
LOG.error("Cannot get ZeppelinHub URI", e);
}
return zeppelinHubUri;
}
private String getZeppelinWebsocketUri(ZeppelinConfiguration conf) {
int port = conf.getServerPort();
if (port <= 0) {
port = 80;
}
String ws = conf.useSsl() ? "wss" : "ws";
return ws + "://localhost:" + port + "/ws";
}
// Used in tests
void setZeppelinhubRestApiHandler(ZeppelinhubRestApiHandler zeppelinhub) {
restApiClient = zeppelinhub;
}
String getZeppelinHubUrl(ZeppelinConfiguration conf) {
if (conf == null) {
LOG.error("Invalid configuration, cannot be null. Using default address {}", DEFAULT_SERVER);
return DEFAULT_SERVER;
}
URI apiRoot;
String zeppelinhubUrl;
try {
String url = conf.getString("ZEPPELINHUB_API_ADDRESS",
ZEPPELIN_CONF_PROP_NAME_SERVER,
DEFAULT_SERVER);
apiRoot = new URI(url);
} catch (URISyntaxException e) {
LOG.error("Invalid zeppelinhub url, using default address {}", DEFAULT_SERVER, e);
return DEFAULT_SERVER;
}
String scheme = apiRoot.getScheme();
if (scheme == null) {
LOG.info("{} is not a valid zeppelinhub server address. proceed with default address {}",
apiRoot, DEFAULT_SERVER);
zeppelinhubUrl = DEFAULT_SERVER;
} else {
zeppelinhubUrl = scheme + "://" + apiRoot.getHost();
if (apiRoot.getPort() > 0) {
zeppelinhubUrl += ":" + apiRoot.getPort();
}
}
return zeppelinhubUrl;
}
private boolean isSubjectValid(AuthenticationInfo subject) {
if (subject == null) {
return false;
}
return (subject.isAnonymous() && !conf.isAnonymousAllowed()) ? false : true;
}
@Override
public List<NoteInfo> list(AuthenticationInfo subject) throws IOException {
if (!isSubjectValid(subject)) {
return Collections.emptyList();
}
String token = getUserToken(subject.getUser());
String response = restApiClient.get(token, StringUtils.EMPTY);
List<NoteInfo> notes = GSON.fromJson(response, new TypeToken<List<NoteInfo>>() {}.getType());
if (notes == null) {
return Collections.emptyList();
}
LOG.info("ZeppelinHub REST API listing notes ");
return notes;
}
@Override
public Note get(String noteId, AuthenticationInfo subject) throws IOException {
if (StringUtils.isBlank(noteId) || !isSubjectValid(subject)) {
return EMPTY_NOTE;
}
String token = getUserToken(subject.getUser());
String response = restApiClient.get(token, noteId);
Note note = GSON.fromJson(response, Note.class);
if (note == null) {
return EMPTY_NOTE;
}
LOG.info("ZeppelinHub REST API get note {} ", noteId);
return note;
}
@Override
public void save(Note note, AuthenticationInfo subject) throws IOException {
if (note == null || !isSubjectValid(subject)) {
throw new IOException("Zeppelinhub failed to save note");
}
String jsonNote = GSON.toJson(note);
String token = getUserToken(subject.getUser());
LOG.info("ZeppelinHub REST API saving note {} ", note.getId());
restApiClient.put(token, jsonNote);
}
@Override
public void remove(String noteId, AuthenticationInfo subject) throws IOException {
if (StringUtils.isBlank(noteId) || !isSubjectValid(subject)) {
throw new IOException("Zeppelinhub failed to remove note");
}
String token = getUserToken(subject.getUser());
LOG.info("ZeppelinHub REST API removing note {} ", noteId);
restApiClient.del(token, noteId);
}
@Override
public void close() {
websocketClient.stop();
restApiClient.close();
}
@Override
public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject)
throws IOException {
if (StringUtils.isBlank(noteId) || !isSubjectValid(subject)) {
return Revision.EMPTY;
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint");
String content = GSON.toJson(ImmutableMap.of("message", checkpointMsg));
String token = getUserToken(subject.getUser());
String response = restApiClient.putWithResponseBody(token, endpoint, content);
return GSON.fromJson(response, Revision.class);
}
@Override
public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException {
if (StringUtils.isBlank(noteId) || StringUtils.isBlank(revId) || !isSubjectValid(subject)) {
return EMPTY_NOTE;
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint", revId);
String token = getUserToken(subject.getUser());
String response = restApiClient.get(token, endpoint);
Note note = GSON.fromJson(response, Note.class);
if (note == null) {
return EMPTY_NOTE;
}
LOG.info("ZeppelinHub REST API get note {} revision {}", noteId, revId);
return note;
}
@Override
public List<Revision> revisionHistory(String noteId, AuthenticationInfo subject) {
if (StringUtils.isBlank(noteId) || !isSubjectValid(subject)) {
return Collections.emptyList();
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint");
List<Revision> history = Collections.emptyList();
try {
String token = getUserToken(subject.getUser());
String response = restApiClient.get(token, endpoint);
history = GSON.fromJson(response, new TypeToken<List<Revision>>(){}.getType());
} catch (IOException e) {
LOG.error("Cannot get note history", e);
}
return history;
}
private String getUserToken(String user) {
return tokenManager.getUserToken(user);
}
@Override
public List<NotebookRepoSettingsInfo> getSettings(AuthenticationInfo subject) {
if (!isSubjectValid(subject)) {
return Collections.emptyList();
}
List<NotebookRepoSettingsInfo> settings = Lists.newArrayList();
String user = subject.getUser();
String zeppelinHubUserSession = UserSessionContainer.instance.getSession(user);
String userToken = getUserToken(user);
List<Instance> instances;
List<Map<String, String>> values = Lists.newLinkedList();
try {
instances = tokenManager.getUserInstances(zeppelinHubUserSession);
} catch (IOException e) {
LOG.warn("Couldnt find instances for the session {}, returning empty collection",
zeppelinHubUserSession);
// user not logged
//TODO(xxx): handle this case.
instances = Collections.emptyList();
}
NotebookRepoSettingsInfo repoSetting = NotebookRepoSettingsInfo.newInstance();
repoSetting.type = NotebookRepoSettingsInfo.Type.DROPDOWN;
for (Instance instance : instances) {
if (instance.token.equals(userToken)) {
repoSetting.selected = Integer.toString(instance.id);
}
values.add(ImmutableMap.of("name", instance.name, "value", Integer.toString(instance.id)));
}
repoSetting.value = values;
repoSetting.name = "Instance";
settings.add(repoSetting);
return settings;
}
private void changeToken(int instanceId, String user) {
if (instanceId <= 0) {
LOG.error("User {} tried to switch to a non valid instance {}", user, instanceId);
return;
}
LOG.info("User {} will switch instance", user);
String ticket = UserSessionContainer.instance.getSession(user);
List<Instance> instances;
String currentToken = StringUtils.EMPTY, targetToken = StringUtils.EMPTY;
try {
instances = tokenManager.getUserInstances(ticket);
if (instances.isEmpty()) {
return;
}
currentToken = tokenManager.getExistingUserToken(user);
for (Instance instance : instances) {
if (instance.id == instanceId) {
LOG.info("User {} switched to instance {}", user, instance.name);
tokenManager.setUserToken(user, instance.token);
targetToken = instance.token;
break;
}
}
if (!StringUtils.isBlank(currentToken) && !StringUtils.isBlank(targetToken)) {
ZeppelinhubUtils.userSwitchTokenRoutine(user, currentToken, targetToken);
}
} catch (IOException e) {
LOG.error("Cannot switch instance for user {}", user, e);
}
}
@Override
public void updateSettings(Map<String, String> settings, AuthenticationInfo subject) {
if (!isSubjectValid(subject)) {
LOG.error("Invalid subject, cannot update Zeppelinhub settings");
return;
}
if (settings == null || settings.isEmpty()) {
LOG.error("Cannot update ZeppelinHub repo settings because of invalid settings");
return;
}
int instanceId = 0;
if (settings.containsKey("Instance")) {
try {
instanceId = Integer.parseInt(settings.get("Instance"));
} catch (NumberFormatException e) {
LOG.error("ZeppelinHub Instance Id in not a valid integer", e);
}
}
changeToken(instanceId, subject.getUser());
}
@Override
public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject)
throws IOException {
// Auto-generated method stub
return null;
}
}