ConverseService.java

/*
 * Copyright (c) 2002-2019, Mairie de Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.botpress.service;

import fr.paris.lutece.plugins.botpress.service.renderers.BotMessageRenderer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import fr.paris.lutece.plugins.botpress.business.RequestMessage;
import fr.paris.lutece.plugins.chatbot.business.BotPost;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.httpaccess.HttpAccess;
import fr.paris.lutece.util.httpaccess.HttpAccessException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.log4j.Logger;

/**
 * ConverseService
 */
public final class ConverseService
{

    private static final String CONTENT_TYPE = "Content-Type";
    private static final String CONTENT_TYPE_JSON = "application/json";
    private static final String NODE_RESPONSES = "responses";
    private static final String NODE_PAYLOAD = "payload";
    private static final int VERSION_1 = 1;

    private static final String LOGGER_NAME = "botpress";
    private static final Logger LOGGER = Logger.getLogger( LOGGER_NAME );
    private static ObjectMapper _objectMapper = new ObjectMapper( );

    /** Private constructor */
    private ConverseService( )
    {
    }

    /**
     * Get responses from the bot
     * 
     * @param strMessage
     *            The message from the User
     * @param strConversationId
     *            The conversation ID (or UserID)
     * @param strBotApiEntryPointUrl
     *            The URL of the Bot API
     * @param strErrorMessage
     *            Error message sent by the bot if the connection fails
     * @param locale
     *            The locale The locale
     * @return A list of bot responses
     */
    public static List<BotPost> getBotResponse( String strMessage, String strConversationId, String strBotApiEntryPointUrl, String strErrorMessage,
            Locale locale )
    {
        List<BotPost> listPosts = new ArrayList<>( );
        RequestMessage message = new RequestMessage( strMessage );
        String strUrl = null;
        String strJsonResponsePretty = null;
        try
        {
            String strJsonMessage = _objectMapper.writeValueAsString( message );
            HttpAccess client = new HttpAccess( );
            HashMap<String, String> mapRequestHeaders = new HashMap<>( );
            mapRequestHeaders.put( CONTENT_TYPE, CONTENT_TYPE_JSON );
            HashMap<String, String> mapResponseHeaders = new HashMap<>( );
            strUrl = strBotApiEntryPointUrl + strConversationId;
            String strJsonResponse = client.doPostJSON( strUrl, strJsonMessage, mapRequestHeaders, mapResponseHeaders );
            Object jsonResponse = _objectMapper.readTree( strJsonResponse );
            strJsonResponsePretty = _objectMapper.writerWithDefaultPrettyPrinter( ).writeValueAsString( jsonResponse );
            if ( LOGGER.isDebugEnabled( ) )
            {
                LOGGER.debug( "Message : " + strMessage + "\nResponse : \n" + strJsonResponsePretty );
            }
            parseJsonResponse( strJsonResponse, listPosts );
            return listPosts;
        }
        catch( HttpAccessException | IOException ex )
        {
            StringBuilder sbError = new StringBuilder( );
            sbError.append( "Error getting response from Botpress API : " ).append( ex.getMessage( ) );
            if ( strUrl != null )
            {
                sbError.append( "\n - POST URL : " ).append( strUrl );
            }
            if ( strJsonResponsePretty != null )
            {
                sbError.append( "\n - JSON response : " ).append( strJsonResponsePretty );
            }
            AppLogService.error( sbError.toString( ), ex );

            BotPost post = new BotPost( strErrorMessage );
            listPosts.add( post );

            return listPosts;
        }

    }

    /**
     * Parse the JSON response from the bot
     * 
     * @param strJsonResponse
     *            The JSON response
     * @param listPosts
     *            The list of bot posts
     * @throws IOException
     *             if an error occurs
     */
    static void parseJsonResponse( String strJsonResponse, List<BotPost> listPosts ) throws IOException
    {
        JsonNode rootNode = _objectMapper.readTree( strJsonResponse );
        JsonNode responsesNode = rootNode.path( NODE_RESPONSES );
        if(  ! (responsesNode instanceof MissingNode ) )
        {
        Iterator<JsonNode> elements = responsesNode.elements( );
        while ( elements.hasNext( ) )
        {
            JsonNode response = elements.next( );
            BotMessageRenderer renderer = RendererService.getRenderer( response );
            if ( renderer != null )
            {
                BotPost post = new BotPost( renderer.render( _objectMapper.convertValue( response, Map.class ) ), renderer.getPostContentType( ) );
                listPosts.add( post );
            }
        }
        }
        else
        {
            JsonNode payloadNode = rootNode.path( NODE_PAYLOAD );
            if(  ! ( payloadNode instanceof MissingNode ))
            {
                String strText = payloadNode.get( "text" ).asText();
                BotPost post = new BotPost( strText , BotPost.CONTENT_TYPE_TEXT );
                listPosts.add( post );
            }
        }

    }

    /**
     * Returns the list of managed API versions
     * 
     * @return A list of versions
     */
    public static ReferenceList getApiVersions( )
    {
        ReferenceList list = new ReferenceList( );
        list.addItem( VERSION_1, "BotPress Converse API Version 1" );
        return list;

    }

    /**
     * Build the entry point URL
     * 
     * @param strBotKey
     *            The bot key
     * @param strServerUrl
     *            The server URL
     * @param nApiVersion
     *            The API version number
     * @return The entry point URL
     */
    public static String getBotApiEntryPointUrl( String strBotKey, String strServerUrl, int nApiVersion )
    {
        StringBuilder sbEntryPointUrl = new StringBuilder( );

        switch( nApiVersion )
        {
            case VERSION_1:
                sbEntryPointUrl.append( ( strServerUrl.endsWith( "/" ) ) ? strServerUrl : strServerUrl + "/" ).append( "api/v1/bots/" ).append( strBotKey )
                        .append( "/converse/" );
                break;

            default:
                AppLogService.error( "Invalid Bot Press API version number : " + nApiVersion );
                break;

        }
        return sbEntryPointUrl.toString( );

    }

}