package water.api; import dontweave.gson.*; import water.AutoBuffer; import water.H2O; import water.Iced; import water.PrettyPrint; import water.api.Request.API; import water.api.RequestBuilders.Response.Status; import water.util.JsonUtil; import water.util.Log; import water.util.RString; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** Builders and response object. * * It just has a stuff of simple builders that walk through the JSON response * and format the stuff into basic html. Understands simplest form of tables, * objects and elements. * * Also defines the response object that contains the response JSON, response * state, other response related automatic variables (timing, etc) and the * custom builders. * * TODO work in progress. * * @author peta */ public class RequestBuilders extends RequestQueries { public static final String ROOT_OBJECT = ""; public static final Gson GSON_BUILDER = new GsonBuilder().setPrettyPrinting().create(); private static final ThreadLocal<DecimalFormat> _format = new ThreadLocal<DecimalFormat>() { @Override protected DecimalFormat initialValue() { return new DecimalFormat("###.####"); } }; static final ThreadLocal<SimpleDateFormat> ISO8601 = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); } }; /** Builds the HTML for the given response. * * This is the root of the HTML. Should display all what is needed, including * the status, timing, etc. Then call the recursive builders for the * response's JSON. */ protected String build(Response response) { StringBuilder sb = new StringBuilder(); sb.append("<div class='container'>"); sb.append("<div class='row-fluid'>"); sb.append("<div class='span12'>"); sb.append(buildJSONResponseBox(response)); if( response._status == Response.Status.done ) response.toJava(sb); sb.append(buildResponseHeader(response)); Builder builder = response.getBuilderFor(ROOT_OBJECT); if (builder == null) { sb.append("<h3>"+name()+"</h3>"); builder = OBJECT_BUILDER; } for( String h : response.getHeaders() ) sb.append(h); if( response._response==null ) { boolean done = response._req.toHTML(sb); if(!done) { JsonParser parser = new JsonParser(); String json = new String(response._req.writeJSON(new AutoBuffer()).buf()); JsonObject o = (JsonObject) parser.parse(json); sb.append(builder.build(response, o, "")); } } else sb.append(builder.build(response,response._response,"")); sb.append("</div></div></div>"); return sb.toString(); } protected String name() { return getClass().getSimpleName(); } private static final String _responseHeader = "<table class='table table-bordered'><tr><td min-width: 60px><table style='font-size:12px;margin:0px;' class='table-borderless'>" + " <tr>" + " <td style='border:0px; min-width: 60px;' rowspan='2' style='vertical-align:top;'>%BUTTON  </td>" + " <td style='border:0px; min-width: 60px' colspan='6'>" + " %TEXT" + " </td>" + " </tr>" + " <tr>" + " <td style='border:0px; min-width: 60px'><b>Cloud:</b></td>" + " <td style='padding-right:70px;border:0px; min-width: 60px'>%CLOUD_NAME</td>" + " <td style='border:0px; min-width: 60px'><b>Node:</b></td>" + " <td style='padding-right:70px;border:0px; min-width: 60px'>%NODE_NAME</td>" + " <td style='border:0px; min-width: 60px'><b>Time:</b></td>" + " <td style='padding-right:70px;border:0px; min-width: 60px'>%TIME</td>" + " </tr>" + "</table></td></tr></table>" + "<script type='text/javascript'>" + "%JSSTUFF" + "</script>" ; private static final String _redirectJs = "var timer = setTimeout('redirect()',000);\n" + "function countdown_stop() {\n" + " clearTimeout(timer);\n" + "}\n" + "function redirect() {\n" + " window.location.replace('%REDIRECT_URL');\n" + "}\n" ; private static final String _pollJs = "var timer = setTimeout(redirect,%TIMEOUT);\n" + "function countdown_stop() {\n" + " clearTimeout(timer);\n" + "}\n" + "function redirect() {\n" + " document.location.reload(true);\n" + "}\n" ; private static final String _jsonResponseBox = "<div class='pull-right'><a href='#' onclick='$(\"#json_box\").toggleClass(\"hide\");' class='btn btn-inverse btn-mini'>JSON</a></div>" + "<div class='hide' id='json_box'><pre><code class=\"language-json\">" + "%JSON_RESPONSE_BOX" + "</code></pre></div>"; protected String buildJSONResponseBox(Response response) { switch (response._status) { case done : RString result = new RString(_jsonResponseBox); result.replace("JSON_RESPONSE_BOX", response._response == null ? new String(response._req.writeJSON(new AutoBuffer()).buf()) : GSON_BUILDER.toJson(response.toJson())); return result.toString(); case error : case redirect: case poll : default : return ""; } } protected String buildResponseHeader(Response response) { RString result = new RString(_responseHeader); JsonObject obj = response.responseToJson(); result.replace("CLOUD_NAME",obj.get(JSON_H2O).getAsString()); result.replace("NODE_NAME",obj.get(NODE).getAsString()); result.replace("TIME", PrettyPrint.msecs(obj.get(REQUEST_TIME).getAsLong(), true)); switch (response._status) { case error: result.replace("BUTTON","<button class='btn btn-danger disabled'>"+response._status.toString()+"</button>"); result.replace("TEXT","An error has occurred during the creation of the response. Details follow:"); break; case done: //result.replace("BUTTON","<button class='btn btn-success disabled'>"+response._status.toString()+"</button>"); //result.replace("TEXT","The result was a success and no further action is needed. JSON results are prettyprinted below."); result = new RString(""); break; case redirect: result.replace("BUTTON","<button class='btn btn-primary' onclick='redirect()'>"+response._status.toString()+"</button>"); result.replace("TEXT","Request was successful and the process was started. You will be redirected to the new page in 1 seconds, or when you click on the redirect" + " button on the left. If you want to keep this page for longer you can <a href='#' onclick='countdown_stop()'>stop the countdown</a>."); RString redirect = new RString(_redirectJs); redirect.replace("REDIRECT_URL",response._redirectName+".html"+encodeRedirectArgs(response._redirectArgs,response._redirArgs)); result.replace("JSSTUFF", redirect.toString()); break; case poll: if (response._redirectArgs != null) { RString poll = new RString(_redirectJs); poll.replace("REDIRECT_URL",requestName()+".html"+encodeRedirectArgs(response._redirectArgs,response._redirArgs)); result.replace("JSSTUFF", poll.toString()); } else { RString poll = new RString(_pollJs); poll.replace("TIMEOUT", response._pollProgress==0 ? 4500 : 5000); result.replace("JSSTUFF", poll.toString()); } int pct = (int) ((double)response._pollProgress / response._pollProgressElements * 100); result.replace("BUTTON","<button class='btn btn-primary' onclick='redirect()'>"+response._status.toString()+"</button>"); result.replace("TEXT","<div style='margin-bottom:0px;padding-bottom:0xp;height:5px;' class='progress progress-stripped'><div class='bar' style='width:"+pct+"%;'></div></div>" + "Request was successful, but the process has not yet finished. The page will refresh every 5 seconds, or you can click the button" + " on the left. If you want you can <a href='#' onclick='countdown_stop()'>disable the automatic refresh</a>."); break; default: result.replace("BUTTON","<button class='btn btn-inverse disabled'>"+response._status.toString()+"</button>"); result.replace("TEXT","This is an unknown response state not recognized by the automatic formatter. The rest of the response is displayed below."); break; } return result.toString(); } /** Basic builder for objects. () */ public static final Builder OBJECT_BUILDER = new ObjectBuilder(); /** Basic builder for arrays. (table) */ public static final Builder ARRAY_BUILDER = new ArrayBuilder(); /** Basic builder for array rows. (tr) */ public static final Builder ARRAY_ROW_BUILDER = new ArrayRowBuilder(); /** Basic build for shaded array rows. (tr class='..') */ public static final Builder ARRAY_HEADER_ROW_BUILDER = new ArrayHeaderRowBuilder(); /** Basic builder for elements inside objects. (dl,dt,dd) */ public static final ElementBuilder ELEMENT_BUILDER = new ElementBuilder(); /** Basic builder for elements in array row objects. (td) */ public static final Builder ARRAY_ROW_ELEMENT_BUILDER = new ArrayRowElementBuilder(); /** Basic builder for elements in array rows single col. (tr and td) */ public static final Builder ARRAY_ROW_SINGLECOL_BUILDER = new ArrayRowSingleColBuilder(); // =========================================================================== // Response // =========================================================================== /** This is a response class for the JSON. * * Instead of simply returning a JsonObject, each request returns a new * response object that it must create. This is (a) cleaner (b) more * explicit and (c) allows to specify response states used for proper * error reporting, stateless and statefull processed and so on, and (d) * allows specification of HTML builder hooks in a nice clean interface. * * The work pattern should be that in the serve() method, a JsonObject is * created and populated with the variables. Then if any error occurs, an * error response should be returned. * * Otherwise a correct state response should be created at the end from the * json object and returned. * * JSON response structure: * * response : status = (done,error,redirect, ...) * h2o = name of the cloud * node = answering node * time = time in MS it took to process the request serve() * other fields as per the response type * other fields that should go to the user * if error: * error : error reported */ public static final class Response { /** Status of the response. * * Defines the state of the response so that it can be nicely reported to * the user in either in JSON or in HTML in a meaningful manner. */ public static enum Status { done, ///< Indicates that the request has completed and no further action from the user is required poll, ///< Indicates that the same request should be repeated to see some progress redirect, ///< Indicates that the request was successful, but new request must be filled to obtain results error ///< The request was an error. } /** Time it took the request to finish. In ms. */ protected long _time; /** Status of the request. */ protected final Status _status; /** Name of the redirected request. This is only valid if the response is * redirect status. */ protected final String _redirectName; /** Arguments of the redirect object. These will be given to the redirect * object when called. */ protected final JsonObject _redirectArgs; protected final Object[] _redirArgs; /** Poll progress in terms of finished elements. */ protected final int _pollProgress; /** Total elements to be finished before the poll will be done. */ protected final int _pollProgressElements; /** Response object for JSON requests. */ protected final JsonObject _response; public final Request _req; protected boolean _strictJsonCompliance = true; /** Custom builders for JSON elements when converting to HTML automatically. */ protected final HashMap<String,Builder> _builders = new HashMap(); /** Custom headers to show in the html. */ protected final List<String> _headers = new ArrayList(); /** Private constructor creating the request with given type and response * JSON object. * * Use the static methods to construct the response objects. (looks better * when we have a lot of them). */ private Response(Status status, JsonObject response) { _status = status; _response = response; _redirectName = null; _redirectArgs = null; _redirArgs = null; _pollProgress = -1; _pollProgressElements = -1; _req = null; } private Response(Status status, JsonObject response, String redirectName, JsonObject redirectArgs) { assert (status == Status.redirect); _status = status; _response = response; _redirectName = redirectName; _redirectArgs = redirectArgs; _redirArgs = null; _pollProgress = -1; _pollProgressElements = -1; _req = null; } private Response(Status status, JsonObject response, int progress, int total, JsonObject pollArgs) { assert (status == Status.poll); _status = status; _response = response; _redirectName = null; _redirectArgs = pollArgs; _redirArgs = null; _pollProgress = progress; _pollProgressElements = total; _req = null; } private Response(Status status, Request req, int progress, int total, Object...pollArgs) { assert (status == Status.poll); _status = status; _response = null; _redirectName = null; _redirectArgs = null; _redirArgs = pollArgs; _pollProgress = progress; _pollProgressElements = total; _req = req; } /** Response v2 constructor */ private Response(Status status, Request req, int progress, int total, String redirTo, Object... args) { _status = status; _response = null; _redirectName = redirTo; _redirectArgs = null; _redirArgs = args; _pollProgress = progress; _pollProgressElements = total; _req = req; } /** Returns new error response with given error message. */ public static Response error(Throwable e) { if( !(e instanceof IllegalAccessException )) Log.err(e); String message = e.getMessage(); if( message == null ) message = e.getClass().toString(); return error(message); } public static Response error(String message) { if( message == null ) message = "no error message"; JsonObject obj = new JsonObject(); obj.addProperty(ERROR,message); Response r = new Response(Status.error,obj); r.setBuilder(ERROR, new PreFormattedBuilder()); return r; } /** Returns new done response with given JSON response object. */ public static Response done(JsonObject response) { assert response != null : "Called Response.done with null JSON response - perhaps you should call Response.doneEmpty"; return new Response(Status.done, response); } /** Response done v2. */ public static Response done(Request req) { return new Response(Response.Status.done,req,-1,-1,(String) null); } /** A unique empty response which carries an empty JSON object */ public static final Response EMPTY_RESPONSE = Response.done(new JsonObject()); /** Returns new done empty done response. * Should be called only in cases which does not need json response. * see HTMLOnlyRequest */ public static Response doneEmpty() { return EMPTY_RESPONSE; } /** Creates the new response with status redirect. This response will be * redirected to another request specified by redirectRequest with the * redirection arguments provided in redirectArgs. */ public static Response redirect(JsonObject response, Class<? extends Request> req, JsonObject args) { return new Response(Status.redirect, response, req.getSimpleName(), args); } /** Redirect for v2 API */ public static Response redirect(Request req, String redirectName, Object...redirectArgs) { return new Response(Response.Status.redirect, req, -1, -1, redirectName, redirectArgs); } /** Returns the poll response object. */ public static Response poll(JsonObject response, int progress, int total) { return new Response(Status.poll,response, progress, total, null); } /** Returns the poll response object initialized by percents completed. */ public static Response poll(JsonObject response, float progress) { int p = (int) (progress * 100); return Response.poll(response, p, 100); } /** returns the poll response object with different arguments that was * this call. */ public static Response poll(JsonObject response, int progress, int total, JsonObject pollArgs) { return new Response(Status.poll,response, progress, total, pollArgs); } /** Returns the poll response object. */ public static Response poll(Request req, int progress, int total, Object...pollArgs) { return new Response(Status.poll,req, progress, total, pollArgs); } /** Sets the time of the response as a difference between the given time and * now. Called automatically by serving request. Only available in JSON and * HTML. */ public final void setTimeStart(long timeStart) { _time = System.currentTimeMillis() - timeStart; } /** Associates a given builder with the specified JSON context. JSON context * is a dot separated path to the JSON object/element starting from root. * * One exception is an array row element, which does not really have a * distinct name in JSON and is thus identified as the context name of the * array + "_ROW" appended to it. * * The builder object will then be called to build the HTML for the * particular JSON element. By wise subclassing of the preexisting builders * and changing their behavior an arbitrarily complex webpage can be * created. */ public Response setBuilder(String contextName, Builder builder) { _builders.put(contextName, builder); return this; } /** Returns the builder for given JSON context element. Null if not found * in which case a default builder object will be used. These default * builders are specified by the builders themselves. */ protected Builder getBuilderFor(String contextName) { return _builders.get(contextName); } public void addHeader(String h) { _headers.add(h); } public List<String> getHeaders() { return _headers; } /** Returns the response system json. That is the response type, time, * h2o basics and other automatic stuff. * @return */ protected JsonObject responseToJson() { JsonObject resp = new JsonObject(); resp.addProperty(STATUS,_status.toString()); resp.addProperty(JSON_H2O, H2O.NAME); resp.addProperty(NODE, H2O.SELF.toString()); resp.addProperty(REQUEST_TIME, _time); switch (_status) { case done: case error: break; case redirect: resp.addProperty(REDIRECT,_redirectName); if (_redirectArgs != null) resp.add(REDIRECT_ARGS,_redirectArgs); break; case poll: resp.addProperty(PROGRESS, _pollProgress); resp.addProperty(PROGRESS_TOTAL, _pollProgressElements); break; default: assert(false): "Unknown response type "+_status.toString(); } return resp; } /** Returns the JSONified version of the request. At the moment just * returns the response. */ public JsonObject toJson() { JsonObject res = _response; if( _strictJsonCompliance ) res = JsonUtil.escape(res); // in this case, creating a cyclical structure would kill us. if( _response != null && _response == _redirectArgs ) { res = new JsonObject(); for( Entry<String, JsonElement> e : _response.entrySet() ) { res.add(e.getKey(), e.getValue()); } } res.add(RESPONSE, responseToJson()); return res; } public String toXml() { JsonObject jo = this.toJson(); String jsonString = jo.toString(); org.json.JSONObject jo2 = new org.json.JSONObject(jsonString); String xmlString = org.json.XML.toString(jo2); return xmlString; } public void toJava(StringBuilder sb) { if( _req != null ) _req.toJava(sb); } /** Returns the error of the request object if any. Returns null if the * response is not in error state. */ public String error() { if (_status != Status.error) return null; return _response.get(ERROR).getAsString(); } public void escapeIllegalJsonElements() { _strictJsonCompliance = true; } public ResponseInfo extractInfo() { String redirectUrl = null; if (_status == Status.redirect) redirectUrl = _redirectName+".json"+encodeRedirectArgs(_redirectArgs,_redirArgs); if (_status == Status.poll) redirectUrl = _req.href()+".json"+encodeRedirectArgs(_redirectArgs,_redirArgs); return new ResponseInfo(redirectUrl, _time, _status); } } /** Class holding technical information about request/response. It will be served as a part of Request2's * response. */ public static class ResponseInfo extends Iced { static final int API_WEAVER=1; static public DocGen.FieldDoc[] DOC_FIELDS; final @API(help="H2O cloud name.") String h2o; final @API(help="Node serving the response.") String node; final @API(help="Request processing time.") long time; final @API(help="Response status") Response.Status status; final @API(help="Redirect name.") String redirect_url; public ResponseInfo(String redirect_url, long time, Status status) { this.h2o = H2O.NAME; this.node = H2O.SELF.toString(); this.redirect_url = redirect_url; this.time = time; this.status = status; } } // --------------------------------------------------------------------------- // Builder // --------------------------------------------------------------------------- /** An abstract class to build the HTML page automatically from JSON. * * The idea is that every JSON element in the response structure (dot * separated) may be a unique context that might be displayed in a different * way. By creating specialized builders and assigning them to the JSON * element contexts you can build arbitrarily complex HTML page. * * The basic builders for elements, arrays, array rows and elements inside * array rows are provided by default. * * Each builder can also specify default builders for its components to make * sure for instance that tables in arrays do not recurse and so on. */ public static abstract class Builder { /** Override this method to provide HTML for the given json element. * * The arguments are the response object, the element whose HTML should be * produced and the contextName of the element. */ public abstract String build(Response response, JsonElement element, String contextName); /** Adds the given element name to the existing context. Dot concatenates * the names. */ public static String addToContext(String oldContext, String name) { if (oldContext.isEmpty()) return name; return oldContext+"."+name; } /** For a given context returns the element name. That is the last word * after a dot, or the full string if dot is not present. */ public static String elementName(String context) { int idx = context.lastIndexOf("."); return context.substring(idx+1); } /** Returns the default builders. * * These are element builder, object builder and array builder. */ public Builder defaultBuilder(JsonElement element) { if (element instanceof JsonArray) return ARRAY_BUILDER; else if (element instanceof JsonObject) return OBJECT_BUILDER; else return ELEMENT_BUILDER; } } // --------------------------------------------------------------------------- // ObjectBuilder // --------------------------------------------------------------------------- /** Object builder. * * By default objects are displayed as a horizontal dl elements with their * heading preceding any of the values. Methods for caption, header, * footer as well as element building are provided so that the behavior can * easily be customized. */ public static class ObjectBuilder extends Builder { /** Displays the caption of the object. */ public String caption(JsonObject object, String objectName) { return objectName.isEmpty() ? "" : "<h4>"+objectName+"</h4>"; } /** Returns the header of the object. * * That is any HTML displayed after caption and before any object's * contents. */ public String header(JsonObject object, String objectName) { return ""; } /** Returns the footer of the object. * * That is any HTML displayed after any object's contents. */ public String footer(JsonObject object, String objectName) { return ""; } /** Creates the HTML of the object. * * That is the caption, header, all its contents in order they were * added and then the footer. There should be no need to overload this * function, rather override the provided hooks above. */ public String build(Response response, JsonObject object, String contextName) { StringBuilder sb = new StringBuilder(); String name = elementName(contextName); sb.append(caption(object, name)); sb.append(header(object, name)); for (Map.Entry<String,JsonElement> entry : object.entrySet()) { JsonElement e = entry.getValue(); String elementContext = addToContext(contextName, entry.getKey()); Builder builder = response.getBuilderFor(elementContext); if (builder == null) builder = defaultBuilder(e); sb.append(builder.build(response, e, elementContext)); } sb.append(footer(object, elementName(contextName))); return sb.toString(); } /** The original build method. Calls build with json object, if not an * object, displays an alert box with the JSON contents. */ public String build(Response response, JsonElement element, String contextName) { if (element instanceof JsonObject) return build(response, (JsonObject) element, contextName); return "<div class='alert alert-error'>Response element "+contextName+" expected to be JsonObject. Automatic display not available</div><pre>"+element.toString()+"</pre>"; } } public static class NoCaptionObjectBuilder extends ObjectBuilder { public String caption(JsonObject object, String objectName) { return ""; } } // --------------------------------------------------------------------------- // Array builder // --------------------------------------------------------------------------- /** Builds the HTML for an array. Arrays generally go to a table. Is similar * to the object, but rather than a horizontal dl generally displays as * a table. * * Can produce a header of the table and has hooks for rows. */ public static class ArrayBuilder extends Builder { /** Caption of the table. */ public String caption(JsonArray array, String name) { return "<h4>"+name+"</h4>"; } /** Header of the table. Produces header off the first element if it is * object, or a single column header named value if it is a primitive. Also * includes the table tag. */ public String header(JsonArray array) { StringBuilder sb = new StringBuilder(); sb.append("<span style='display: inline-block;'>"); sb.append("<table class='table table-striped table-bordered'>"); if (array.get(0) instanceof JsonObject) { sb.append("<tr>"); for (Map.Entry<String,JsonElement> entry : ((JsonObject)array.get(0)).entrySet()) sb.append("<th style='min-width: 60px;'>").append(header(entry.getKey())).append("</th>"); sb.append("</tr>"); } return sb.toString(); } public String header(String key) { return JSON2HTML(key); } /** Footer of the table, the end of table tag. */ public String footer(JsonArray array) { return "</table></span>"; } /** Default builders for the table. It is either a table row builder if the * row is an object, or a row single column builder if it is a primitive * or another array. */ @Override public Builder defaultBuilder(JsonElement element) { return element instanceof JsonObject ? ARRAY_ROW_BUILDER : ARRAY_ROW_SINGLECOL_BUILDER; } /** Builds the array. Creates the caption, header, all the rows and the * footer or determines that the array is empty. */ public String build(Response response, JsonArray array, String contextName) { StringBuilder sb = new StringBuilder(); sb.append(caption(array, elementName(contextName))); if (array.size() == 0) { sb.append("<div class='alert alert-info'>empty array</div>"); } else { sb.append(header(array)); for (JsonElement e : array) { Builder builder = response.getBuilderFor(contextName+"_ROW"); if (builder == null) builder = defaultBuilder(e); sb.append(builder.build(response, e, contextName)); } sb.append(footer(array)); } return sb.toString(); } /** Calls the build method with array. If not an array, displays an alert * with the JSON contents of the element. */ public String build(Response response, JsonElement element, String contextName) { if (element instanceof JsonArray) return build(response, (JsonArray)element, contextName); return "<div class='alert alert-error'>Response element "+contextName+" expected to be JsonArray. Automatic display not available</div><pre>"+element.toString()+"</pre>"; } } // --------------------------------------------------------------------------- // ElementBuilder // --------------------------------------------------------------------------- /** A basic element builder. * * Elements are displayed as their string values, everything else as their * JSON values. */ public static class ElementBuilder extends Builder { /** Displays the element in the horizontal dl layout. Override this method * to change the layout. */ public String build(String elementContents, String elementName) { return "<dl class='dl-horizontal'><dt>"+elementName+"</dt><dd>"+elementContents+"</dd></dl>"; } public String arrayToString(JsonArray array, String contextName) { return array.toString(); } public String objectToString(JsonObject obj, String contextName) { return obj.toString(); } public String elementToString(JsonElement elm, String contextName) { String elementName = elementName(contextName); if( elementName.endsWith(Suffixes.BYTES_PER_SECOND) ) { return PrettyPrint.bytesPerSecond(elm.getAsLong()); } else if( elementName.endsWith(Suffixes.BYTES) ) { return PrettyPrint.bytes(elm.getAsLong()); } else if( elementName.endsWith(Suffixes.MILLIS) ) { return PrettyPrint.msecs(elm.getAsLong(), true); } else if( elm instanceof JsonPrimitive && ((JsonPrimitive)elm).isString() ) { return elm.getAsString(); } else if( elm instanceof JsonPrimitive && ((JsonPrimitive)elm).isNumber() ) { Number n = elm.getAsNumber(); if( n instanceof Double ) { Double d = (Double) n; return format(d); } return elm.getAsString(); } else { return elm.toString(); } } public static String format(double value) { if( Double.isNaN(value) ) return ""; return _format.get().format(value); } public String elementToName(String contextName) { String base = elementName(contextName); for( String s : new String[] { Suffixes.BYTES_PER_SECOND, Suffixes.BYTES, Suffixes.MILLIS, }) { if( base.endsWith(s) ) return base.substring(0, base.length() - s.length()); } return base; } /** Based of the element type determines its string value and then calls * the string build version. */ @Override public String build(Response response, JsonElement element, String contextName) { String base; if (element instanceof JsonArray) { base = arrayToString((JsonArray)element, contextName); } else if (element instanceof JsonObject) { base = objectToString((JsonObject)element, contextName); } else { base = elementToString(element, contextName); } return build(base, elementToName(contextName)); } } public static class KeyElementBuilder extends ElementBuilder { @Override public String build(String content, String name) { try { String k = URLEncoder.encode(content, "UTF-8"); return super.build("<a href='Inspect.html?key="+k+"'>"+content+"</a>", name); } catch( UnsupportedEncodingException e ) { throw Log.errRTExcept(e); } } } public static class PreFormattedBuilder extends ElementBuilder { @Override public String build(String content, String name) { return super.build("<pre>"+content+"</pre>", name); } } // Just the Key as a link, without any other cruft public static class KeyLinkElementBuilder extends ElementBuilder { @Override public String build(Response response, JsonElement element, String contextName) { try { String key = element.getAsString(); String k = URLEncoder.encode(key, "UTF-8"); return "<a href='Inspect.html?key="+k+"'>"+key+"</a>"; } catch( UnsupportedEncodingException e ) { throw Log.errRTExcept(e); } } } public static class BooleanStringBuilder extends ElementBuilder { final String _t, _f; public BooleanStringBuilder(String t, String f) { _t=t; _f=f; } @Override public String build(Response response, JsonElement element, String contextName) { boolean b = element.getAsBoolean(); return "<dl class='dl-horizontal'><dt></dt><dd>"+(b?_t:_f)+"</dd></dl>"; } } public static class HideBuilder extends ElementBuilder { @Override public String build(Response response, JsonElement element, String contextName) { return ""; } } // --------------------------------------------------------------------------- // ArrayRowBuilder // --------------------------------------------------------------------------- /** A row in the array table. * * Is an object builder with no caption and header and footer being the * table row tags. Default builder is array row element (td). */ public static class ArrayRowBuilder extends ObjectBuilder { @Override public String caption(JsonObject object, String objectName) { return ""; } @Override public String header(JsonObject object, String objectName) { //Gson g = new Gson(); //Log.info(g.toJson(object)); return "<tr id='row_"+object.get("row")+"'>"; } @Override public String footer(JsonObject object, String objectName) { return "</tr>"; } @Override public Builder defaultBuilder(JsonElement element) { return ARRAY_ROW_ELEMENT_BUILDER; } } public static class ArrayHeaderRowBuilder extends ArrayRowBuilder { @Override public String header(JsonObject object, String objectName) { return "<tr class='warning'>"; } } // --------------------------------------------------------------------------- // ArrayRowElementBuilder // --------------------------------------------------------------------------- /** Default array row element. * * A simple element builder than encapsulates into a td. */ public static class ArrayRowElementBuilder extends ElementBuilder { public String build(String elementContents, String elementName) { return "<td style='min-width: 60px;'>"+elementContents+"</td>"; } } // --------------------------------------------------------------------------- // ArrayRowSingleColBuilder // --------------------------------------------------------------------------- /** Array row for primitives. * * A row with single td element. */ public static class ArrayRowSingleColBuilder extends ElementBuilder { public String build(String elementContents, String elementName) { return "<tr style='min-width: 60px;'><td style='min-width: 60px;'>"+elementContents+"</td></tr>"; } } // --------------------------------------------------------------------------- // PaginatedTable // --------------------------------------------------------------------------- /** A table with pagination controls. * * Use this builder when large data is returned not at once. */ public static class PaginatedTable extends ArrayBuilder { protected final String _offsetJSON; protected final String _viewJSON; protected final JsonObject _query; protected final long _max; protected final boolean _allowInfo; protected final long _offset; protected final int _view; public PaginatedTable(JsonObject query, long offset, int view, long max, boolean allowInfo, String offsetJSON, String viewJSON) { _offsetJSON = offsetJSON; _viewJSON = viewJSON; _query = query; _max = max; _allowInfo = allowInfo; _offset = offset; _view = view; } public PaginatedTable(JsonObject query, long offset, int view, long max, boolean allowInfo) { this(query, offset, view, max, allowInfo, OFFSET, VIEW); } protected String link(String caption, long offset, int view, boolean disabled) { _query.addProperty(_offsetJSON, offset); _query.addProperty(_viewJSON, view); if (disabled) return "<li class='disabled'><a>"+caption+"</a></li>"; else return "<li><a href='"+RequestStatics.encodeRedirectArgs(_query,null)+"'>"+caption+"</a></li>"; } protected String infoButton() { if (!_allowInfo) return ""; return "<span class='pagination'><ul>"+link("info",-1,_view,_offset==1)+"</ul></span>  "; } protected String pagination() { StringBuilder sb = new StringBuilder(); sb.append("<div style='text-align:center;'>"); sb.append(infoButton()); long firstPageItems = _offset % _view; long lastPageItems = (_max-_offset) % _view; long prevPages = _offset / _view + (firstPageItems>0?1:0); long nextPages = _offset + _view >= _max ? 0 : Math.max(_max-_offset-_view, 0) / _view + (lastPageItems>0?1:0); long lastOffset = _offset + nextPages * _view; long currentIdx = prevPages; long lastIdx = currentIdx+nextPages; long startIdx = Math.max(currentIdx-5,0); long endIdx = Math.min(startIdx + 11, lastIdx); if (_offset == -1) currentIdx = -1; sb.append("<span class='pagination'><ul>"); sb.append(link("|<",0,_view, _offset == 0)); sb.append(link("<",Math.max(_offset-_view,0),_view, currentIdx==0)); if (startIdx>0) sb.append(link("...",0,0,true)); for (long i = startIdx; i <= endIdx; ++i) sb.append(link(String.valueOf(i),_view*i,_view,i == currentIdx)); if (endIdx<lastIdx) sb.append(link("...",0,0,true)); sb.append(link(">",_offset+_view,_view, currentIdx == lastIdx)); sb.append(link(">|",lastOffset,_view, currentIdx == lastIdx)); sb.append("</ul></span>"); sb.append("</div>"); return sb.toString(); } @Override public String header(JsonArray array) { StringBuilder sb = new StringBuilder(); sb.append(pagination()); sb.append(super.header(array)); return sb.toString(); } @Override public String footer(JsonArray array) { StringBuilder sb = new StringBuilder(); sb.append(super.footer(array)); sb.append(pagination()); return sb.toString(); } } public class WarningCellBuilder extends ArrayRowElementBuilder { @Override public String arrayToString(JsonArray arr, String contextName) { StringBuilder sb = new StringBuilder(); String sep = ""; for( JsonElement e : arr ) { sb.append(sep).append(e.getAsString()); sep = "</br>"; } return sb.toString(); } } public class KeyCellBuilder extends ArrayRowElementBuilder { @Override public String elementToString(JsonElement element, String contextName) { String str = element.getAsString(); try { String key = URLEncoder.encode(str,"UTF-8"); String delete = "<a href='RemoveAck.html?"+KEY+"="+key+"'><button class='btn btn-danger btn-mini'>X</button></a>"; return delete + "  " + Inspector.link(str, str); } catch( UnsupportedEncodingException e ) { throw Log.errRTExcept(e); } } } public class KeyMinAvgMaxBuilder extends ArrayRowElementBuilder { private String trunc(JsonObject obj, String fld, int n) { JsonElement je = obj.get(fld); if( je == null || je instanceof JsonNull ) return "<br>"; String s1 = je.getAsString(); String s2 = (s1.length() > n ? s1.substring(0,n) : s1); String s3 = s2.replace(" "," "); return s3+"<br>"; } @Override public String objectToString(JsonObject obj, String contextName) { if (!obj.has(MIN)) return ""; return "<strong>"+trunc(obj,HEADER,10)+"</strong>"+trunc(obj,MIN,6)+trunc(obj,MEAN,6)+trunc(obj,MAX,6); } } }