# TenantRestHandler.java

    /*
    ** Module   : TenantRestHandler.java
    ** Abstract : Class that handles requests for REST administration of tenants.
    **
    ** Copyright (c) 2024-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 RAA 20240604 Created initial version.
    **     RAA 20240610 Added a GET method for listing all the registered tenants.
    **     RAA 20240611 Rewrote printTenants to not use manual string concatenation for JSON format.
    **     RAA 20240627 Added functions for adding/deleting a single database of a tenant. Rewrote parsing
    **                  methods to allow multiple database instances per tenant.
    **     RAA 20240627 Fixed response message in checkTenantEnabled.
    **     RAA 20240702 Changed the initialization process. Removed AddTenantBody. Added executeAndWriteResponse.
    **     RAA 20240702 Fixed path parsing after last initialization change.
    **     RAA 20240715 Clarified the type of database name occurrences.
    **     RAA 20240726 Made distinction between physical and logical database name.
    **     RAA 20240807 Removed TenantDatabaseDescriptorKey occurrences.
    ** 002 RAA 20241011 Added tenant description logic.
    **     RAA 20241014 Added AdminServerImpl.setTargetLive() call before executing the actual task.
    ** 003 RAA 20241106 Added tenant info field.
    ** 004 RAA 20241118 Added path for retrieving the data of a single tenant.
    ** 005 RNC 20250109 Added common auth code in AdminHandlerBase.
    ** 006 OM  20250128 Added REST API for managing domains. Renamed to AdminHandler.java. Added sink for
    **                  collecting and reporting errors occurred during REST invocations.
    **     OM  20250310 Big refactoring. Extracted tenant specific implementation to TenantRestHandler.
    ** 007 OM  20250331 Added c3p0 connection parameters.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.admin.server;

    import com.fasterxml.jackson.core.*;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.logging.*;
    import org.eclipse.jetty.server.*;
    import javax.servlet.http.*;
    import java.io.*;
    import java.util.*;
    import java.util.logging.*;

    /**
     * A handler for administration of tenant resources ({@code _tenant meta table and landlord database}) using
     * REST APIs.
     */
    public class TenantRestHandler
    extends BaseAdminRestHandler
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(TenantRestHandler.class);

       /** Constant for identifying operations with the status of a tenant. */
       private static final String STATUS_SUFFIX = "/status";

       /** Constant for identifying add/remove database operations on a tenant. */
       private static final String DB_SUFFIX = "/db";

       /** The JSON property for {@code c3p0.maxStatementsPerConnection} database connection. */
       private static final String JSON_MAX_STATEMENTS_PER_CONNECTION = "maxStatementsPerConnection";

       /** The JSON property for {@code c3p0.minPoolSize} database connection. */
       private static final String JSON_MIN_POOL_SIZE = "minPoolSize";

       /** The JSON property for {@code c3p0.acquireIncrement} database connection. */
       private static final String JSON_ACQUIRE_INCREMENT = "acquireIncrement";

       /** The JSON property for {@code c3p0.maxPoolSize} database connection. */
       private static final String JSON_MAX_POOL_SIZE = "maxPoolSize";

       /** The JSON property for {@code c3p0.maxIdleTime} database connection. */
       private static final String JSON_MAX_IDLE_TIME = "maxIdleTime";

       /** The JSON property name for database connection URL string. */
       private static final String JSON_URL = "url";

       /** The JSON property name for database user-name. */
       private static final String JSON_USERNAME = "username";

       /** The JSON property name for database password. */
       private static final String JSON_PASSWORD = "password";

       /** A set of accepted keys for the tenant update list. */
       private static final Set<String> acceptedTenantKeys = new HashSet<>(
             Arrays.asList(TenantManager.JSON_TENANT_NAME,
                           TenantManager.JSON_TENANT_DESCRIPTION,
                           TenantManager.JSON_TENANT_INFO));

       /** A set of accepted keys for the database (of a tenant) update list. */
       private static final Set<String> acceptedDatabaseKeys = new HashSet<>(
             Arrays.asList(TenantManager.JSON_PHYSICAL_NAME,
                           TenantManager.JSON_LOGICAL_NAME,
                           JSON_URL,
                           JSON_USERNAME,
                           JSON_PASSWORD));

       /** The singleton which handles tenant REST requests. */
       private static TenantRestHandler instance;

       /**
        * Constructs a REST handler for administration of tenants.
        *
        * @param   ah
        *          The WebAuthHandler used for authorisation.
        */
       private TenantRestHandler(WebAuthHandler ah)
       {
          super("/admin/tenants", ah);
       }

       /**
        * Tries to initialize this REST handler. It does this by reading the directory and creating the
        * {@code TenantRestHandler} according to the settings.
        *
        * @param   ah
        *          The authentication handler which will be used for authorisation.
        *
        * @return  An instance of {@link BaseAdminRestHandler} on success or {@code null} if the directory is not
        *          configured with a REST handler for tenant administration.
        */
       public static TenantRestHandler initialize(WebAuthHandler ah)
       {
          if (!TenantConfig.isEnabled())
          {
             return null;
          }

          if (instance == null)
          {
             instance = new TenantRestHandler(ah);
          }

          return instance;
       }

       /**
        * Implement this method for handling the GET requests. The result should be written directly to
        * {@code response}'s writer.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handleGet(String target, String body, HttpServletResponse response, String token)
       {
          ArrayList<String> errorSink = new ArrayList<>();

          if (target.indexOf('/') == -1)
          {
             if (target.isEmpty())
             {
                // admin/tenant - GET for listing all tenants
                List<TenantManager.Tenant>[] tenants = (ArrayList<TenantManager.Tenant>[]) new ArrayList[1];
                Runnable task = () ->
                {
                   AdminServerImpl.setTargetLive(true, false, true);
                   tenants[0] = AdminServerImpl.listTenants(errorSink);
                };

                wsm.executeInContext(token, task, true);
                try
                {
                   response.getWriter().write(printTenants(tenants[0]));
                }
                catch (IOException e)
                {
                   LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
                   response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                   return;
                }
             }
             else
             {
                // admin/tenant/{tenant-id} - GET for a specific tenant
                TenantManager.Tenant[] tenant = new TenantManager.Tenant[1];
                Runnable task = () ->
                {
                   AdminServerImpl.setTargetLive(true, false, true);
                   tenant[0] = AdminServerImpl.listTenant(target, errorSink);
                };

                wsm.executeInContext(token, task, true);
                try
                {
                   response.getWriter().write(printTenant(tenant[0]));
                }
                catch (IOException e)
                {
                   LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
                   response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                   return;
                }
             }

             response.setStatus(HttpServletResponse.SC_OK);
          }
          else if (target.endsWith(STATUS_SUFFIX))
          {
             // admin/tenant/{tenant-id}/status
             String tenantName = target.substring(0, target.length() - STATUS_SUFFIX.length());
             if (tenantName.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] enabled = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                enabled[0] = AdminServerImpl.isTenantEnabled(tenantName, errorSink);
             };

             if (executeAndWriteResponse(token, response, task, "enabled", enabled, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else
          {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
          }
       }

       /**
        * Implement this method for handling the POST requests. The result should be written directly to
        * {@code response}'s writer.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handlePost(String target, String body, HttpServletResponse response, String token)
       {
          ArrayList<String> errorSink = new ArrayList<>();

          if (target.isEmpty())
          {
             // admin/tenant
             String[] tenantDetails = new String[3];
             List<TenantDatabaseDescriptor> databases = parseAddTenantBody(body, tenantDetails);
             if (databases == null || databases.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.addTenant(tenantDetails[0],
                                                       tenantDetails[1],
                                                       tenantDetails[2],
                                                       databases,
                                                       errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else if (target.endsWith(DB_SUFFIX))
          {
             // admin/tenant/{tenant-id}/db
             String tenantName = target.substring(0, target.length() - DB_SUFFIX.length());
             if (tenantName.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             if (!checkTenantEnabled(token, response, tenantName, errorSink))
             {
                return;
             }

             TenantDatabaseDescriptor dbConfig = parseAddDatabaseToTenantBody(body);
             if (dbConfig == null)
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.addDatabaseToTenant(tenantName, dbConfig, errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else
          {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
          }
       }

       /**
        * Implement this method for handling the PUT requests. The result should be written directly to
        * {@code response}'s writer.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handlePut(String target, String body, HttpServletResponse response, String token)
       {
          ArrayList<String> errorSink = new ArrayList<>();

          if (target.indexOf('/') == -1)
          {
             // admin/tenant/{tenant-id}
             if (target.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             if (!checkTenantEnabled(token, response, target, errorSink))
             {
                return;
             }

             UpdateTenantBody tenant = parseUpdateTenantBody(body);
             if (tenant == null)
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.updateTenant(target,
                                                          tenant.pdbName,
                                                          tenant.bodyMapping,
                                                          tenant.databaseUpdate,
                                                          errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else if (target.endsWith(STATUS_SUFFIX))
          {
             // admin/tenant/{tenant-id}/status
             String tenantName = target.substring(0, target.length() - STATUS_SUFFIX.length());
             if (tenantName.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean status = (Boolean) extractFirstLevelValue(body, "status", Boolean.class);
             if (status == null)
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.changeTenantStatus(tenantName, status, errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else
          {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
          }
       }

       /**
        * Implement this method for handling the DELETE requests. The result should be written directly to
        * {@code response}'s writer.
        *
        * @param   target
        *          The relative target of the request. From the absolute URL, the context prefix was eliminated.
        * @param   body
        *          The payload of the request (optional).
        * @param   response
        *          The response as the {@link Response} object or a wrapper of that request.
        * @param   token
        *          The authentication token.
        */
       protected void handleDelete(String target, String body, HttpServletResponse response, String token)
       {
          ArrayList<String> errorSink = new ArrayList<>();

          if (target.indexOf('/') == -1)
          {
             // admin/tenant/{tenant-name}
             if (target.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             if (!checkTenantEnabled(token, response, target, errorSink))
             {
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.deleteTenant(target, errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else if (target.endsWith(DB_SUFFIX))
          {
             // admin/tenant/{tenant-id}/db
             String tenantName = target.substring(0, target.length() - DB_SUFFIX.length());
             if (tenantName.isEmpty())
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             if (!checkTenantEnabled(token, response, tenantName, errorSink))
             {
                return;
             }

             String databaseName = (String) extractFirstLevelValue(body, "databaseName", String.class);
             if (databaseName == null)
             {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
             }

             Boolean[] success = new Boolean[1];
             Runnable task = () ->
             {
                AdminServerImpl.setTargetLive(true, false, true);
                success[0] = AdminServerImpl.deleteDatabaseFromTenant(tenantName, databaseName, errorSink);
             };

             if (executeAndWriteResponse(token, response, task, success, errorSink))
             {
                response.setStatus(HttpServletResponse.SC_OK);
             }
          }
          else
          {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
          }
       }

       /**
        * List all the tenants from the given list in a JSON format.
        *
        * @param   tenants
        *          The list with the tenants.
        *
        * @return  A string in JSON format with the tenant information.
        */
       private String printTenants(List<TenantManager.Tenant> tenants)
       {
          int numberOfTenants = tenants != null ? tenants.size() : 0;
          if (numberOfTenants == 0)
          {
             return "{\n\"tenants\":[]\n}";
          }

          ObjectMapper objectMapper = mapper.get();
          ObjectNode tenantsNode = objectMapper.createObjectNode();
          ArrayNode tenantArray = objectMapper.createArrayNode();
          for (int i = 0; i < numberOfTenants; i++)
          {
             TenantManager.Tenant tenant = tenants.get(i);
             tenantArray.add(createTenantNode(tenant));
          }

          tenantsNode.set("tenants", tenantArray);
          return tenantsNode.toString();
       }

       /**
        * Create a JSON formatted string with the tenant information extracted from the given tenant.
        *
        * @param   tenant
        *          The tenant for which to print its information.
        *
        * @return  A string in JSON format with the tenant information.
        */
       private String printTenant(TenantManager.Tenant tenant)
       {
          JsonNode tenantNode = createTenantNode(tenant);
          return tenantNode != null ? tenantNode.toString() : "{}";
       }

       /**
        * Create a {@code JsonNode} that stores all the information to be printed about the given tenant.
        *
        * @param   tenant
        *          The tenant for which its information will be stored in the JSON node.
        *
        * @return  The JSON node with the tenant information.
        */
       private JsonNode createTenantNode(TenantManager.Tenant tenant)
       {
          if (tenant == null)
          {
             return null;
          }

          ObjectMapper objectMapper = mapper.get();
          ObjectNode tenantNode = objectMapper.createObjectNode();
          tenantNode.put(TenantManager.JSON_TENANT_NAME, tenant.getTenantName());
          tenantNode.put(TenantManager.JSON_TENANT_DESCRIPTION, tenant.getTenantDescription());
          tenantNode.put(TenantManager.JSON_TENANT_INFO, tenant.getTenantInfo());
          tenantNode.put("status", tenant.getStatus());
          ArrayNode dbNode = objectMapper.createArrayNode();
          for (Map.Entry<String, TenantDatabaseDescriptor> entry : tenant.getDatabaseCredentials().entrySet())
          {
             TenantDatabaseDescriptor c = entry.getValue();
             ObjectNode dbNodeSettings = dbNode.addObject().put(TenantManager.JSON_PHYSICAL_NAME, c.getPdbName())
                                                           .put(TenantManager.JSON_LOGICAL_NAME, c.getLdbName())
                                                           .put(JSON_URL, c.getUrl())
                                                           .put(JSON_USERNAME, c.getUsername());
             if (c.getC3p0MaxStmts() != null)
             {
                dbNodeSettings.put(JSON_MAX_STATEMENTS_PER_CONNECTION, c.getC3p0MaxStmts());
             }
             if (c.getC3p0MinPool() != null)
             {
                dbNodeSettings.put(JSON_MIN_POOL_SIZE, c.getC3p0MinPool());
             }
             if (c.getC3p0MaxPool() != null)
             {
                dbNodeSettings.put(JSON_MAX_POOL_SIZE, c.getC3p0MaxPool());
             }
             if (c.getC3p0AcqInc() != null)
             {
                dbNodeSettings.put(JSON_ACQUIRE_INCREMENT, c.getC3p0AcqInc());
             }
             if (c.getC3p0MaxIdle() != null)
             {
                dbNodeSettings.put(JSON_MAX_IDLE_TIME, c.getC3p0MaxIdle());
             }
          }

          tenantNode.set("databaseInstances", dbNode);
          return tenantNode;
       }

       /**
        * Parse a JSON and extract the required values for adding a tenant.
        *
        * @param   body
        *          The JSON data.
        * @param   tenantDetails
        *          An array that will hold the tenant details, after it is discovered in the body.
        *
        * @return  A list of instances, each one storing a configuration for a database instance.
        */
       private List<TenantDatabaseDescriptor> parseAddTenantBody(String body, String[] tenantDetails)
       {
          try
          {
             JsonNode rootNode = mapper.get().readTree(body);
             if (rootNode.has(TenantManager.JSON_TENANT_NAME))
             {
                tenantDetails[0] = rootNode.get(TenantManager.JSON_TENANT_NAME).asText();
             }
             else
             {
                return null;
             }

             if (rootNode.has(TenantManager.JSON_TENANT_DESCRIPTION))
             {
                tenantDetails[1] = rootNode.get(TenantManager.JSON_TENANT_DESCRIPTION).asText();
             }

             if (rootNode.has(TenantManager.JSON_TENANT_INFO))
             {
                tenantDetails[2] = rootNode.get(TenantManager.JSON_TENANT_INFO).asText();
             }

             List<TenantDatabaseDescriptor> dbCredentials = new ArrayList<>();
             if (rootNode.has("databases"))
             {
                ArrayNode databases = (ArrayNode) rootNode.get("databases");
                for (JsonNode database : databases)
                {
                   JsonNode c3p0MaxStmts = database.get(JSON_MAX_STATEMENTS_PER_CONNECTION);
                   JsonNode c3p0MinPool = database.get(JSON_MIN_POOL_SIZE);
                   JsonNode c3p0MaxPool = database.get(JSON_MAX_POOL_SIZE);
                   JsonNode c3p0AcqInc = database.get(JSON_ACQUIRE_INCREMENT);
                   JsonNode c3p0MaxIdle = database.get(JSON_MAX_IDLE_TIME);
                   dbCredentials.add(new TenantDatabaseDescriptor(
                         database.get(TenantManager.JSON_PHYSICAL_NAME).asText(),
                         database.get(TenantManager.JSON_LOGICAL_NAME).asText(),
                         database.get(JSON_URL).asText(),
                         database.get(JSON_USERNAME).asText(),
                         database.get(JSON_PASSWORD).asText(),
                         c3p0MaxStmts == null ? null : c3p0MaxStmts.asInt(),
                         c3p0MinPool == null ? null : c3p0MinPool.asInt(),
                         c3p0MaxPool == null ? null : c3p0MaxPool.asInt(),
                         c3p0AcqInc == null ? null : c3p0AcqInc.asInt(),
                         c3p0MaxIdle == null ? null : c3p0MaxIdle.asInt(),
                         null));
                }
             }

             return dbCredentials;
          }
          catch (JsonProcessingException e)
          {
             LOG.log(Level.WARNING, "Unable to parse JSON due to " + e.getMessage());
          }

          return null;
       }

       /**
        * Check if a specific tenant is enabled.
        * If not, the function also sets the response accordingly.
        *
        * @param   token
        *          The token used in this request.
        * @param   response
        *          The response for the request.
        * @param   tenantName
        *          The tenant to be checked.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the tenant is enabled, {@code false} otherwise.
        *          {@code null} if the caller does not have sufficient rights or performs a bad request.
        */
       private boolean checkTenantEnabled(String token,
                                          HttpServletResponse response,
                                          String tenantName,
                                          List<String> errorSink)
       {
          Boolean[] enabled = new Boolean[1];
          Runnable task = () ->
          {
             AdminServerImpl.setTargetLive(true, false, true);
             enabled[0] = AdminServerImpl.isTenantEnabled(tenantName, errorSink);
          };

          wsm.executeInContext(token, task, true);
          if (enabled[0] == null || !enabled[0])
          {
             try
             {
                response.getWriter().write("{\n \"message\": " +
                                           "\"Cannot perform operation because tenant is not enabled!\"\n}");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
             }
             catch (IOException e)
             {
                LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return false;
             }
          }

          return true;
       }

       /**
        * Parse a JSON and extract the required values for adding a database to a tenant.
        *
        * @param   body
        *          The JSON data.
        *
        * @return  A {@code TenantDatabaseDescriptor} instance that contains the database information.
        */
       private TenantDatabaseDescriptor parseAddDatabaseToTenantBody(String body)
       {
          try
          {
             JsonNode rootNode = mapper.get().readTree(body);
             String[] fields = {// 5 mandatory Strings:
                                TenantManager.JSON_PHYSICAL_NAME,
                                TenantManager.JSON_LOGICAL_NAME,
                                JSON_URL,
                                JSON_USERNAME,
                                JSON_PASSWORD,
                                // 5 optional integers:
                                JSON_MAX_STATEMENTS_PER_CONNECTION,
                                JSON_MIN_POOL_SIZE,
                                JSON_MAX_POOL_SIZE,
                                JSON_ACQUIRE_INCREMENT,
                                JSON_MAX_IDLE_TIME};
             Object[] dbConfig = new Object[fields.length];
             for (int i = 0; i < fields.length; i++)
             {
                if (rootNode.has(fields[i]))
                {
                   if (i < 5) // String
                   {
                      dbConfig[i] = rootNode.get(fields[i]).asText();
                   }
                   else // int
                   {
                      dbConfig[i] = rootNode.get(fields[i]).asInt();
                   }
                }
                else
                {
                   if (i < 5) // String, mandatory
                   {
                      return null;
                   }
                   else
                   {
                      dbConfig[i] = null;
                   }
                }
             }

             return new TenantDatabaseDescriptor(
                   (String) dbConfig[0], // physicalName
                   (String) dbConfig[1], // logicalName
                   (String) dbConfig[2], // url
                   (String) dbConfig[3], // username
                   (String) dbConfig[4], // password
                   (Integer) dbConfig[5], // maxStatementsPerConnection -> c3p0_max_stmts
                   (Integer) dbConfig[6], // minPoolSize -> c3p0_min_pool
                   (Integer) dbConfig[7], // maxPoolSize -> c3p0_max_pool
                   (Integer) dbConfig[8], // acquireIncrement -> c3p0_acq_inc
                   (Integer) dbConfig[9], // maxIdleTime -> c3p0_max_idle
                   null);
          }
          catch (JsonProcessingException e)
          {
             LOG.log(Level.WARNING, "Unable to parse JSON due to " + e.getMessage());
          }

          return null;
       }

       /**
        * Parse a JSON and extract the required values for updating a tenant or a database assigned to a tenant.
        *
        * @param   body
        *          The JSON data.
        *
        * @return  An {@code UpdateTenantBody} instance that holds the information required by the update.
        */
       private UpdateTenantBody parseUpdateTenantBody(String body)
       {
          try
          {
             ObjectMapper objectMapper = mapper.get();
             JsonNode rootNode = objectMapper.readTree(body);
             UpdateTenantBody tenant = new UpdateTenantBody();
             tenant.setDatabaseUpdate(rootNode.has("databaseName"));
             if (tenant.databaseUpdate)
             {
                tenant.setDatabaseName(rootNode.get("databaseName").asText());
             }

             if (rootNode.has("updateList"))
             {
                JsonNode updateNode = rootNode.get("updateList");
                Map<String, String> updateList = objectMapper.convertValue(updateNode, HashMap.class);
                if (!checkUpdateListKeys(updateList, tenant.databaseUpdate))
                {
                   return null;
                }

                tenant.setBodyMapping(updateList);
                return tenant;
             }
          }
          catch (JsonProcessingException e)
          {
             LOG.log(Level.WARNING, "Unable to parse JSON due to " + e.getMessage());
          }

          return null;
       }

       /**
        * Check if the mapping contains other keys than the accepted ones.
        *
        * @param   updateList
        *          The mapping of updates.
        * @param   databaseUpdate
        *          Is this update targeting a database update?
        *
        * @return  {@code true} if the mapping is correct, meaning that no unknown keys are found,
        *          {@code false} if there is at least an unknown key.
        */
       private boolean checkUpdateListKeys(Map<String, String> updateList, boolean databaseUpdate)
       {
          for (String key : updateList.keySet())
          {
             boolean keyInDatabaseKeys = acceptedDatabaseKeys.contains(key);
             if (!keyInDatabaseKeys && !acceptedTenantKeys.contains(key))
             {
                return false;
             }

             if (!databaseUpdate && keyInDatabaseKeys)
             {
                return false;
             }
          }

          return true;
       }

       /** Class responsible with storing the information required by a tenant update operation. */
       private static class UpdateTenantBody
       {
          /** Is the update targeting a database change, or just tenant details? */
          private boolean databaseUpdate;

          /** The physical name of the database for which the update is intended. */
          private String pdbName;

          /** The mapping of updates represented by the pair (field to be updated, new value). */
          private Map<String, String> bodyMapping;

          /**
           * Setter for the {@code databaseUpdate} field.
           *
           * @param   databaseUpdate
           *          {@code true} if the update is intended for a database, {@code false} if it just targets
           *          tenant details changes.
           */
          private void setDatabaseUpdate(boolean databaseUpdate)
          {
             this.databaseUpdate = databaseUpdate;
          }

          /**
           * Setter for the {@code pdbName} field.
           *
           * @param   pdbName
           *          The physical name of the database.
           */
          private void setDatabaseName(String pdbName)
          {
             this.pdbName = pdbName;
          }

          /**
           * Setter for the {@code bodyMapping} field.
           *
           * @param   bodyMapping
           *          The map with updates.
           */
          private void setBodyMapping(Map<String, String> bodyMapping)
          {
             this.bodyMapping = new HashMap<>(bodyMapping);
          }
       }
    }