javajsongoogle-apijacksongoogle-hangouts

Json to Java POJO with Jackson API Hangaouts


I can't find any help in the internet, so I ask here. I want to create a Google card with java classes. So, the JSON I want to create is:

{
   "thread":{
      "name":"some url here"
   },
   "cards":[
      {
         "sections":[
            {
               "widgets":[
                  {
                     "textParagraph":{
                        "text":"bla bla"
                     }
                  },
                  {
                     "buttons":[
                        {
                           "textButton":{
                              "text":"reminder in 10",
                              "onClick":{
                                 "openLink":{
                                    "url":"some Method"
                                 }
                              }
                           }
                        }
                     ]
                  }
               ]
            }
         ]
      }
   ]
}

I have tried several things. I even discovered a site to generate POJO but I can't understand it. My try goes like this:

 Cards cards = new Cards();
 Sections sections = new Sections();
 Widgets widgets = new Widgets();
 TextParagraph textParagraph = new TextParagraph();
 Text text = new Text();
 Buttons button = new Buttons();
 TextButton textButton = new TextButton();
 OnClick onClick = new OnClick();
 OpenLink openLink = new OpenLink();
 NoNameClass haha = new NoNameClass();
 ThreadX threadx = new ThreadX();

 threadx.name = "spaces/" + reminder.getSpaceId() + "/threads/" + reminder.getThreadId();
 text.text = "<" + reminder.getSenderDisplayName() + "> " + reminder.getWhat();
 openLink.url = "https://media0.giphy.com/media/QNnKbtl03OGsM/giphy.gif?cid=3640f6095c5851de3064736a2ef2345a";
 onClick.openLink = openLink;
 textButton.onClick = onClick;
 textButton.text = "se 10";
 haha.textButton = textButton;
 textParagraph.textParagraph = text;
 button.buttons = Lists.newArrayList(haha);
 widgets.widgets = Lists.newArrayList(textParagraph, button);
 sections.sections = Lists.newArrayList(widgets);
 cards.thread = threadx;
 cards.cards = Lists.newArrayList(sections);

and to send that request I have created a JsonHttpContent

private String send(GenericUrl url, Cards message, String httpMethod) {
    HttpContent content = new JsonHttpContent(new JacksonFactory(),message);

    HttpRequest request;
    try {
        if (httpMethod.equals("POST")) {
            request = requestFactory.buildPostRequest(url, content);
        } else {
            request = requestFactory.buildGetRequest(url);
        }
    } catch (Exception e) {
        logger.error("Error creating request using url: {}", url, e);
        return null;
    }

    String response = "";
    try {
        HttpResponse httpResponse = request.execute();
        response = httpResponse.parseAsString();
    } catch (IOException e) {
        logger.error("Error creating request using url: {}", url, e);
    }

    return response;
}

But that doesn't work at all and I have JsonIgnoreProperties to true but I get that error

{
   "error":{
      "code":400,
      "message":"Message cannot be empty. Discarding empty create message request in spaces/AAAADvB8eGY.",
      "errors":[
         {
            "message":"Message cannot be empty. Discarding empty create message request in spaces/AAAADvB8eGY.",
            "domain":"global",
            "reason":"badRequest"
         }
      ],
      "status":"INVALID_ARGUMENT"
   }
}

I have set a ton of parameters but that doesn't read anything, so I need help.


