Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
provide a UI websocket endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
jmazzitelli committed Jul 14, 2015
1 parent 8deb60e commit c0a53c3
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 30 deletions.
21 changes: 21 additions & 0 deletions modules/feed-comm/feed-comm-api/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
= Hawkular Feed Communications API

This is used for the server to communication with its feeds over WebSockets. It also is used for UI clients to talk to the server as well (presumably for the UI to manage remote resources that are managed by feeds).

This API (both requests and responses) is driven by JSON schemas so clients (both feed and UI) need not be Java in order to talk over this API to the Hawkular server. See the src/main/resources/schema folder. All schemas come with this Maven module's artifact jar inside the "schema" folder. If you have a non-Java client, you can generate your JSON based on those schemas.

If your client is Java, you can use the Java API that also comes with the artifact jar. All JSON messages (requests and responses) have Java POJOs associated with the JSON schemas. These Java POJOs are auto-generated from the schemas. All POJOs are generated in the package "org.hawkular.feedcomm.api" with the name of the POJO being the name of the schema. For example, the schema NotificationMessage.schema.json will have an associated Java POJO autogenerated with the name org.hawkular.feedcomm.api.NotificationMessage.

Because this API flows over WebSockets, multiple requests and responses may be sent and received to clients. Because of this, in order to make it easier on the client to deserialize incoming requests and responses, the messages that flow over the WebSocket connection are not merely the JSON-encoded data. Instead, the JSON-encoded data is prefixed with the name of the schema, optionally prefixed with the package name. For example, suppose NotificationMessage schema results in a JSON of the form:

{noformat}{"message": "the msg here"}{noformat}

The client receiving this message will actually receive the following data:

{noformat}NotificationMessage={"message": "the msg here"}{noformat}

It may optionally look like:

{noformat}org.hawkular.feedcomm.api.NotificationMessage={"message": "the msg here"}{noformat}

