package jj.jjmessage; // lololololol java import static jj.jjmessage.JJMessage.Type.*; import static jj.util.StringUtils.*; import java.util.Map; import jj.http.server.websocket.WebSocketMessage; import jj.script.Continuation; import jj.script.PendingKey; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; /** * <p> * Encapsulates a communication between client and server * </p> * * TODO replace the serialized forms with field tags, after setting up a way of * preprocessing js being served to the client so that socket-connect.js (or * wherever everything ends up) can use easy-to-read symbols in the source and tags * in the execution phase * @author jason * */ public class JJMessage implements Continuation, WebSocketMessage { private static final ObjectMapper mapper = new ObjectMapper(); static { mapper.setSerializationInclusion(Include.NON_NULL); } public static enum Type { /** server --> client append request */ Append, /** server --> client event binding request */ Bind, /** server --> client general invocation, expecting a result */ Call, /** server --> client element creation request, followed by result containing selector */ Create, /** client --> server event fired */ Event, /** client --> server element result */ Element, /** server --> client getter invocation, followed by result containing value */ Get, /** server --> client general invocation, expecting no result! */ Invoke, /** client --> server general string result */ Result, /** server --> client request information in client storage */ Retrieve, /** server --> client setter invocation */ Set, /** server --> client request to store information on the client */ Store, /** server --> client event unbinding request */ Unbind; } public static JJMessage makeGet(String selector, String type) { return makeGet(selector, type, null); } public static JJMessage makeGet(String selector, String type, String name) { JJMessage result = new JJMessage(Get); result.get().selector = selector; result.get().type = type; result.get().name = name; return result; } public static JJMessage makeSet(String selector, String type, String value) { return makeSet(selector, type, null, value); } public static JJMessage makeSet(String selector, String type, String name, String value) { JJMessage result = new JJMessage(Set); result.set().selector = selector; result.set().type = type; result.set().name = name; result.set().value = value; return result; } public static JJMessage makeInlineCreate(String html, Map<?,?> args) { JJMessage result = new JJMessage(Create); result.create().html = html; result.create().args = args; return result; } public static JJMessage makeCreate(String html, Map<?,?> args) { JJMessage result = new JJMessage(Create); result.create().html = html; result.create().args = args; return result; } public static JJMessage makeAppend(String parent, String child) { assert !isEmpty(parent) : "append message requires parent"; assert !isEmpty(child) : "append message requires child"; JJMessage result = new JJMessage(Append); result.append().parent = parent; result.append().child = child; return result; } public static JJMessage makeBind(String context, String selector, String type) { assert !isEmpty(type) : "bind message requires type"; JJMessage result = new JJMessage(Bind); result.bind().context = context; result.bind().selector = selector; result.bind().type = type; return result; } /** * The Invoke messages invokes a function on the client, expecting a result. * * @param name the remote function to invoke * @param args the JSON reprepesentation of the arguments array * @return */ public static JJMessage makeInvoke(String name, String args) { assert !isEmpty(name) : "invoke message requires type"; assert !isEmpty(args) && args.startsWith("[") && args.endsWith("]") : "invoke message must have a JSON array argument"; JJMessage result = new JJMessage(Invoke); result.invoke().name = name; result.invoke().args = args; return result; } /** * The Call message calls a function on the client, expecting no result. * * @param name the remote function to call * @param args the JSON reprepesentation of the arguments array * @return */ public static JJMessage makeCall(String name, String args) { assert !isEmpty(name) : "call message requires type"; assert !isEmpty(args) && args.startsWith("[") && args.endsWith("]") : "call message must have a JSON array argument"; JJMessage result = new JJMessage(Call); result.call().name = name; result.call().args = args; return result; } public static JJMessage makeStore(String key, String value) { JJMessage result = new JJMessage(Store); result.store().key = key; result.store().value = value; return result; } public static JJMessage makeRetrieve(String key) { JJMessage result = new JJMessage(Retrieve); result.retrieve().key = key; return result; } public static JJMessage makeUnbind(String context, String selector, String type) { assert !isEmpty(type) : "unbind message requires type"; JJMessage result = new JJMessage(Unbind); result.unbind().context = context; result.unbind().selector = selector; result.unbind().type = type; return result; } JJMessage() {} JJMessage(final Type type) { switch(this.type = type) { case Append: message = new Append(); break; case Bind: message = new Bind(); break; case Call: message = new Invoke(); break; case Create: message = new Create(); break; case Get: message = new Get(); break; case Invoke: message = new Invoke(); break; case Retrieve: message = new Retrieve(); break; case Set: message = new Set(); break; case Store: message = new Store(); break; case Unbind: message = new Unbind(); break; default: throw new AssertionError("can't create a JJMessage of type " + type); } } // -- type flag. used this way to keep memory use efficient // but still with a convenient API for reading/writing @JsonIgnore private Type type; @JsonIgnore public Type type() { return type; } @JsonIgnore private Object message; // --- script messages @JsonProperty public Bind bind() { return (Bind)(type == Bind ? message : null); } @JsonProperty public Event event() { return (Event)(type == Event ? message : null); } @JsonProperty // setters only needed for client -- > server void event(Event event) { type = Event; message = event; } @JsonProperty public Element element() { return (Element)(type == Element ? message : null); } @JsonProperty void element(Element element) { type = Element; message = element; } @JsonProperty public Get get() { return (Get)(type == Get ? message : null); } @JsonProperty public Set set() { return (Set)(type == Set ? message : null); } @JsonProperty public Store store() { return (Store)(type == Store ? message : null); } @JsonProperty public Retrieve retrieve() { return (Retrieve)(type == Retrieve ? message : null); } @JsonProperty public Result result() { return (Result)(type == Result ? message : null); } @JsonProperty // setters only needed for client -- > server void result(Result result) { type = Result; message = result; } @JsonProperty public Create create() { return (Create)(type == Create ? message : null); } @JsonProperty public Append append() { return (Append)(type == Append ? message : null); } @JsonProperty public Invoke invoke() { return (Invoke)(type == Invoke ? message : null); } @JsonProperty public Invoke call() { return (Invoke)(type == Call ? message : null); } @JsonProperty public Unbind unbind() { return (Unbind)(type == Unbind ? message : null); } @Override public boolean equals(Object obj) { return obj != null && obj instanceof JJMessage && toString().equals(obj.toString()); } @Override public String toString() { try { return mapper.writeValueAsString(this); } catch (Exception e) { throw new JJMessageException(e); } } @Override public String stringify() { return toString(); } public static JJMessage fromString(String input) { try { return mapper.readValue(input, JJMessage.class); } catch (Exception e) { throw new JJMessageException(e); } } // WE ARE CONTINUABLE! MIGHTY BABY MIGHTY! @JsonIgnore @Override public PendingKey pendingKey() { return (message instanceof HasResultID) ? new PendingKey(((HasResultID)message).id) : null; } @JsonIgnore @Override public void pendingKey(PendingKey pendingKey) { if (message instanceof HasResultID) { ((HasResultID)message).id = pendingKey.id(); } } }