Solution

  • Google API generally is not easy for building structured data model. On Card Formatting Messages page we can see many different JSON payloads for different types of cards. On Github we can find Hangouts Chat code samples projects which introduces CardResponseBuilder class which provides some builder methods to build different kind of cards. I think, this is a good approach to do that. If you can, you can try to use this class. It has dependency to javax.json library. Using this class and knowing that Jackson serialises Map to JSON Object and List to JSON Array we can create very similar class:

    class CardResponseBuilder {
    
        private interface Builder {
            Object get();
        }
    
        private ObjectBuilder createObjectBuilder() {
            return new ObjectBuilder();
        }
    
        private static class ObjectBuilder implements Builder {
            Map<String, Object> map = new HashMap<>(5);
    
            ObjectBuilder add(String key, Object value) {
                map.put(key, value);
                return this;
            }
    
            ObjectBuilder add(String key, Builder builder) {
                return add(key, builder.get());
            }
    
            @Override
            public Map<String, Object> get() {
                return map;
            }
        }
    
        private ArrayBuilder createArrayBuilder() {
            return new ArrayBuilder();
        }
    
        private static class ArrayBuilder implements Builder {
            List<Object> list = new ArrayList<>(4);
    
            ArrayBuilder add(Builder builder) {
                list.add(builder.get());
                return this;
            }
    
            @Override
            public List<Object> get() {
                return list;
            }
        }
    
        public static final String UPDATE_MESSAGE = "UPDATE_MESSAGE";
        public static final String NEW_MESSAGE = "NEW_MESSAGE";
    
        private ObjectBuilder headerNode;
        private ObjectBuilder responseNode;
        private ArrayBuilder widgetsArray;
        private ArrayBuilder cardsArray;
    
        /**
         * Default public constructor.
         */
        public CardResponseBuilder() {
            this.responseNode = createObjectBuilder();
            this.cardsArray = createArrayBuilder();
            this.widgetsArray = createArrayBuilder();
        }
    
        /**
         * Creates a new CardResponseBuilder object for responding to an interactive card click.
         *
         * @param updateType the update type, either UPDATE_MESSAGE or NEW_MESSAGE.
         */
        public CardResponseBuilder(String updateType) {
            this();
            responseNode.add("actionResponse",
                    createObjectBuilder().add("type", updateType));
        }
    
        /**
         * Adds a header to the card response.
         *
         * @param title    the header title
         * @param subtitle the header subtitle
         * @param imageUrl the header image
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder header(String title, String subtitle, String imageUrl) {
            this.headerNode = createObjectBuilder()
                    .add("header", createObjectBuilder()
                            .add("title", title)
                            .add("subtitle", subtitle)
                            .add("imageUrl", imageUrl)
                            .add("imageStyle", "IMAGE"));
            return this;
        }
    
        /**
         * Adds a TextParagraph widget to the card response.
         *
         * @param message the message in the text paragraph
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder textParagraph(String message) {
            this.widgetsArray.add(
                    createObjectBuilder()
                            .add("textParagraph",
                                    createObjectBuilder().add("text", message)));
            return this;
        }
    
        /**
         * Adds a KeyValue widget to the card response.
         * <p>
         * For a list of icons that can be used, see:
         * https://developers.google.com/hangouts/chat/reference/message-formats/cards#builtinicons
         *
         * @param key         the key or top label
         * @param value       the value or content
         * @param bottomLabel the content below the key/value pair
         * @param iconName    a specific icon
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder keyValue(String key, String value,
                                            String bottomLabel, String iconName) {
            this.widgetsArray.add(createObjectBuilder()
                    .add("keyValue", createObjectBuilder()
                            .add("topLabel", key)
                            .add("content", value)
                            .add("bottomLabel", bottomLabel)
                            .add("icon", iconName)));
            return this;
        }
    
        /**
         * Adds an Image widget to the card response.
         *
         * @param imageUrl    the URL of the image to display
         * @param redirectUrl the URL to open when the image is clicked.
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder image(String imageUrl, String redirectUrl) {
            this.widgetsArray.add(createObjectBuilder()
                    .add("image", createObjectBuilder()
                            .add("imageUrl", imageUrl)
                            .add("onClick", createObjectBuilder()
                                    .add("openLink", createObjectBuilder()
                                            .add("url", redirectUrl)))));
            return this;
        }
    
        /**
         * Adds a Text Button widget to the card response.
         * <p>
         * When clicked, the button opens a link in the user's browser.
         *
         * @param text        the text on the button
         * @param redirectUrl the link to open
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder textButton(String text, String redirectUrl) {
            this.widgetsArray.add(createObjectBuilder()
                    .add("buttons", createArrayBuilder()
                            .add(createObjectBuilder()
                                    .add("textButton", createObjectBuilder()
                                            .add("text", text)
                                            .add("onClick", createObjectBuilder()
                                                    .add("openLink", createObjectBuilder()
                                                            .add("url", redirectUrl)))))));
            return this;
        }
    
        /**
         * Adds an Image Button widget to the card response.
         * <p>
         * When clicked, the button opens a link in the user's browser.
         *
         * @param iconName    the icon to display
         * @param redirectUrl the link to open
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder imageButton(String iconName, String redirectUrl) {
            this.widgetsArray.add(createObjectBuilder()
                    .add("buttons", createArrayBuilder()
                            .add(createObjectBuilder()
                                    .add("imageButton", createObjectBuilder()
                                            .add("icon", iconName)
                                            .add("onClick", createObjectBuilder()
                                                    .add("openLink", createObjectBuilder()
                                                            .add("url", redirectUrl)))))));
            return this;
        }
    
        /**
         * Adds an interactive Text Button widget to the card response.
         * <p>
         * When clicked, the button sends a new request to the bot, passing along the custom actionName
         * and parameter values. The actionName and parameter values are defined by the developer when the
         * widget is first declared (as shown below).
         *
         * @param text                   the text to display
         * @param actionName             the custom action name
         * @param customActionParameters the custom key value pairs
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder interactiveTextButton(String text, String actionName,
                                                         Map<String, String> customActionParameters) {
    
            // Define the custom action name and parameters for the interactive button.
            ObjectBuilder actionNode = createObjectBuilder()
                    .add("actionMethodName", actionName);
    
            if (customActionParameters != null && customActionParameters.size() > 0) {
                addCustomActionParameters(actionNode, customActionParameters);
            }
    
            this.widgetsArray.add(createObjectBuilder()
                    .add("buttons", createArrayBuilder()
                            .add(createObjectBuilder()
                                    .add("textButton", createObjectBuilder()
                                            .add("text", text)
                                            .add("onClick", createObjectBuilder()
                                                    .add("action", actionNode))))));
            return this;
        }
    
        /**
         * Adds an interactive Image Button widget to the card response.
         * <p>
         * When clicked, the button sends a new request to the bot, passing along the custom actionName
         * and parameter values. The actionName and parameter values are defined by the developer when the
         * widget is first declared (as shown below).
         *
         * @param iconName               the pre-defined icon to display.
         * @param actionName             the custom action name
         * @param customActionParameters the custom key value pairs
         * @return this CardResponseBuilder
         */
        public CardResponseBuilder interactiveImageButton(String iconName, String actionName,
                                                          Map<String, String> customActionParameters) {
    
            // Define the custom action name and parameters for the interactive button.
            ObjectBuilder actionNode = createObjectBuilder()
                    .add("actionMethodName", actionName);
    
            if (customActionParameters != null && customActionParameters.size() > 0) {
                addCustomActionParameters(actionNode, customActionParameters);
            }
    
            this.widgetsArray.add(createObjectBuilder()
                    .add("buttons", createArrayBuilder()
                            .add(createObjectBuilder()
                                    .add("imageButton", createObjectBuilder()
                                            .add("icon", iconName)
                                            .add("onClick", createObjectBuilder()
                                                    .add("action", actionNode))))));
            return this;
        }
    
        /**
         * Builds the card response and returns a JSON object node.
         *
         * @return card response as JSON-formatted string
         */
        public Object build() {
    
            // If you want your header to appear before all other cards,
            // you must add it to the `cards` array as the first / 0th item.
            if (this.headerNode != null) {
                this.cardsArray.add(this.headerNode);
            }
    
            return responseNode.add("cards", this.cardsArray
                    .add(createObjectBuilder()
                            .add("sections", createArrayBuilder()
                                    .add(createObjectBuilder()
                                            .add("widgets", this.widgetsArray)))))
                    .get();
        }
    
        /**
         * Applies sets of custom parameters to the parameter field of an action.
         *
         * @param actionNode             the JSON action node
         * @param customActionParameters the parameters to apply to the custom action
         */
        private void addCustomActionParameters(ObjectBuilder actionNode,
                                               Map<String, String> customActionParameters) {
            ArrayBuilder parametersArray = createArrayBuilder();
    
            customActionParameters.forEach((k, v) -> {
                parametersArray.add(createObjectBuilder()
                        .add("key", k)
                        .add("value", v));
            });
    
            actionNode.add("parameters", parametersArray);
        }
    }
    

    I just replaced JsonObjectBuilder by ObjectBuilder based on Map and JsonArrayBuilder by ArrayBuilder based on List. Below is a simple usage of that builder:

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializationFeature;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class JsonApp {
    
        public static void main(String[] args) throws Exception {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
    
            Object response = new CardResponseBuilder()
                    .textParagraph("bla bla")
                    .textButton("reminder in 10", "some Method")
                    .build();
            System.out.println(mapper.writeValueAsString(response));
        }
    }
    

    Above code prints:

    {
      "cards" : [ {
        "sections" : [ {
          "widgets" : [ {
            "textParagraph" : {
              "text" : "bla bla"
            }
          }, {
            "buttons" : [ {
              "textButton" : {
                "onClick" : {
                  "openLink" : {
                    "url" : "some Method"
                  }
                },
                "text" : "reminder in 10"
              }
            } ]
          } ]
        } ]
      } ]
    }
    

    I did not add methods for handling thread property because I did not find it in documentation but you should be able to do that by yourself.