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