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);
      }
   }
}