A Java client can use the utility org.hawkular.feedcomm.api.ApiDeserializer.deserialize() (passing in that string) to obtain the Java POJO that represents this notification message. A non-Java client can utilize the NotificationMessage.schema.json (note, the name of the schema is the same name as that prefix, minus any optional package name) in order to know how to deserialize the JSON message.
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,33 @@ public class ApiDeserializer {
// note that this assumes this class is in the same package as all the API POJOs
private static final String API_PKG = ApiDeserializer.class.getPackage().getName();

public ApiDeserializer() {
/**
* Returns a string that encodes the given object as a JSON message but then
* prefixes that JSON with additional information that a Hawkular client will
* need to be able to deserialize the JSON.
*
* This string can be used to deserialize the object via {@link #deserialize(String)}.
*
* @param msg the message object that will be serialized into JSON
* @return a string that includes the JSON that can be used by other Hawkular endpoints to deserialize the message.
*/
public static String toHawkularFormat(BasicMessage msg) {
return String.format("%s=%s", msg.getClass().getSimpleName(), msg.toJSON());
}

public <T extends BasicMessage> T deserialize(String nameAndJson) {
String[] nameAndJsonArray = nameAndJson.split("=", 2);
private static String[] fromHawkularFormat(String msg) {
String[] nameAndJsonArray = msg.split("=", 2);
if (nameAndJsonArray.length != 2) {
throw new IllegalArgumentException("Cannot deserialize: [" + nameAndJson + "]");
throw new IllegalArgumentException("Cannot deserialize: [" + msg + "]");
}
return nameAndJsonArray;
}

public ApiDeserializer() {
}

public <T extends BasicMessage> T deserialize(String nameAndJson) {
String[] nameAndJsonArray = fromHawkularFormat(nameAndJson);
String name = nameAndJsonArray[0];
String json = nameAndJsonArray[1];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

// probably not needed, but its here in case its useful later
public abstract class JsonUtil {

// prohibit instantiation
private JsonUtil() {
}

// probably not needed, but its here in case its useful later
public static String toJson(Object obj) {
final ObjectMapper mapper = new ObjectMapper();
mapper.setVisibilityChecker(mapper.getSerializationConfig().getDefaultVisibilityChecker()
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
Expand All @@ -44,6 +46,7 @@ public static String toJson(Object obj) {
return json;
}

// probably not needed, but its here in case its useful later
public static <T> T fromJson(String json, Class<T> clazz) {
final ObjectMapper mapper = new ObjectMapper();
final T obj;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "object",
"javaType": "org.hawkular.bus.common.BasicMessage"
},
"javaType": "org.hawkular.feedcomm.api.ExecuteOperation",
"javaType": "org.hawkular.feedcomm.api.ExecuteOperationRequest",
"additionalProperties": false,
"description": "A request to execute an operation.",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "object",
"extends": {
"type": "object",
"javaType": "org.hawkular.bus.common.BasicMessage"
},
"javaType": "org.hawkular.feedcomm.api.NotificationMessage",
"description": "An general notification message.",
"additionalProperties": false,
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ public void testApiDeserializerError() {
}

@Test
public void testExecuteOperation() {
ExecuteOperation newpojo;
ExecuteOperation pojo = new ExecuteOperation();
public void testExecuteOperationRequest() {
ExecuteOperationRequest newpojo;
ExecuteOperationRequest pojo = new ExecuteOperationRequest();
pojo.setOperationName("opname");
pojo.setResourceId("resid");
pojo.setParameters(new HashMap<String, String>());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.hawkular.feedcomm.ws;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.websocket.Session;

import org.hawkular.feedcomm.api.ApiDeserializer;
import org.hawkular.feedcomm.api.NotificationMessage;

/**
* Maintains a runtime list of UI clients currently connected with this server.
*/
@Startup
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class ConnectedUIClients {

// key=sessionID, value=UI client websocket session
private Map<String, Session> sessions;

@PostConstruct
public void initialize() {
MsgLogger.LOG.debugf("ConnectedUIClients has initialized");
this.sessions = new HashMap<>();
}

@Lock(LockType.READ)
public int getTotalSessions() {
return this.sessions.size();

}

@Lock(LockType.WRITE)
public void addSession(Session newSession) {
this.sessions.put(newSession.getId(), newSession);
MsgLogger.LOG.infof("A UI client session has been added [%s]. There are now [%d] connected UI clients",
newSession.getId(), this.sessions.size());

}

@Lock(LockType.WRITE)
public void removeSession(Session doomedSession) {
Session removed = this.sessions.remove(doomedSession.getId());
if (removed != null) {
MsgLogger.LOG.infof("A UI client session has been removed [%s]. There are now [%d] connected UI clients",
removed.getId(), this.sessions.size());
}
}

@Lock(LockType.READ)
public void sendNotificationMessageToAll(NotificationMessage notification) {
String message = ApiDeserializer.toHawkularFormat(notification);
MsgLogger.LOG.debugf("Sending notification message to all UI clients: [%s]", message);
for (Session session : this.sessions.values()) {
session.getAsyncRemote().sendText(message);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
@ServerEndpoint("/{feedId}")
public class FeedCommWebSocket {

private static final Map<String, Class<? extends Command>> VALID_COMMANDS;
private static final Map<String, Class<? extends Command<?, ?>>> VALID_COMMANDS;

static {
VALID_COMMANDS = new HashMap<>();
VALID_COMMANDS.put(EchoCommand.REQUEST_CLASS.getName(), EchoCommand.class);
Expand All @@ -62,39 +63,39 @@ public void feedSessionOpen(Session session, @PathParam("feedId") String feedId)
* @return the results of the command invocation; this is sent back to the feed
*/
@OnMessage
@SuppressWarnings({ "unchecked", "rawtypes" })
public String feedMessage(String nameAndJsonStr, Session session, @PathParam("feedId") String feedId) {

MsgLogger.LOG.infof("Received message from feed [%s]", feedId);

String requestClassName = "?";
String responseJson;
BasicMessage response;

try {
BasicMessage request = new ApiDeserializer().deserialize(nameAndJsonStr);
requestClassName = request.getClass().getName();

Class<? extends Command> commandClass = VALID_COMMANDS.get(requestClassName);
Class<? extends Command<?, ?>> commandClass = VALID_COMMANDS.get(requestClassName);
if (commandClass == null) {
MsgLogger.LOG.errorInvalidCommandRequest(feedId, requestClassName);
MsgLogger.LOG.errorInvalidCommandRequestFeed(feedId, requestClassName);
String errorMessage = "Invalid command request: " + requestClassName;
responseJson = new GenericErrorResponseBuilder().setErrorMessage(errorMessage).build().toJSON();
response = new GenericErrorResponseBuilder().setErrorMessage(errorMessage).build();
} else {
Command command = commandClass.newInstance();
BasicMessage response = command.execute(request);
responseJson = response.toJSON();
response = command.execute(request);
}
} catch (Throwable t) {
MsgLogger.LOG.errorCommandExecutionFailure(requestClassName, feedId, t);
MsgLogger.LOG.errorCommandExecutionFailureFeed(requestClassName, feedId, t);
String errorMessage = "Command failed[" + requestClassName + "]";
responseJson = new GenericErrorResponseBuilder()
response = new GenericErrorResponseBuilder()
.setThrowable(t)
.setErrorMessage(errorMessage)
.build()
.toJSON();
.build();

}

return responseJson;
String responseText = ApiDeserializer.toHawkularFormat(response);
return responseText;
}

@OnClose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public interface MsgLogger extends BasicLogger {

@LogMessage(level = Logger.Level.ERROR)
@Message(id = 1, value = "Feed [%s] provided an invalid command request: [%s]")
void errorInvalidCommandRequest(String feedId, String invalidCommandRequest);
void errorInvalidCommandRequestFeed(String feedId, String invalidCommandRequest);

@LogMessage(level = Logger.Level.ERROR)
@Message(id = 2, value = "Failed to execute command [%s] for feed [%s]")
void errorCommandExecutionFailure(String commandRequest, String feedId, @Cause Throwable t);
void errorCommandExecutionFailureFeed(String commandRequest, String feedId, @Cause Throwable t);

@LogMessage(level = Logger.Level.ERROR)
@Message(id = 3, value = "A feed [%s] opened multiple sessions. This is a violation; closing the extra session")
Expand All @@ -46,4 +46,12 @@ public interface MsgLogger extends BasicLogger {
@Message(id = 4, value = "Cannot close the extra session created by feed [%s]")
void errorCannotCloseExtraFeedSession(String feedId, @Cause Throwable t);

@LogMessage(level = Logger.Level.ERROR)
@Message(id = 5, value = "UI client session [%s] provided an invalid command request: [%s]")
void errorInvalidCommandRequestUIClient(String sessionId, String invalidCommandRequest);

@LogMessage(level = Logger.Level.ERROR)
@Message(id = 6, value = "Failed to execute command [%s] for UI client session [%s]")
void errorCommandExecutionFailureUIClient(String commandRequest, String sessionId, @Cause Throwable t);

}

0 comments on commit c0a53c3

Please sign in to comment.