upload current sources

This commit is contained in:
2025-08-15 22:17:59 -07:00
commit f46e155eb7
88 changed files with 21110 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
package dev.alexzaw.rest4i;
import dev.alexzaw.rest4i.api.SessionAPIImpl;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.GlobalProperties;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Base64;
import java.util.Properties;
/**
* Unified Authentication Service that integrates the existing RSE session-based
* authentication with API token authentication for the new integration APIs.
*
* This service provides backward compatibility while supporting additional
* authentication methods for database, file operations, and PDF services.
*
* @author alexzaw
*/
public class UnifiedAuthenticationService {
// This code is not IBM's intellectual property
private static final String API_TOKEN_HEADER = "X-API-Token";
private static final String API_TOKEN_PARAM = "apiToken";
private Properties apiTokens;
/**
* Constructor that initializes the authentication service.
* In a production environment, API tokens would be loaded from a secure configuration.
*/
public UnifiedAuthenticationService() {
this.apiTokens = new Properties();
// In production, load from secure configuration
// For now, this would be configured through system properties or external config
}
/**
* Constructor that allows injection of API tokens configuration.
*
* @param apiTokens Properties containing valid API tokens
*/
public UnifiedAuthenticationService(Properties apiTokens) {
this.apiTokens = apiTokens != null ? apiTokens : new Properties();
}
/**
* Authenticates a request using the existing RSE session authentication
* or API token authentication.
*
* @param req The HTTP request
* @param authorization The authorization header value
* @param allowBasicAuth Whether to allow basic authentication
* @return Authenticated session or null if API token authentication is used
* @throws RestWAUnauthorizedException if authentication fails
*/
public Session authenticateRequest(HttpServletRequest req, String authorization, boolean allowBasicAuth) {
// Check for secure connection if required
checkSecureConnection(req);
// First try API token authentication
if (authenticateWithApiToken(req)) {
// API token authentication successful, return null to indicate
// that session-based authentication is not needed
return null;
}
// Fall back to existing RSE session authentication
return authenticateWithSession(req, authorization, allowBasicAuth);
}
/**
* Authenticates using API token from header or parameter.
*
* @param req The HTTP request
* @return true if API token authentication is successful, false otherwise
*/
private boolean authenticateWithApiToken(HttpServletRequest req) {
// Check header first
String apiToken = req.getHeader(API_TOKEN_HEADER);
// If not in header, check parameter
if (apiToken == null) {
apiToken = req.getParameter(API_TOKEN_PARAM);
}
if (apiToken == null || apiToken.trim().isEmpty()) {
return false;
}
// Validate API token
return validateApiToken(apiToken.trim());
}
/**
* Validates an API token against the configured tokens.
*
* @param token The API token to validate
* @return true if the token is valid, false otherwise
*/
private boolean validateApiToken(String token) {
if (apiTokens == null || apiTokens.isEmpty()) {
return false;
}
// Check if the token exists in the configuration
// In a production environment, tokens should be hashed
return apiTokens.containsValue(token) || apiTokens.containsKey(token);
}
/**
* Authenticates using the existing RSE session mechanism.
* This is the same logic as in RESTService.getSession().
*
* @param req The HTTP request
* @param authorization The authorization header value
* @param allowBasicAuth Whether to allow basic authentication
* @return Authenticated session
* @throws RestWAUnauthorizedException if authentication fails
*/
private Session authenticateWithSession(HttpServletRequest req, String authorization, boolean allowBasicAuth) {
String token = null;
SessionAPIImpl.RSEAPI_LoginCredentials creds = null;
Session session = null;
if (authorization != null) {
if (authorization.startsWith("Bearer")) {
token = authorization.replaceFirst("Bearer ", "").trim();
session = SessionAPIImpl.getSession(token);
} else if (allowBasicAuth && authorization.startsWith("Basic")) {
String encodedUserPassword = authorization.replaceFirst("Basic ", "").trim();
String usernameAndPassword = null;
try {
byte[] decodedBytes = Base64.getDecoder().decode(encodedUserPassword);
usernameAndPassword = new String(decodedBytes, "UTF-8");
} catch (Exception e) {
AsyncLogger.traceDebug("authenticateWithSession", "Base64 decoder error: " + e.getMessage(), new Object[0]);
}
int separatorIndex;
if (usernameAndPassword != null && (separatorIndex = usernameAndPassword.indexOf(':')) != -1) {
creds = new SessionAPIImpl.RSEAPI_LoginCredentials();
creds.userid = usernameAndPassword.substring(0, separatorIndex);
creds.password = usernameAndPassword.substring(separatorIndex + 1);
session = SessionAPIImpl.sessionLogin(creds);
}
}
}
if (session == null) {
throw new RestWAUnauthorizedException("User ID or password is not set or not valid.", new Object[0]);
}
session.lock();
try {
session.setLastUsed();
if (creds != null) {
session.setSingleUse(true);
}
} finally {
session.unlock();
}
return session;
}
/**
* Checks if a secure connection is required and enforces it.
*
* @param req The HTTP request
* @throws dev.alexzaw.rest4i.exception.RestWAForbiddenException if secure connection is required but not present
*/
private void checkSecureConnection(HttpServletRequest req) {
if (GlobalProperties.useSecureConnections() && !req.isSecure()) {
throw new dev.alexzaw.rest4i.exception.RestWAForbiddenException("Resource must be accessed with a secure connection.", new Object[0]);
}
}
/**
* Releases a session if it was created during authentication.
*
* @param session The session to release, can be null
*/
public static void releaseSession(Session session) {
if (session != null) {
if (session.getSingleUse()) {
SessionAPIImpl.sessionLogout(session);
}
session.unlock();
}
}
/**
* Determines if the current request is authenticated via API token.
*
* @param req The HTTP request
* @return true if authenticated via API token, false if via session
*/
public boolean isApiTokenAuthenticated(HttpServletRequest req) {
String apiToken = req.getHeader(API_TOKEN_HEADER);
if (apiToken == null) {
apiToken = req.getParameter(API_TOKEN_PARAM);
}
return apiToken != null && validateApiToken(apiToken.trim());
}
}

View File

@@ -0,0 +1,95 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.ConnectionDroppedException;
import com.ibm.as400.access.ExtendedIOException;
import com.ibm.as400.access.ExtendedIllegalArgumentException;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAConflictException;
import dev.alexzaw.rest4i.exception.RestWAForbiddenException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.exception.RestWANotFoundException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedMustAuthenticateException;
import dev.alexzaw.rest4i.util.AsyncLogger;
import javax.ws.rs.WebApplicationException;
public abstract class APIImpl {
public static void handleException(String classSignature, Exception e, boolean isAuthenticated) {
if (e instanceof WebApplicationException)
throw (WebApplicationException)e;
if (e instanceof com.ibm.as400.access.ObjectDoesNotExistException)
throw new RestWANotFoundException(e.getMessage(), new Object[0]);
if (e instanceof com.ibm.as400.access.AS400SecurityException) {
if (isAuthenticated)
throw new RestWAForbiddenException(e.getMessage(), new Object[0]);
throw new RestWAUnauthorizedMustAuthenticateException("User ID or password is not set or not valid.", new Object[0]);
}
if (e instanceof com.ibm.as400.access.ObjectAlreadyExistsException)
throw new RestWAConflictException(e.getMessage(), new Object[0]);
if (e instanceof javax.net.ssl.SSLException)
throw new RestWAForbiddenException("Unable to connect to server over a secure communication channel. %s", new Object[] { e.getMessage() });
if (e instanceof java.net.ConnectException)
throw new RestWABadRequestException("Unable to connect to server. %s", new Object[] { e.getMessage() });
if (e instanceof java.net.UnknownHostException)
throw new RestWABadRequestException("The host '%s' is not known.", new Object[] { e.getMessage() });
if (e instanceof ConnectionDroppedException) {
int rc = ((ConnectionDroppedException)e).getReturnCode();
switch (rc) {
case 2:
throw new RestWAInternalServerErrorException("The connection to server dropped unexpectedly. Retry operation.", new Object[0]);
}
} else {
if (e instanceof ExtendedIllegalArgumentException) {
int rc = ((ExtendedIllegalArgumentException)e).getReturnCode();
switch (rc) {
case 1:
throw new RestWABadRequestException("The length of a parameter value is not valid.", new Object[0]);
}
throw new RestWABadRequestException("A parameter value is not valid.", new Object[0]);
}
if (e instanceof ExtendedIOException) {
int rc = ((ExtendedIOException)e).getReturnCode();
switch (rc) {
case 24:
throw new RestWAUnauthorizedException(e.getMessage(), new Object[0]);
case 5:
case 13:
throw new RestWAForbiddenException(e.getMessage(), new Object[0]);
case 4:
throw new RestWAConflictException(e.getMessage(), new Object[0]);
case 2:
case 3:
case 7:
throw new RestWANotFoundException(e.getMessage(), new Object[0]);
case 23:
throw new RestWABadRequestException(e.getMessage(), new Object[0]);
}
} else if (e instanceof com.ibm.as400.access.AS400Exception) {
String errorMsg = e.getMessage();
if (errorMsg != null) {
if (errorMsg.indexOf("CPF5812") != -1)
throw new RestWAConflictException(e.getMessage(), new Object[0]);
if (errorMsg.indexOf("CPF9810") != -1 ||
errorMsg.indexOf("CPF9801") != -1)
throw new RestWANotFoundException(e.getMessage(), new Object[0]);
if (errorMsg.indexOf("CPF9802") != -1)
throw new RestWAUnauthorizedException(e.getMessage(), new Object[0]);
if (errorMsg.indexOf("CPF3C31") != -1 ||
errorMsg.indexOf("CPE3014") != -1)
throw new RestWABadRequestException(e.getMessage(), new Object[0]);
if (errorMsg.indexOf("CPF9820") != -1)
throw new RestWAForbiddenException(e.getMessage(), new Object[0]);
}
}
}
AsyncLogger.traceException(classSignature, e, null, new Object[0]);
throw new RestWAInternalServerErrorException(e.getMessage(), new Object[0]);
}
public static void handleException(String classSignature, Exception e) {
handleException(classSignature, e, true);
}
public static abstract class RSEResult {}
}

View File

@@ -0,0 +1,226 @@
package dev.alexzaw.rest4i.api;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAForbiddenException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.session.SessionRepository;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class AdminAPIImpl extends APIImpl {
public static String adminGetSettings(Session session) {
String output = null;
try {
checkIsAdminUser(session);
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("adminUsers", GlobalProperties.getAdminUsers());
json.add("includeUsers", GlobalProperties.getIncludeUsers());
json.add("excludeUsers", GlobalProperties.getExcludeUsers());
json.add("maxFileSize", Long.valueOf(GlobalProperties.getMaxFileSize()));
json.add("maxSessionInactivity", Long.valueOf(GlobalProperties.getSessionMaxInactivity()));
json.add("maxSessionLifetime", Long.valueOf(GlobalProperties.getSessionMaxLifetime()));
json.add("maxSessionUseCount", Long.valueOf(GlobalProperties.getSessionMaxUseCount()));
json.add("maxSessionWaitTime", Long.valueOf(GlobalProperties.getSessionMaxWaitTime()));
json.add("maxSessions", Long.valueOf(GlobalProperties.getMaxSessions()));
json.add("maxSessionsPerUser", Long.valueOf(GlobalProperties.getMaxSessionsPerUser()));
json.add("sessionCleanupInterval", Long.valueOf(GlobalProperties.getSessionCleanupInterval()));
json.endObject();
output = json.toString();
} catch (Exception e) {
handleException("getRSEAPISettings", e);
}
return output;
}
public static void adminSetSettings(Session session, RSEAPI_Settings rseapiSettings) {
try {
checkIsAdminUser(session);
if (rseapiSettings == null)
return;
if (rseapiSettings.persist && (!GlobalProperties.isIBMi() || !session.isLocalhost()))
throw new RestWABadRequestException("The combination of values specified is not supported.", new Object[0]);
if (rseapiSettings.maxFileSize != null)
CommonUtil.isValid("maxFileSize", rseapiSettings.maxFileSize.longValue(),
0L, 15360000L, true);
if (rseapiSettings.maxSessions != null)
CommonUtil.isValid("maxSessions", rseapiSettings.maxSessions.longValue(),
0L, -1L, true);
if (rseapiSettings.maxSessionsPerUser != null)
CommonUtil.isValid("maxSessionsPerUser", rseapiSettings.maxSessionsPerUser.longValue(),
1L, -1L, true);
if (rseapiSettings.maxSessionWaitTime != null)
CommonUtil.isValid("maxWaitTime", rseapiSettings.maxSessionWaitTime.longValue(),
0L, -1L, true);
if (rseapiSettings.maxSessionInactivity != null)
CommonUtil.isValid("maxSessionIdleTime", rseapiSettings.maxSessionInactivity.longValue(),
30L, 7200L, true);
if (rseapiSettings.maxSessionLifetime != null)
CommonUtil.isValid("maxSessionLifetime", rseapiSettings.maxSessionLifetime.longValue(),
30L, -1L, true);
if (rseapiSettings.maxSessionUseCount != null)
CommonUtil.isValid("maxSessionUseCount", rseapiSettings.maxSessionUseCount.longValue(),
1L, -1L, true);
if (rseapiSettings.sessionCleanupInterval != null)
CommonUtil.isValid("sessionCleanupInterval", rseapiSettings.sessionCleanupInterval.longValue(),
30L, 900L, true);
if (rseapiSettings.adminUsers != null)
GlobalProperties.setAdminUserids(rseapiSettings.adminUsers);
if (rseapiSettings.includeUsers != null)
GlobalProperties.setIncludeUserids(rseapiSettings.includeUsers);
if (rseapiSettings.excludeUsers != null)
GlobalProperties.setExcludeUserids(rseapiSettings.excludeUsers);
if (rseapiSettings.maxFileSize != null)
GlobalProperties.setMaxFileSize(rseapiSettings.maxFileSize.longValue());
if (rseapiSettings.maxSessions != null)
GlobalProperties.setMaxSessions(rseapiSettings.maxSessions.longValue());
if (rseapiSettings.maxSessionsPerUser != null)
GlobalProperties.setMaxSessionsPerUser(rseapiSettings.maxSessionsPerUser.longValue());
if (rseapiSettings.maxSessionWaitTime != null)
GlobalProperties.setSessionMaxWaitTime(rseapiSettings.maxSessionWaitTime.longValue());
if (rseapiSettings.maxSessionInactivity != null)
GlobalProperties.setSessionMaxInactivity(rseapiSettings.maxSessionInactivity.longValue());
if (rseapiSettings.maxSessionLifetime != null)
GlobalProperties.setSessionMaxLifetime(rseapiSettings.maxSessionLifetime.longValue());
if (rseapiSettings.maxSessionUseCount != null)
GlobalProperties.setSessionMaxUseCount(rseapiSettings.maxSessionUseCount.longValue());
if (rseapiSettings.sessionCleanupInterval != null)
GlobalProperties.setSessionCleanupInterval(rseapiSettings.sessionCleanupInterval.longValue());
if (rseapiSettings.persist)
GlobalProperties.save();
} catch (Exception e) {
handleException("setRSEAPISettings", e);
}
}
public static String adminGetSessions(Session session) {
String output = null;
try {
checkIsAdminUser(session);
Map<String, AtomicLong> sessionStatistics = SessionRepository.getSessionsStatisticsData();
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("totalSessions", Long.valueOf(SessionRepository.getTotalSessions()));
json.startArray("sessions");
for (String u : sessionStatistics.keySet()) {
json.startObject();
json.add("userid", u);
json.add("sessionCount", Long.valueOf(((AtomicLong)sessionStatistics.get(u)).longValue()));
json.endObject();
}
json.endArray();
json.endObject();
output = json.toString();
} catch (Exception e) {
handleException("getSessions", e);
}
return output;
}
public static void adminClearSessions(Session session, String user) {
try {
checkIsAdminUser(session);
if (user != null && user.length() > 10)
throw new RestWABadRequestException("The length of a parameter value is not valid.", new Object[0]);
SessionRepository.clearSessions(user);
} catch (Exception e) {
handleException("clearSessions", e);
}
}
public static String adminGetMemoryUsage(Session session) {
String output = null;
try {
checkIsAdminUser(session);
Runtime rt = Runtime.getRuntime();
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("jvmFreeMemory", Long.valueOf(rt.freeMemory()));
json.add("jvmMaxMemory", Long.valueOf(rt.maxMemory()));
json.add("jvmTotalMemory", Long.valueOf(rt.totalMemory()));
json.endObject();
output = json.toString();
} catch (Exception e) {
handleException("getMemoryUsage", e);
}
return output;
}
public static String adminGetEnvironment(Session session) {
String output = null;
try {
checkIsAdminUser(session);
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("rseapiBasepath", GlobalProperties.getRSEAPIBasePath());
json.add("rseapiHostname", GlobalProperties.getHostName());
json.add("rseapiPort", Integer.valueOf(CommonUtil.getServerPort()));
json.add("rseapiVersion", GlobalProperties.getRSEAPIVersion());
json.add("osName", GlobalProperties.getOSName());
json.add("osVersion", GlobalProperties.getOSVersion());
json.add("javaVersion", GlobalProperties.getJavaVersion());
json.endObject();
output = json.toString();
} catch (Exception e) {
handleException("getEnvironment", e);
}
return output;
}
private static void checkIsAdminUser(Session session) throws Exception {
if (GlobalProperties.isIBMi() && !session.getHost().equalsIgnoreCase("localhost"))
throw new RestWAForbiddenException("Host connection is not set to localhost.", new Object[0]);
if (!GlobalProperties.isAdminUserid(session.getUserid()) && !CommonUtil.hasAllObjectAuthority(session.getAS400()))
throw new RestWAForbiddenException("User not an administrator.", new Object[0]);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"persist", "adminUsers", "includeUsers", "excludeUsers", "maxFileSize", "maxSessions", "maxSessionsPerUser", "maxSessionInactivity", "maxSessionLifetime", "maxSessionUseCount", "maxSessionWaitTime", "sessionCleanupInterval"})
@Schema(required = true, name = "RSEAPI_Settings", description = "Global settings for RSE API.")
public static class RSEAPI_Settings {
@Schema(required = false, defaultValue = "false", description = "Save settings to property file in persistent storage (hard disk).")
public boolean persist;
@Schema(required = false, description = "User IDs that will be designated as an RSE API administrator.")
public List<String> adminUsers;
@Schema(required = false, description = "User IDs allowed to use RSE API.")
public List<String> includeUsers;
@Schema(required = false, description = "User IDs not allowed to use RSE API.")
public List<String> excludeUsers;
@Schema(required = false, defaultValue = "3072000", minimum = "0", maximum = "15360000", description = "Maximum size of IFS file data that can be processed (reading or writing) by RSE API.")
public Long maxFileSize;
@Schema(required = false, defaultValue = "100", minimum = "-1", description = "Maximum number of total sessions. The value of -1 indicates there is no limit.")
public Long maxSessions;
@Schema(required = false, defaultValue = "20", description = "Maximum number of sessions allowed on a per-user basis. The value of -1 indicates there is no limit. A value other than -1 must be greater than zero.")
public Long maxSessionsPerUser;
@Schema(required = false, defaultValue = "7200", minimum = "30", maximum = "7200", description = "Maximum amount of inactive time, in seconds, before an available sesson is invalidated.")
public Long maxSessionInactivity;
@Schema(required = false, defaultValue = "-1", description = "Maximum life, in seconds, for a session. The value of -1 indicates there is no limit. A value other than -1 must be greater or equal to 30.")
public Long maxSessionLifetime;
@Schema(required = false, defaultValue = "-1", description = "Maximum number of times a session can be used before it is invalidated. The value of -1 indicates there is no limit. A value other than -1 must be greater than zero.")
public Long maxSessionUseCount;
@Schema(required = false, defaultValue = "300", minimum = "-1", description = "Maximum time, in seconds, to wait on a session to become available. The value of -1 indicates there is no limit.")
public Long maxSessionWaitTime;
@Schema(required = false, defaultValue = "300", minimum = "30", maximum = "900", description = "The time interval, in seconds, for how often the session maintenance daemon is run.")
public Long sessionCleanupInterval;
}
}

View File

@@ -0,0 +1,280 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Bin4;
import com.ibm.as400.access.AS400Exception;
import com.ibm.as400.access.AS400Message;
import com.ibm.as400.access.CharConverter;
import com.ibm.as400.access.CommandCall;
import com.ibm.as400.access.ProgramCall;
import com.ibm.as400.access.ProgramParameter;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class CLCommandAPIImpl extends APIImpl {
private static final AS400Bin4 intConverter = new AS400Bin4();
public static CLCommandOperationResult clRunCLCommand(Session session, RSEAPI_CLCommands clCommands) {
if (clCommands == null || !CommonUtil.hasContent(clCommands.clCommands))
throw new RestWABadRequestException("Invalid command. At least one CL command must be specified.", new Object[0]);
CLCommandOperationResult output = new CLCommandOperationResult(clCommands.includeMessageOnSuccess, clCommands.includeMessageHelpText);
for (String cmd : clCommands.clCommands) {
if (CommonUtil.detectReflectedXSS(cmd))
throw new RestWABadRequestException("Reflected cross site scripting vulnerabilities detected in input data.", new Object[0]);
}
AS400 sys = null;
try {
sys = session.getAS400();
CommandCall cmdCall = new CommandCall(sys);
cmdCall.setThreadSafe(false);
for (String cmd : clCommands.clCommands) {
if (!CommonUtil.hasContent(cmd))
continue;
output.totalIssued_++;
if (!cmdCall.run(cmd)) {
output.totalFailures_++;
output.add(cmd, false, cmdCall.getMessageList());
if (!clCommands.continueOnError)
break;
continue;
}
output.totalSuccesses_++;
if (clCommands.includeMessageOnSuccess)
output.add(cmd, true, cmdCall.getMessageList());
}
} catch (Exception e) {
handleException("runCLCommand", e);
}
return output;
}
public static String clGetCommandDefinition(Session session, String commandName, String library, boolean ignoreCase) {
AsyncLogger.traceDebug("getCommandDefinition", "in-parms: commandName='%s', library='%s'", new Object[] { commandName, library });
commandName = (commandName == null) ? "" : commandName.trim();
if (!CommonUtil.hasContent(commandName) || commandName.length() > 10)
throw new RestWABadRequestException("CL command not specified or not valid.", new Object[0]);
library = (library == null) ? "" : library.trim();
if (!CommonUtil.hasContent(library))
library = "*LIBL";
if (library.length() > 10)
throw new RestWABadRequestException("Library not specified or not valid.", new Object[0]);
if (ignoreCase) {
commandName = commandName.toUpperCase();
library = library.toUpperCase();
}
AS400 sys = null;
String output = null;
try {
sys = session.getAS400();
output = doQCDRCMDD(sys, library, commandName);
} catch (Exception e) {
handleException("getCommandDefinition", e);
}
JSONSerializer ser = new JSONSerializer();
ser.startObject().add("definition", output).endObject();
return ser.toString();
}
private static String doQCDRCMDD(AS400 sys, String library, String clCommand) throws Exception {
String clDefinition = null;
int initialReceiverVariableSize = 32768;
ProgramCall pc = CommonUtil.getProgramCall(sys, "/QSYS.LIB/QCDRCMDD.PGM", 6, true);
CharConverter charConverter = new CharConverter(sys.getCcsid(), sys);
CharConverter charConverterUTF8 = new CharConverter(1208, sys);
ProgramParameter[] pgmParmList = pc.getParameterList();
byte[] bytes = null;
String qualifiedCLCommand = String.format("%-10s%-10s", new Object[] { clCommand, library });
bytes = charConverter.stringToByteArray(qualifiedCLCommand);
pgmParmList[0].setInputData(bytes);
bytes = intConverter.toBytes(initialReceiverVariableSize);
pgmParmList[1].setInputData(bytes);
bytes = charConverter.stringToByteArray("DEST0100");
pgmParmList[2].setInputData(bytes);
pgmParmList[3].setOutputDataLength(initialReceiverVariableSize);
bytes = charConverter.stringToByteArray("CMDD0100");
pgmParmList[4].setInputData(bytes);
int bytesReturned = 0;
int bytesAvailable = 0;
int count = 0;
while (true) {
count++;
if (!pc.run()) {
AS400Message[] msgs = pc.getMessageList();
StringBuilder sb = new StringBuilder();
if (msgs != null)
for (int i = 0; i < msgs.length; i++)
sb.append(msgs[i].getID()).append(":").append(msgs[i].getText()).append(" ");
throw new AS400Exception(msgs);
}
byte[] outBuf = pgmParmList[3].getOutputData();
bytesReturned = intConverter.toInt(outBuf, 0);
bytesAvailable = intConverter.toInt(outBuf, 4);
if (bytesReturned > 0) {
clDefinition = charConverterUTF8.byteArrayToString(outBuf, 8, bytesReturned);
break;
}
if (bytesAvailable > 0)
if (count < 2) {
bytes = intConverter.toBytes(bytesAvailable);
pgmParmList[1].setInputData(bytes);
pgmParmList[3].setOutputDataLength(bytesAvailable);
continue;
}
break;
}
if (clDefinition == null || clDefinition.trim().isEmpty())
throw new RestWAInternalServerErrorException("Unable to process the request due to an internal error. %s", new Object[] { "Call to QCDRCMDD() did not return command definition." });
return clDefinition;
}
public static void runClCommand(AS400 sys, String cmd) throws Exception {
if (!CommonUtil.hasContent(cmd))
return;
CommandCall cmdCall = new CommandCall(sys);
cmdCall.setThreadSafe(false);
if (!cmdCall.run(cmd)) {
AS400Message[] msgs = cmdCall.getMessageList();
StringBuilder sb = new StringBuilder();
if (msgs != null)
for (int i = 0; i < msgs.length; i++)
sb.append(msgs[i].getID()).append(":").append(msgs[i].getText()).append(" ");
throw new RestWABadRequestException("CL command failed. Command is [%s]. Error message(s): %s", new Object[] { cmd, sb.toString() });
}
}
public static List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> runClCommands(AS400 sys, List<String> clCommands, boolean continueOnError) throws Exception {
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> es = null;
StringBuilder errorMessages = null;
if (clCommands == null || clCommands.isEmpty())
return es;
CommandCall cmdCall = new CommandCall(sys);
cmdCall.setThreadSafe(false);
for (String cmd : clCommands) {
if (!CommonUtil.hasContent(cmd))
continue;
if (!cmdCall.run(cmd)) {
AS400Message[] msgs = cmdCall.getMessageList();
if (errorMessages == null)
errorMessages = new StringBuilder();
if (msgs != null)
for (int i = 0; i < msgs.length; i++)
errorMessages.append(msgs[i].getID()).append(":").append(msgs[i].getText()).append(" ");
if (errorMessages != null && errorMessages.length() > 0) {
if (continueOnError) {
if (es == null)
es = new ArrayList<>();
es.add(new SessionAPIImpl.SetSessionSettingsResults.ErrorSource(cmd, errorMessages.toString()));
errorMessages.setLength(0);
continue;
}
throw new RestWABadRequestException("CL command failed. Command is [%s]. Error message(s): %s", new Object[] { cmd, errorMessages.toString() });
}
}
}
return es;
}
public static class CLCommandOperationResult {
ArrayList<CLCommandAPIImpl.CLCommandStatus> clCommandStatus = new ArrayList<>();
int totalIssued_ = 0;
int totalSuccesses_ = 0;
int totalFailures_ = 0;
boolean includeHelpText_ = false;
boolean includeMessageOnSuccess_ = false;
public CLCommandOperationResult(boolean includeMessageOnSuccess, boolean includeHelpText) {
this.includeHelpText_ = includeHelpText;
this.includeMessageOnSuccess_ = includeMessageOnSuccess;
}
public void add(String cmd, boolean success, AS400Message[] msgs) {
CLCommandAPIImpl.CLCommandStatus cs = new CLCommandAPIImpl.CLCommandStatus();
this.clCommandStatus.add(cs);
cs.success_ = success;
cs.command_ = cmd;
if (msgs != null) {
cs.commandOutput_ = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < msgs.length; i++) {
sb.append(msgs[i].getID()).append(": ").append(msgs[i].getText()).append(" ");
if (this.includeHelpText_ && msgs[i].getHelp() != null)
sb.append(msgs[i].getHelp());
cs.commandOutput_.add(sb.toString());
sb.setLength(0);
}
}
}
public String toJSON() {
if (this.totalFailures_ > 0 || this.includeMessageOnSuccess_) {
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("totalIssued", Integer.valueOf(this.totalIssued_));
json.add("totalSuccesses", Integer.valueOf(this.totalSuccesses_));
json.add("totalFailures", Integer.valueOf(this.totalFailures_));
json.startArray("commandOutputList");
if (!this.clCommandStatus.isEmpty())
for (CLCommandAPIImpl.CLCommandStatus cs : this.clCommandStatus) {
if (cs.success_ && !this.includeMessageOnSuccess_)
continue;
json.startObject();
json.add("success", Boolean.valueOf(cs.success_));
json.add("command", cs.command_);
json.add("output", cs.commandOutput_);
json.endObject();
}
json.endArray();
json.endObject();
return json.toString();
}
return null;
}
}
private static class CLCommandStatus {
String command_;
ArrayList<String> commandOutput_;
boolean success_;
private CLCommandStatus() {
this.command_ = null;
this.commandOutput_ = null;
this.success_ = false;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"continueOnError", "includeMessageOnSuccess", "includeMessageHelpText", "clCommands"})
@Schema(required = true, name = "RSEAPI_CLCommands", description = "List of CL commands to run.")
public static class RSEAPI_CLCommands {
@Schema(required = false, defaultValue = "false", description = "Continue processing CL commands if an error is encountered.")
public boolean continueOnError;
@Schema(required = false, defaultValue = "false", description = "Return CL command messages on success.")
public boolean includeMessageOnSuccess;
@Schema(required = false, defaultValue = "false", description = "Return message help text for CL command messages.")
public boolean includeMessageHelpText;
@Schema(required = true, description = "CL command to run.")
public ArrayList<String> clCommands;
}
}

View File

@@ -0,0 +1,495 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400File;
import com.ibm.as400.access.AS400FileRecordDescription;
import com.ibm.as400.access.ExtendedIOException;
import com.ibm.as400.access.IFSFile;
import com.ibm.as400.access.IFSFileInputStream;
import com.ibm.as400.access.IFSFileOutputStream;
import com.ibm.as400.access.IFSTextFileInputStream;
import com.ibm.as400.access.IFSTextFileOutputStream;
import com.ibm.as400.access.Record;
import com.ibm.as400.access.RecordFormat;
import com.ibm.as400.access.SequentialFile;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAConflictException;
import dev.alexzaw.rest4i.exception.RestWANotFoundException;
import dev.alexzaw.rest4i.exception.RestWAPreconditionFailedException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.ChecksumUtil;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JSONSerializer;
import dev.alexzaw.rest4i.util.SystemObject;
import dev.alexzaw.rest4i.util.SystemObjectFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class IFSAPIImpl extends APIImpl {
public static String ifsListDir(Session session, String path, String subtypeFilter, boolean includehidden) {
path = CommonUtil.normalizePath(path);
AS400 sys = null;
List<SystemObject> list = null;
List<SystemObject.SystemObjectInfo> objInfoList = null;
if (!CommonUtil.hasContent(subtypeFilter))
subtypeFilter = null;
AsyncLogger.traceDebug("listDir", "in-parms: path='%s', subtype='%s', includehidden=%s", new Object[] { path, subtypeFilter, Boolean.toString(includehidden) });
try {
String root = "/";
String pattern = null;
String[] pathArray = null;
if (path.indexOf("*") != -1) {
long splatCount = path.chars().filter(num -> (num == 42)).count();
boolean walkthrough = true;
if (splatCount == 1L) {
int index = path.lastIndexOf("/");
if (path.indexOf("*", index) != -1) {
pattern = path.substring(index + 1);
root = (index == 0) ? "/" : path.substring(0, index);
walkthrough = false;
}
}
if (walkthrough)
pathArray = path.split("/");
} else {
root = path;
}
sys = session.getAS400();
SystemObject rootDirObject = SystemObjectFactory.getSystemObject(sys, root);
if (pathArray == null) {
list = rootDirObject.listDir(includehidden, pattern, subtypeFilter);
} else {
list = new ArrayList<>();
walkthrough(rootDirObject, 0, pathArray, list, includehidden, subtypeFilter);
}
objInfoList = new ArrayList<>();
for (SystemObject f : list) {
SystemObject.SystemObjectInfo fi = f.getInfo(false);
if (fi != null)
objInfoList.add(fi);
}
} catch (Exception e) {
handleException("listDir", e);
}
JSONSerializer json = new JSONSerializer();
json.startObject();
json.startArray("objects");
if (!objInfoList.isEmpty())
for (SystemObject.SystemObjectInfo f : objInfoList)
f.toJSON(json);
json.endArray();
json.endObject();
return json.toString();
}
public static void ifsDeleteFile(Session session, String path) {
AsyncLogger.traceDebug("ifsDeleteFile", "in-parms: path='%s'", new Object[] { path });
AS400 sys = null;
boolean deletedFile = false;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
deletedFile = sysObj.delete();
if (!deletedFile) {
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
throw new RestWABadRequestException("Unable to delete object specified by path '%s'.", new Object[] { sysObj.getPath() });
}
} catch (Exception e) {
handleException("deleteFile", e);
}
}
public static IFSFileOperationResult ifsGetFileContent(Session session, String path, boolean setEtag) {
SequentialFile sequentialFile = null;
AsyncLogger.traceDebug("getContents", "in-parms: path='%s', setEtag='%s'", new Object[] { path, Boolean.valueOf(setEtag) });
AS400 sys = null;
IFSTextFileInputStream istream = null;
AS400File as400file = null;
StringBuilder sb = null;
IFSFileOperationResult fileOpResult = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
if (!sysObj.isFile())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
if (sysObj.getSize() > GlobalProperties.getMaxFileSize())
throw new RestWABadRequestException("File '%s' has size %d which exceeds maximum allowed file size of %d.", new Object[] { sysObj.getPath(), Long.valueOf(sysObj.getSize()), Long.valueOf(GlobalProperties.getMaxFileSize()) });
if (sysObj.isQSYSObject()) {
if (!sysObj.isQSYSPFMBR())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
sequentialFile = new SequentialFile(sys, path);
AS400FileRecordDescription fileRecDesc = new AS400FileRecordDescription(sys, sysObj.getPath());
RecordFormat rf = fileRecDesc.retrieveRecordFormat()[0];
sequentialFile.setRecordFormat(rf);
sequentialFile.open(0, 500, 3);
sb = new StringBuilder();
Record record = null;
int dataField = rf.getNumberOfFields() - 1;
while ((record = sequentialFile.readNext()) != null) {
String data = (String)record.getField(dataField);
sb.append(CommonUtil.trimRight(data)).append("\n");
}
} else {
int sizeToRead = 131072;
istream = new IFSTextFileInputStream(sys, sysObj.getIFSFile(), -3);
sb = new StringBuilder();
String fileData;
while (!(fileData = istream.read(sizeToRead)).isEmpty())
sb.append(fileData);
}
fileOpResult = new IFSFileOperationResult();
fileOpResult.fileContentInternal_ = new FileContentInternal();
fileOpResult.fileContentInternal_.ccsid = sysObj.getCCSID();
fileOpResult.fileContentInternal_.content = sb.toString();
if (setEtag)
fileOpResult.etagHeader_ = ChecksumUtil.getMessageDigest(fileOpResult.fileContentInternal_.content);
} catch (Exception e) {
handleException("getContents", e);
} finally {
try {
if (istream != null)
istream.close();
} catch (Exception exception) {}
try {
if (sequentialFile != null)
sequentialFile.close();
} catch (Exception exception) {}
}
return fileOpResult;
}
public static void ifsCreateFile(Session session, String path, RSEAPI_NewIFSFileAttributes fileInfo) {
AsyncLogger.traceDebug("createFile", "in-parms: path='%s', NewIFSFileAttributes=[ccsid='%s', description='%s', permissions='%s', type='%s']", new Object[] { path, Integer.valueOf(fileInfo.ccsid), fileInfo.description, fileInfo.permissions, fileInfo.type });
if (fileInfo == null || !CommonUtil.hasContent(fileInfo.type))
throw new RestWABadRequestException("Path name and type are required.", new Object[0]);
String objectType = fileInfo.type.toLowerCase();
if (!objectType.equals("file") && !objectType.equals("directory"))
throw new RestWABadRequestException("The value for parameter '%s' is not valid.", new Object[] { "type" });
AS400 sys = null;
boolean createdFile = false;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
IFSFile file = sysObj.getIFSFile();
if (objectType.equals("file")) {
if (sysObj.isQSYSPFMBR()) {
SequentialFile sf = new SequentialFile(sys, sysObj.getPath());
sf.addPhysicalFileMember(sf.getMemberName(), fileInfo.description);
createdFile = true;
} else {
createdFile = file.createNewFile();
file.setCCSID(fileInfo.ccsid);
}
} else {
createdFile = file.mkdirs();
}
if (!createdFile)
throw new RestWAConflictException("Target path '%s' already exists or path is not valid.", new Object[] { sysObj.getPath() });
} catch (Exception e) {
handleException("createFile", e);
}
}
public static String ifsPutFileContent(Session session, String path, String matchEtag, String content) {
SequentialFile sequentialFile = null;
AsyncLogger.traceDebug("updateFile", "in-parms: path='%s', matchEtag='%s'", new Object[] { path, matchEtag });
AS400 sys = null;
IFSFile file = null;
IFSTextFileOutputStream ostream = null;
IFSFileInputStream fileis = null;
int ccsid = 1208;
String newChecksum = null;
AS400File as400file = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
file = sysObj.getIFSFile();
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
if (!sysObj.isFile())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
ccsid = sysObj.getCCSID();
if (matchEtag != null) {
fileis = new IFSFileInputStream(file, -4);
String checksum = ChecksumUtil.getMessageDigest((InputStream)fileis);
fileis.close();
if (!checksum.equals(matchEtag))
throw new RestWAPreconditionFailedException("Update failed. File referenced by path '%s' does not match client version.", new Object[] { sysObj.getPath() });
}
if (content == null)
content = "";
if (sysObj.isQSYSObject()) {
if (!sysObj.isQSYSPFMBR())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
sequentialFile = new SequentialFile(sys, sysObj.getPath());
AS400FileRecordDescription fileRecDesc = new AS400FileRecordDescription(sys, sysObj.getPath());
RecordFormat rf = fileRecDesc.retrieveRecordFormat()[0];
sequentialFile.setRecordFormat(rf);
String[] lines = content.split("\n");
Record dataRecord = rf.getNewRecord();
ArrayList<String> clCommands = new ArrayList<>();
clCommands.add(String.format("QSYS/CLRPFM FILE(%s/%s) MBR(%s)", new Object[] { sysObj.getQSYSPath().getLibraryName(),
sysObj.getQSYSPath().getObjectName(),
sysObj.getQSYSPath().getMemberName() }));
CLCommandAPIImpl.runClCommands(sys, clCommands, false);
int blockingLevel = Integer.min(lines.length, 500);
sequentialFile.open(1, blockingLevel, 3);
byte b;
int i;
String[] arrayOfString1;
for (i = (arrayOfString1 = lines).length, b = 0; b < i; ) {
String data = arrayOfString1[b];
dataRecord.setField(2, data);
sequentialFile.write(dataRecord);
b++;
}
} else {
ostream = new IFSTextFileOutputStream(sys, file, -4, false, ccsid);
ostream.write(content);
}
newChecksum = (matchEtag == null) ? null : ChecksumUtil.getMessageDigest(content);
} catch (Exception e) {
handleException("updateFile", e);
} finally {
try {
if (fileis != null)
fileis.close();
} catch (Exception exception) {}
try {
if (ostream != null)
ostream.close();
} catch (Exception exception) {}
try {
if (sequentialFile != null)
sequentialFile.close();
} catch (Exception exception) {}
}
return newChecksum;
}
public static IFSFileOperationResult ifsGetFileRawContent(Session session, String path, boolean convert, boolean setEtag) {
if (convert)
return ifsGetFileContent(session, path, setEtag);
AsyncLogger.traceDebug("ifsGetFileRawContent", "in-parms: path='%s', convert='%s', setEtag='%s'", new Object[] { path, Boolean.valueOf(convert), Boolean.valueOf(setEtag) });
AS400 sys = null;
IFSFile file = null;
IFSFileInputStream istream = null;
IFSFileOperationResult fileOpResult = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
file = sysObj.getIFSFile();
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
if (!sysObj.isFile())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
if (convert) {
IFSTextFileInputStream iFSTextFileInputStream = new IFSTextFileInputStream(sys, file.getAbsolutePath(), -3);
} else {
istream = new IFSFileInputStream(sys, file.getAbsolutePath(), -3);
}
fileOpResult = new IFSFileOperationResult();
fileOpResult.iStream_ = istream;
if (setEtag)
fileOpResult.etagHeader_ = ChecksumUtil.getMessageDigest((InputStream)istream);
} catch (Exception e) {
try {
if (istream != null)
istream.close();
} catch (Exception exception) {}
handleException("getContentsBinary", e);
}
return fileOpResult;
}
public static String ifsPutFileRawContent(Session session, String path, boolean convert, String matchEtag, byte[] fileData) {
AsyncLogger.traceDebug("updateFileRaw", "in-parms: path='%s', convert='%s', matchEtag='%s'", new Object[] { path, Boolean.valueOf(convert), matchEtag });
AS400 sys = null;
IFSFile file = null;
IFSFileOutputStream ostream = null;
IFSFileInputStream fileis = null;
InputStream byteStream = null;
int ccsid = 1208;
String newChecksum = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
file = sysObj.getIFSFile();
if (sysObj.exists()) {
if (!sysObj.isFile())
throw new RestWABadRequestException("Operation on object referenced by the path '%s' is not supported.", new Object[] { sysObj.getPath() });
ccsid = file.getCCSID();
if (matchEtag != null) {
fileis = new IFSFileInputStream(file, -4);
String checksum = ChecksumUtil.getMessageDigest((InputStream)fileis);
fileis.close();
if (!checksum.equals(matchEtag))
throw new RestWAConflictException("Update failed. File referenced by path '%s' does not match client version.", new Object[] { sysObj.getPath() });
}
}
ostream = new IFSFileOutputStream(sys, file.getAbsolutePath(), -4, false, ccsid);
ostream.write(fileData);
ostream.close();
byteStream = new ByteArrayInputStream(fileData);
newChecksum = (matchEtag == null) ? null : ChecksumUtil.getMessageDigest(byteStream);
} catch (Exception e) {
handleException("updateFile", e);
} finally {
try {
if (fileis != null)
fileis.close();
} catch (Exception exception) {}
try {
if (ostream != null)
ostream.close();
} catch (Exception exception) {}
try {
if (byteStream != null)
byteStream.close();
} catch (Exception exception) {}
}
return newChecksum;
}
public static void ifsRenameFile(Session session, String path, String newName) {
AsyncLogger.traceDebug("renameFile", "in-parms: path='%s', newName='%s'", new Object[] { path, newName });
AS400 sys = null;
IFSFile file = null;
IFSFile fileNew = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
file = sysObj.getIFSFile();
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
SystemObject sysObjNew = SystemObjectFactory.getSystemObject(sys, newName);
fileNew = sysObjNew.getIFSFile();
if (sysObjNew.exists())
throw new RestWAConflictException("Target path '%s' already exists or path is not valid.", new Object[] { sysObjNew.getPath() });
if (!file.renameTo(fileNew))
throw new RestWAConflictException("Target path '%s' already exists or path is not valid.", new Object[] { sysObjNew.getPath() });
} catch (Exception e) {
handleException("renameFile", e);
}
}
public static void ifsCopyFile(Session session, String path, String destination) {
AsyncLogger.traceDebug("copyFile", "in-parms: path='%s', destination='%s'", new Object[] { path, destination });
AS400 sys = null;
IFSFile file = null;
try {
sys = session.getAS400();
SystemObject sysObj = SystemObjectFactory.getSystemObject(sys, path);
file = sysObj.getIFSFile();
if (!sysObj.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { sysObj.getPath() });
SystemObject sysObjNew = new SystemObject(sys, destination);
if (sysObjNew.exists())
throw new RestWAConflictException("Target path '%s' already exists or path is not valid.", new Object[] { sysObjNew.getPath() });
if (!file.copyTo(destination))
throw new RestWAConflictException("Target path '%s' already exists or path is not valid.", new Object[] { sysObjNew.getPath() });
} catch (Exception e) {
handleException("copyFile", e);
}
}
public static String ifsGetFileInfo(Session session, String path) {
AsyncLogger.traceDebug("getFileInfo", "in-parms: path='%s'", new Object[] { path });
AS400 sys = null;
SystemObject.SystemObjectInfo objInfo = null;
try {
sys = session.getAS400();
SystemObject rootDirObject = SystemObjectFactory.getSystemObject(sys, path);
if (!rootDirObject.exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { rootDirObject.getPath() });
objInfo = rootDirObject.getInfo(true);
} catch (Exception e) {
handleException("getFileInfo", e);
}
JSONSerializer json = new JSONSerializer();
objInfo.toJSON(json);
return json.toString();
}
private static void walkthrough(SystemObject root, int paternIdx, String[] wildcards, List<SystemObject> matchingPaths, boolean includeHidden, String subtypeFilter) throws Exception {
if (paternIdx == wildcards.length)
return;
String pattern = wildcards[paternIdx++];
List<SystemObject> objList = null;
try {
objList = root.listDir(includeHidden, pattern, subtypeFilter);
} catch (ExtendedIOException e) {
int rc = e.getReturnCode();
if (rc == 5 || rc == 13)
return;
throw e;
}
if (objList.isEmpty())
return;
if (paternIdx < wildcards.length)
for (SystemObject f : objList) {
if (f.isDirectory())
walkthrough(f, paternIdx, wildcards, matchingPaths, includeHidden, subtypeFilter);
}
if (paternIdx == wildcards.length)
matchingPaths.addAll(objList);
}
public static class IFSFileOperationResult extends APIImpl.RSEResult {
public boolean setEtag_ = false;
public String etagHeader_ = null;
public IFSFileInputStream iStream_ = null;
public IFSAPIImpl.FileContentInternal fileContentInternal_ = null;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"content"})
@Schema(required = true, name = "RSEAPI_FileContent", description = "The file content.")
public static class RSEAPI_FileContent {
@Schema(required = true)
public String content;
}
public static class FileContentInternal {
public int ccsid;
public String content;
public String toJSON() {
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("ccsid", Integer.valueOf(this.ccsid));
json.add("content", this.content);
json.endObject();
return json.toString();
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"type", "permissions", "ccsid", "description"})
@Schema(required = true, name = "RSEAPI_NewIFSFileAttributes", description = "File attributes for newly created file.")
public static class RSEAPI_NewIFSFileAttributes {
public String type;
public String permissions;
public int ccsid;
public String description;
}
}

View File

@@ -0,0 +1,157 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Exception;
import com.ibm.as400.access.ExtendedIOException;
import com.ibm.as400.access.ObjectDescription;
import com.ibm.as400.access.ObjectList;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.JSONSerializer;
import dev.alexzaw.rest4i.util.SystemObject;
import dev.alexzaw.rest4i.util.SystemObjectQSYS;
import java.util.ArrayList;
import java.util.List;
public class QSYSAPIImpl extends APIImpl {
public static QSYSOperationResult qsysGetQsysObjects(Session session, String objectName, String objectLibrary, String objectType, String objectSubtype, String memberName, String memberType) {
AS400 sys = null;
ObjectList objList = null;
List<SystemObject.SystemObjectInfo> qsysObjList = new ArrayList<>();
boolean subtypeSpecified = false;
try {
if (CommonUtil.hasContent(objectName)) {
objectName = objectName.trim();
if (objectName.length() == 11 && objectName.endsWith("*"))
objectName = objectName.substring(0, objectName.length() - 1);
}
objectName = checkParam("objectName", objectName, 10, null);
objectLibrary = checkParam("objectLibrary", objectLibrary, 10, "*USRLIBL");
objectType = checkParam("objectType", objectType, 10, "*ALL");
objectSubtype = checkParam("objectSubtype", objectSubtype, 10, "*ALL");
memberName = checkParam("memberName", memberName, 10, "");
memberType = checkParam("memberType", memberType, 10, "*ALL");
if (memberName.equalsIgnoreCase("*ALL"))
memberName = "*";
subtypeSpecified = !objectSubtype.equalsIgnoreCase("*ALL");
AsyncLogger.traceDebug("search",
"in-parms: objectName='%s', objectLibrary='%s', objectType='%s', objectSubtype='%s', memberName='%s', memberType='%s', ", new Object[] { objectName, objectLibrary, objectType, objectSubtype, memberName, memberType });
if (objectLibrary.equalsIgnoreCase("*ALL") && objectType.equalsIgnoreCase("*ALL") && objectName.equalsIgnoreCase("*ALL") && !subtypeSpecified)
throw new RestWABadRequestException("The combination of values specified is not supported.", new Object[0]);
sys = session.getAS400();
objList = new ObjectList(sys, objectLibrary, objectName, objectType);
ObjectDescription[] objects = objList.getObjects(-1, 0);
if (objects != null) {
SystemObjectQSYS qsysObject = null;
byte b;
int i;
ObjectDescription[] arrayOfObjectDescription;
for (i = (arrayOfObjectDescription = objects).length, b = 0; b < i; ) {
ObjectDescription objdesc = arrayOfObjectDescription[b];
String path = objdesc.getPath();
if (path.equalsIgnoreCase("/qsys.lib/qsys.lib"))
continue;
if (subtypeSpecified) {
try {
String subtype = objdesc.getValueAsString(202);
String requestedSubtype = objectSubtype.toUpperCase();
if (subtype.equals("PF") && (
requestedSubtype.startsWith("PF-") ||
requestedSubtype.startsWith("PF38-") ||
requestedSubtype.startsWith("PF"))) {
boolean reqsrcpf = !(!objectSubtype.equalsIgnoreCase("PF-SRC") && !objectSubtype.equalsIgnoreCase("PF38-SRC"));
boolean reqpf = requestedSubtype.equalsIgnoreCase("PF");
qsysObject = new SystemObjectQSYS(sys, objdesc);
boolean isSrcPF = qsysObject.getQSYSPFSubtype().equalsIgnoreCase("PF-SRC");
if (!reqpf && ((!isSrcPF && reqsrcpf) || (isSrcPF && !reqsrcpf)))
continue;
if (CommonUtil.hasContent(memberName)) {
List<SystemObject> mbrs = qsysObject.listDir(true, memberName, memberType);
for (SystemObject f : mbrs) {
SystemObject.SystemObjectInfo systemObjectInfo = f.getInfo(false);
if (systemObjectInfo != null)
qsysObjList.add(systemObjectInfo);
}
continue;
}
} else {
if (!subtype.equalsIgnoreCase(objectSubtype))
continue;
qsysObject = new SystemObjectQSYS(sys, objdesc);
}
} catch (ExtendedIOException e) {
int rc = e.getReturnCode();
if (rc != 5 && rc != 13)
throw e;
continue;
} catch (AS400Exception e) {
String errorMsg = e.getMessage();
if (errorMsg != null)
if (errorMsg.indexOf("CPF9802") != -1 || errorMsg.indexOf("CPF9822") != -1)
continue;
throw e;
}
} else {
qsysObject = new SystemObjectQSYS(sys, objdesc);
}
SystemObject.SystemObjectInfo fi = qsysObject.getInfo(false);
if (fi != null)
qsysObjList.add(fi);
continue;
}
}
} catch (Exception e) {
handleException("search", e);
} finally {
if (objList != null)
try {
objList.close();
} catch (Exception exception) {}
}
QSYSOperationResult qsysOpResult = new QSYSOperationResult();
qsysOpResult.objectList_ = new QSYSObjectList();
qsysOpResult.objectList_.qsysObjects_ = qsysObjList;
return qsysOpResult;
}
private static String checkParam(String nm, String objectValue, int maxLen, String defaultValue) {
if (!CommonUtil.hasContent(objectValue)) {
if (defaultValue == null)
throw new RestWABadRequestException("Object name not set.", new Object[0]);
objectValue = defaultValue;
}
objectValue = objectValue.trim();
if (maxLen != -1 && !objectValue.startsWith("*") && objectValue.length() > maxLen)
throw new RestWABadRequestException("The length of the value for parameter '%s' is not valid.", new Object[] { nm });
if (objectValue.equals("*"))
objectValue = "*ALL";
return objectValue;
}
public static class QSYSOperationResult {
public QSYSAPIImpl.QSYSObjectList objectList_ = null;
public boolean setEtag_ = false;
public String etagHeader_ = null;
}
public static class QSYSObjectList {
public List<SystemObject.SystemObjectInfo> qsysObjects_ = null;
public String toJSON() {
JSONSerializer json = new JSONSerializer();
json.startObject();
json.startArray("objects");
if (!this.qsysObjects_.isEmpty())
for (SystemObject.SystemObjectInfo f : this.qsysObjects_)
f.toJSON(json);
json.endArray();
json.endObject();
return json.toString();
}
}
}

View File

@@ -0,0 +1,505 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.AS400JDBCConnection;
import com.ibm.as400.access.AS400JDBCResultSet;
import com.ibm.as400.access.AS400JDBCResultSetMetaData;
import com.ibm.as400.access.AS400JDBCStatement;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class SQLAPIImpl extends APIImpl {
public static SQLOperationResult runSQLStatement(Session session, RSEAPI_SQLRequest sqlRequest) {
if (sqlRequest == null || !CommonUtil.hasContent(sqlRequest.sqlStatement))
throw new RestWABadRequestException("Invalid SQL statement. At least one SQL statement must be specified.", new Object[0]);
SQLOperationResult output = new SQLOperationResult();
try {
boolean alwaysReturnSQLStateInformation = (sqlRequest.alwaysReturnSQLStateInformation != null) ?
sqlRequest.alwaysReturnSQLStateInformation.booleanValue() : false;
boolean treatWarningsAsErrors = (sqlRequest.treatWarningsAsErrors != null) ?
sqlRequest.treatWarningsAsErrors.booleanValue() : session.getSQLTreatWarningsAsErrors();
RSESQLPayload payload = runSQLStatement(session, sqlRequest.sqlStatement, alwaysReturnSQLStateInformation, treatWarningsAsErrors);
output.add(sqlRequest.sqlStatement, payload);
} catch (Exception e) {
handleException("runSQLStatement", e);
}
return output;
}
private static RSESQLPayload runSQLStatement(Session session, String stmt, boolean alwaysReturnSQLStateInformation, boolean treatWarningsAsErrors) throws Exception {
AS400JDBCConnection dbconn = null;
AS400JDBCStatement dbstmt = null;
RSESQLPayload sp = new RSESQLPayload(alwaysReturnSQLStateInformation, true, treatWarningsAsErrors);
try {
dbconn = session.getJDBC();
SQLWarning sw = dbconn.getWarnings();
sp.setWarning(sw);
if (sw == null || !treatWarningsAsErrors) {
dbstmt = (AS400JDBCStatement)dbconn.createStatement();
sw = dbstmt.getWarnings();
sp.setWarning(sw);
if (sw == null || !treatWarningsAsErrors) {
boolean resultSet = dbstmt.execute(stmt);
sw = dbstmt.getWarnings();
sp.setWarning(sw);
if (sw == null || !treatWarningsAsErrors)
sp.generatePayload(dbstmt, resultSet);
}
}
} catch (SQLException _e) {
sp.setException(_e);
} finally {
try {
if (dbstmt != null)
dbstmt.close();
} catch (Exception exception) {}
}
return sp;
}
public static void sqlExceptionCheck(SQLException se, String stmt, boolean treatWarningsAsErrors) throws Exception {
if (se == null || (se instanceof SQLWarning && !treatWarningsAsErrors))
return;
StringBuilder sb = new StringBuilder();
sb.append(se.getMessage());
throw new RestWAInternalServerErrorException("Session initialization failed. The failed SQL statement is [%s]. Error message(s): %s", new Object[] { stmt, sb.toString() });
}
public static List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> runSQLStatements(AS400JDBCConnection dbconn, List<String> sqlStatements, boolean treatWarningsAsErrors, boolean continueOnError) throws Exception {
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> es = null;
AS400JDBCStatement dbstmt = null;
String currentStmt = "*N";
try {
sqlExceptionCheck(dbconn.getWarnings(), "*N", treatWarningsAsErrors);
dbconn.clearWarnings();
if (sqlStatements == null || sqlStatements.isEmpty())
return es;
dbstmt = (AS400JDBCStatement)dbconn.createStatement();
sqlExceptionCheck(dbstmt.getWarnings(), "*N", treatWarningsAsErrors);
} catch (SQLException sqlex) {
sqlExceptionCheck(sqlex, currentStmt, treatWarningsAsErrors);
} finally {
try {
if (dbstmt != null)
dbstmt.close();
} catch (Exception exception) {}
}
try {
if (dbstmt != null)
dbstmt.close();
} catch (Exception exception) {}
return es;
}
public static class SQLOperationResult extends APIImpl.RSEResult {
ArrayList<SQLAPIImpl.RSESQLPayload> sqlStatementResults_ = new ArrayList<>();
int totalIssued_ = 0;
int totalSuccesses_ = 0;
int totalFailures_ = 0;
public boolean processResults() {
return ((SQLAPIImpl.RSESQLPayload)this.sqlStatementResults_.get(0)).processResults();
}
public void add(String cmd, SQLAPIImpl.RSESQLPayload payload) {
this.sqlStatementResults_.add(payload);
this.totalIssued_++;
if (payload.sqlOperationFailed()) {
this.totalFailures_++;
} else {
this.totalSuccesses_++;
}
}
public String toJSON() {
Iterator<SQLAPIImpl.RSESQLPayload> iterator = this.sqlStatementResults_.iterator();
if (iterator.hasNext()) {
SQLAPIImpl.RSESQLPayload payload = iterator.next();
return payload.toJSON();
}
return null;
}
}
public static class RSESQLPayload {
public abstract class RSESQLResult {
protected SQLWarning _sqlWarnings = null;
protected int[] _fieldTypes = null;
protected String[] _fieldTypeNames = null;
public abstract StringBuilder toJSON(StringBuilder param2StringBuilder);
public abstract boolean isEmpty();
}
public class RSESQLResultSet extends RSESQLResult {
protected String[] _columnHeaders = null;
protected ArrayList<Object[]> _rows = new ArrayList();
public StringBuilder toJSON(StringBuilder payload) {
String[] sanitizedColHdrs = SQLAPIImpl.RSESQLPayload.sanitizeColHdrsJSON(this._columnHeaders);
payload.append("[\n");
int rowsProcessed = 0;
for (Object[] array : this._rows) {
if (rowsProcessed++ > 0)
payload.append(",\n");
if (array == null || array.length == 0) {
payload.append("null");
continue;
}
payload.append("{ ");
int fieldsProcessed = 0;
for (int i = 0; i < array.length; i++) {
if (fieldsProcessed++ > 0)
payload.append(", ");
payload.append("\"").append(sanitizedColHdrs[i]).append("\" : ");
if (array[i] == null) {
payload.append("null");
} else if (SQLAPIImpl.RSESQLPayload.isNumeric(this._fieldTypes[i], this._fieldTypeNames[i])) {
payload.append(array[i]);
} else if (SQLAPIImpl.RSESQLPayload.isBinary(this._fieldTypes[i], this._fieldTypeNames[i])) {
payload.append("\"").append(CommonUtil.toBase64((byte[])array[i])).append("\"");
} else if (SQLAPIImpl.RSESQLPayload.isString(this._fieldTypes[i], this._fieldTypeNames[i])) {
payload.append("\"").append(JSONSerializer.escapeJSON((String)array[i])).append("\"");
} else if (this._fieldTypes[i] == 91) {
payload.append("\"").append(CommonUtil.toDate((Date)array[i])).append("\"");
} else if (this._fieldTypes[i] == 92) {
payload.append("\"").append(CommonUtil.toTime((Time)array[i])).append("\"");
} else if (this._fieldTypes[i] == 93) {
payload.append("\"").append(CommonUtil.toTimestamp((Timestamp)array[i])).append("\"");
} else {
payload.append("null");
}
}
payload.append("}");
}
payload.append("\n]");
return payload;
}
public boolean isEmpty() {
return !(this._rows != null && !this._rows.isEmpty());
}
}
private ArrayList<SQLWarning> _sqlWarnings = null;
private SQLException _sqlException = null;
private ArrayList<RSESQLResult> _results = null;
private int _resultSetCount = 0;
private boolean _returnSQLStateAlways = false;
private boolean _returnSQLStateErrors = false;
private boolean _treatWarningAsError = false;
private ArrayList<Integer> _updateCount = new ArrayList<>();
public RSESQLPayload(boolean returnSQLStateAlways, boolean returnSQLStateErrors, boolean treatWarningAsError) {
this._returnSQLStateAlways = returnSQLStateAlways;
this._returnSQLStateErrors = returnSQLStateErrors;
this._treatWarningAsError = treatWarningAsError;
}
public void setWarning(SQLWarning w) {
if (w == null)
return;
if (this._sqlWarnings == null)
this._sqlWarnings = new ArrayList<>();
this._sqlWarnings.add(w);
}
public void setException(SQLException e) {
this._sqlException = e;
}
public boolean warningsExist(boolean checkResults) {
if (this._sqlWarnings != null && this._sqlWarnings.size() > 0)
return true;
if (checkResults && this._results != null && !this._results.isEmpty())
for (RSESQLResult rs : this._results) {
if (rs._sqlWarnings != null)
return true;
}
return false;
}
public void generatePayload(AS400JDBCStatement pstmt, boolean moreResults) throws SQLException {
if (sqlOperationFailed())
return;
if (this._results == null) {
this._results = new ArrayList<>();
} else {
this._results.clear();
}
int count = 0;
do {
if (moreResults) {
AS400JDBCResultSet rs = (AS400JDBCResultSet)pstmt.getResultSet();
this._updateCount.add(Integer.valueOf(-1));
RSESQLResultSet sqlRS = new RSESQLResultSet();
this._results.add(sqlRS);
this._resultSetCount++;
AS400JDBCResultSetMetaData rsMetadata = (AS400JDBCResultSetMetaData)rs.getMetaData();
int colCount = rsMetadata.getColumnCount();
sqlRS._columnHeaders = new String[colCount];
sqlRS._fieldTypes = new int[colCount];
sqlRS._fieldTypeNames = new String[colCount];
for (int i = 1; i <= colCount; i++) {
sqlRS._columnHeaders[i - 1] = rsMetadata.getColumnName(i).trim();
sqlRS._fieldTypes[i - 1] = rsMetadata.getColumnType(i);
sqlRS._fieldTypeNames[i - 1] = rsMetadata.getColumnTypeName(i);
}
sqlRS._sqlWarnings = rs.getWarnings();
while (rs.next()) {
Object[] rows = new Object[colCount];
sqlRS._rows.add(rows);
for (int j = 1; j <= colCount; j++) {
Object fldObj = null;
if (isString(sqlRS._fieldTypes[j - 1], sqlRS._fieldTypeNames[j - 1]) ||
isNumeric(sqlRS._fieldTypes[j - 1], sqlRS._fieldTypeNames[j - 1])) {
fldObj = normalizeString(rs.getString(j));
} else if (isDateTime(sqlRS._fieldTypes[j - 1], sqlRS._fieldTypeNames[j - 1])) {
fldObj = rs.getObject(j);
} else if (isBinary(sqlRS._fieldTypes[j - 1], sqlRS._fieldTypeNames[j - 1])) {
fldObj = rs.getBytes(j);
}
rows[j - 1] = fldObj;
}
}
moreResults = pstmt.getMoreResults();
} else {
count = pstmt.getUpdateCount();
moreResults = pstmt.getMoreResults();
if (count != -1 || moreResults)
this._updateCount.add(Integer.valueOf(count));
}
} while (moreResults || count != -1);
}
public static boolean isDateTime(int t, String coltype) {
return !(t != 91 &&
t != 92 &&
t != 93);
}
public static boolean isBinary(int t, String coltype) {
return !(t != -2 &&
t != -7 &&
t != 2004 &&
t != -4 &&
t != -3);
}
public static boolean isString(int t, String coltype) {
return !(t != 16 &&
t != 1 &&
t != 2005 &&
t != 70 &&
t != -16 &&
t != -1 &&
t != -15 &&
t != 2011 &&
t != -9 &&
t != 12);
}
public static boolean isNumeric(int t, String coltype) {
return !(t != -5 &&
t != 3 &&
t != 8 &&
t != 6 &&
t != 4 &&
t != 2 &&
t != 7 &&
t != 5 &&
t != -6 && (
t != 1111 || !coltype.equalsIgnoreCase("DECFLOAT")));
}
public boolean returnSQLStateInResponse() {
return !((!sqlOperationFailed() || !this._returnSQLStateErrors) && !this._returnSQLStateAlways);
}
private boolean processResults() {
return !(!returnSQLStateInResponse() && (this._results == null || this._results.size() <= 0));
}
private boolean updateCountsExist() {
if (this._updateCount != null)
for (Integer uc : this._updateCount) {
if (uc.intValue() != -1)
return true;
}
return false;
}
public String toJSON() {
if (!processResults())
return "";
StringBuilder payload = new StringBuilder("{\n");
if (returnSQLStateInResponse())
if (updateCountsExist() || this._sqlException != null || warningsExist(false)) {
payload.append("\"SQLStateInfo\": {\n");
payload.append("\"rowsAffectedCounts\": ");
if (!updateCountsExist()) {
payload.append("null");
} else {
payload.append("\"").append(this._updateCount).append("\"");
}
payload.append(",\n");
if (this._sqlException == null) {
payload.append("\"SQLError\": null,\n");
} else {
payload.append("\"SQLError\": {\n");
payload.append("\"SQLState\": \"").append(this._sqlException.getSQLState()).append("\",\n");
payload.append("\"SQLCode\": ").append(this._sqlException.getErrorCode()).append(",\n");
payload.append("\"message\": \"").append(JSONSerializer.escapeJSON(normalizeErrorMessage(this._sqlException, false))).append("\"\n");
payload.append("},\n");
}
if (!warningsExist(false)) {
payload.append("\"SQLWarnings\": null");
} else {
payload.append("\"SQLWarnings\": [\n");
for (int i = 0; i < this._sqlWarnings.size(); i++) {
if (i != 0)
payload.append(",\n");
SQLWarning warning = this._sqlWarnings.get(i);
while (warning != null) {
payload.append("{\n");
payload.append("\"SQLState\": \"").append(warning.getSQLState()).append("\",\n");
payload.append("\"SQLCode\": ").append(warning.getErrorCode()).append(",\n");
payload.append("\"message\": \"").append(JSONSerializer.escapeJSON(normalizeErrorMessage(warning, false))).append("\"\n");
payload.append("}");
warning = warning.getNextWarning();
if (warning != null)
payload.append(",\n");
}
}
payload.append("\n]\n");
}
payload.append("\n}");
} else {
payload.append("\"SQLStateInfo\": null");
}
if (this._results != null && !this._results.isEmpty()) {
if (returnSQLStateInResponse())
payload.append(",\n");
boolean paramsProcessed = false;
int resultSetsProcessed = 0;
for (RSESQLResult r : this._results) {
String rsIdentifier = "resultSet";
if (this._resultSetCount > 1)
rsIdentifier = String.valueOf(rsIdentifier) + ++resultSetsProcessed;
if (resultSetsProcessed > 1 || paramsProcessed)
payload.append(",\n");
payload.append("\"" + rsIdentifier + "\" : ");
if (!r.isEmpty()) {
r.toJSON(payload);
continue;
}
payload.append("[ ]");
}
}
payload.append("\n}");
return payload.toString();
}
public boolean sqlOperationFailed() {
return !(this._sqlException == null && (!this._treatWarningAsError || !warningsExist(false)));
}
public SQLException getException() {
return this._sqlException;
}
public String getErrorInfoAsString() {
if (this._sqlException == null)
return "";
String msg = normalizeErrorMessage(this._sqlException, false);
msg = String.valueOf(msg) + " (SQLState=" + this._sqlException.getSQLState() + ", ErrorCode=" + this._sqlException.getErrorCode() + ")";
return msg;
}
private String normalizeString(String s) {
if (s == null || s.isEmpty())
return s;
return CommonUtil.trimRight(s);
}
private String normalizeErrorMessage(SQLException se, boolean doChained) {
if (se == null)
return "";
String msg = se.getMessage();
if (msg == null)
msg = "";
StringBuilder sbMsg = new StringBuilder(msg);
Throwable t = se.getCause();
while (t != null) {
if (t.getMessage() != null)
sbMsg.append(" ").append(t.getMessage());
t = t.getCause();
}
if (doChained && se instanceof SQLWarning) {
SQLWarning warning = ((SQLWarning)se).getNextWarning();
while (warning != null) {
sbMsg.append(" ").append(warning.getMessage());
warning = warning.getNextWarning();
}
}
return sbMsg.toString();
}
private static String[] sanitizeColHdrsJSON(String[] columnHeaders) {
String[] ar = new String[columnHeaders.length];
int m = 0;
byte b;
int i;
String[] arrayOfString1;
for (i = (arrayOfString1 = columnHeaders).length, b = 0; b < i; ) {
String h = arrayOfString1[b];
ar[m++] = JSONSerializer.escapeJSON(h);
b++;
}
return ar;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"alwaysReturnSQLStateInformation", "treatWarmingsAsErrors", "sqlStatement"})
@Schema(required = true, name = "RSEAPI_SQLRequest", description = "SQL request to run.")
public static class RSEAPI_SQLRequest {
@Schema(required = false, description = "Always return SQL state information. Default value is whatever has been set for the session.")
public Boolean alwaysReturnSQLStateInformation;
@Schema(required = false, description = "Treat SQL warnings as errors. Default value is whatever has been set for the session.")
public Boolean treatWarningsAsErrors;
@Schema(required = true, description = "The SQL statement to be run.")
public String sqlStatement;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
package dev.alexzaw.rest4i.api;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.BinaryConverter;
import com.ibm.as400.access.CharConverter;
import com.ibm.as400.access.Job;
import com.ibm.as400.access.JobLog;
import com.ibm.as400.access.ObjectDescription;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.QueuedMessage;
import com.ibm.as400.access.ServiceProgramCall;
import com.ibm.as400.access.User;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedMustAuthenticateException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.session.SessionRepository;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class SessionAPIImpl extends APIImpl {
public static Session sessionLogin(RSEAPI_LoginCredentials loginCredentials) {
if (loginCredentials == null)
throw new RestWABadRequestException("User ID or password is not set or not valid.", new Object[0]);
String host = loginCredentials.host;
String userid = loginCredentials.userid;
char[] password = null;
if (loginCredentials.password != null) {
password = loginCredentials.password.toCharArray();
loginCredentials.password = null;
}
if (!CommonUtil.hasContent(userid) || userid.length() > 10 || !CommonUtil.hasContent(password))
throw new RestWABadRequestException("User ID or password is not set or not valid.", new Object[0]);
if (!CommonUtil.hasContent(host))
host = "localhost";
if (!GlobalProperties.isIBMi() && (host.trim().equalsIgnoreCase("localhost") || host.trim().equals("127.0.0.1")))
throw new RestWABadRequestException("Host name not set or server is not an IBM i server.", new Object[0]);
if (!GlobalProperties.isIncludeUserid(userid) || GlobalProperties.isExcludeUserid(userid)) {
AsyncLogger.traceAudit("login", "Login attempt by user '%s' failed. ", new Object[] { loginCredentials.userid });
throw new RestWAUnauthorizedException("User ID or password is not set or not valid.", new Object[0]);
}
Session session = null;
try {
session = SessionRepository.createSession(host, userid, password);
} catch (Exception e) {
if (e instanceof com.ibm.as400.access.AS400SecurityException)
AsyncLogger.traceAudit("login", "Login attempt by user '%s' failed. " + e.getMessage(), new Object[] { loginCredentials.userid });
handleException("getSessionToken", e, false);
}
AsyncLogger.traceAudit("login", "User '%s' authenticated.", new Object[] { session.getUserid() });
return session;
}
public static Session getSession(String sessionToken) {
if (!CommonUtil.hasContent(sessionToken))
throw new RestWAUnauthorizedMustAuthenticateException("Authorization token not found.", new Object[0]);
Session session = SessionRepository.findSession(sessionToken);
if (session == null)
throw new RestWAUnauthorizedMustAuthenticateException("Authorization token not valid or may have expired.", new Object[0]);
String remoteAddrHex = "-" + CommonUtil.stringToHex(CommonUtil.getRemoteAddr());
if (!sessionToken.endsWith(remoteAddrHex))
throw new RestWAUnauthorizedMustAuthenticateException("Requestor is not originator of the authorization token.", new Object[0]);
return session;
}
public static void sessionLogout(Session session) {
SessionRepository.deleteSession(session);
}
public static SessionSettingsAndJobInfo sessionQuery(Session session, Integer maxJobLogMessages, String envvars, String jobLogFilter) {
AS400 sys = null;
SessionSettingsAndJobInfo sessionInfo = null;
boolean getLibl = true;
boolean getCurrentLib = true;
boolean getCCSID = true;
boolean getJobID = true;
boolean getJobLog = false;
boolean getEnvVars = false;
if (maxJobLogMessages == null)
maxJobLogMessages = new Integer(0);
long maxMessages = CommonUtil.getNumericProperty(null, maxJobLogMessages.toString(), -1L, 2147483647L, 0L);
if (maxMessages != 0L)
getJobLog = true;
if (CommonUtil.hasContent(envvars))
getEnvVars = true;
try {
sys = session.getAS400();
Job[] jobs = sys.getJobs(2);
if (jobs.length > 0) {
Job rmtCmdJob = jobs[0];
List<QSYSLibraryInfo> sysLibl = null;
List<QSYSLibraryInfo> usrLibl = null;
QSYSLibraryInfo curLib = null;
Integer ccsid = null;
String jobID = null;
List<String> jobLogList = null;
Map<String, String> envVariableList = null;
if (getCurrentLib)
curLib = getLibInfo(sys, rmtCmdJob.getCurrentLibrary());
if (getLibl) {
sysLibl = getLibInfo(sys, rmtCmdJob.getSystemLibraryList());
usrLibl = getLibInfo(sys, rmtCmdJob.getUserLibraryList());
}
if (getCCSID)
ccsid = new Integer(rmtCmdJob.getCodedCharacterSetID());
if (getJobID)
jobID = String.valueOf(rmtCmdJob.getNumber()) + "/" + rmtCmdJob.getUser() + "/" + rmtCmdJob.getName();
if (getJobLog) {
Set<String> filters = null;
if (CommonUtil.hasContent(jobLogFilter)) {
jobLogFilter = jobLogFilter.replaceAll("\\s", "").toUpperCase();
filters = (Set<String>)Arrays.<String>stream(jobLogFilter.split(",")).collect(Collectors.toSet());
}
jobLogList = new ArrayList<>();
JobLog jobLog = rmtCmdJob.getJobLog();
jobLog.setListDirection(false);
int jobLogOffset = (maxMessages == -1L || filters != null) ? -1 : 0;
QueuedMessage[] qmsgs = jobLog.getMessages(jobLogOffset, (int)maxMessages);
int counter = 0;
byte b;
int i;
QueuedMessage[] arrayOfQueuedMessage1;
for (i = (arrayOfQueuedMessage1 = qmsgs).length, b = 0; b < i; ) {
QueuedMessage qmsg = arrayOfQueuedMessage1[b];
String msgID = qmsg.getID();
if (filters == null || msgID == null || !filters.contains(msgID)) {
String mh = String.valueOf(msgID) + ": " + qmsg.getText();
if (CommonUtil.hasContent(mh)) {
jobLogList.add(0, mh);
counter++;
}
if (maxMessages != -1L && counter >= maxMessages)
break;
}
b++;
}
}
if (getEnvVars)
envVariableList = getEnvVariables(sys, Arrays.asList(envvars.split(",")));
String homeDir = (new User(sys, session.getUserid())).getHomeDirectory();
sessionInfo = new SessionSettingsAndJobInfo(session, jobID, ccsid, sysLibl, usrLibl, curLib, jobLogList, envVariableList, homeDir);
}
} catch (Exception e) {
handleException("getSessionSettings", e);
}
return sessionInfo;
}
public static SetSessionSettingsResults sessionRefresh(Session session, RSEAPI_SessionSettings insettings) {
SetSessionSettingsResults sr = null;
try {
sr = session.setSettings(insettings);
} catch (Exception e) {
handleException("setSessionSettings", e);
}
return sr;
}
private static String[] extractBasicCredentials(String authCredentials) throws Exception {
String encodedUserPassword = authCredentials.replaceFirst("Basic ", "");
String usernameAndPassword = null;
byte[] decodedBytes = Base64.getDecoder().decode(encodedUserPassword);
usernameAndPassword = new String(decodedBytes, "UTF-8");
String[] pair = usernameAndPassword.split(":");
return pair;
}
private static QSYSLibraryInfo getLibInfo(AS400 sys, String libName) throws Exception {
if (!CommonUtil.hasContent(libName))
return null;
ObjectDescription objDesc = new ObjectDescription(sys, "/QSYS.LIB/" + libName + ".LIB");
return new QSYSLibraryInfo(objDesc.getName(),
objDesc.getValueAsString(202),
objDesc.getValueAsString(203));
}
private static ArrayList<QSYSLibraryInfo> getLibInfo(AS400 sys, String[] libList) throws Exception {
ArrayList<QSYSLibraryInfo> arr = new ArrayList<>();
if (libList != null && libList.length > 0) {
byte b;
int i;
String[] arrayOfString;
for (i = (arrayOfString = libList).length, b = 0; b < i; ) {
String lib = arrayOfString[b];
QSYSLibraryInfo li = getLibInfo(sys, lib);
if (li != null)
arr.add(li);
b++;
}
}
return arr;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"host", "userid", "password"})
@Schema(required = true, name = "RSEAPI_LoginCredentials", description = "The login credentials.")
public static class RSEAPI_LoginCredentials {
@Schema(required = false, defaultValue = "localhost", description = "The IBM i server from which objects are to be accessed.")
public String host;
@Schema(required = true, description = "The user ID.")
public String userid;
@Schema(required = true, description = "The password.")
public String password;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"resetSettings", "continueOnError", "libraryList", "clCommands", "envVariables", "sqlDefaultSchema", "sqlTreatWarningsAsErrors", "sqlProperties", "sqlStatements"})
@Schema(required = true, name = "RSEAPI_SessionSettings", description = "The settings for the session.")
public static class RSEAPI_SessionSettings {
@Schema(required = false, defaultValue = "false", description = "Replace existing sessions attributes. A value of false will merge settings in request with existing session settings.")
public Boolean resetSettings;
@Schema(required = false, defaultValue = "false", description = "Continue with session processing if an error occurs.")
public Boolean continueOnError;
@Schema(required = false, description = "Library to be added to library list of the remote command host server job tied to session.")
public List<String> libraryList;
@Schema(required = false, description = "CL command to be run in remote command host server job tied to session.")
public List<String> clCommands;
@Schema(required = false, description = "Environment variable to set in remote command host server job tied to session.")
public Map<String, String> envVariables;
@Schema(required = false, description = "Default SQL schema to use when running SQL statements in database host server job. ")
public String sqlDefaultSchema;
@Schema(required = false, defaultValue = "false", description = "Treat SQL warnings as errors. ")
public Boolean sqlTreatWarningsAsErrors;
@Schema(required = false, description = "Java toolbox JDBC property. ")
public Map<String, String> sqlProperties;
@Schema(required = false, description = "SQL statment to be run in database host server job tied to session.")
public List<String> sqlStatements;
}
public static Map<String, String> getEnvVariables(AS400 as400, List<String> envVariables) throws Exception {
if (envVariables == null || envVariables.isEmpty())
return null;
HashMap<String, String> map = new HashMap<>();
ServiceProgramCall spc = CommonUtil.getServiceProgramCall(as400, "/QSYS.LIB/QHTTPSVR.LIB/QZHBCGI.SRVPGM", "QtmhGetEnv", 6, true, false);
CharConverter charConverter = new CharConverter(as400.getCcsid(), as400);
ProgramParameter[] pgmParmList = spc.getParameterList();
for (String envvar : envVariables) {
if (!CommonUtil.hasContent(envvar))
continue;
pgmParmList[0].setOutputDataLength(8192);
pgmParmList[1].setInputData(BinaryConverter.intToByteArray(8192));
pgmParmList[2].setOutputDataLength(4);
pgmParmList[3].setInputData(charConverter.stringToByteArray(String.valueOf(envvar) + "\000"));
pgmParmList[4].setInputData(BinaryConverter.intToByteArray(envvar.length()));
try {
String value = "";
if (spc.run()) {
int envValueLen = BinaryConverter.byteArrayToInt(pgmParmList[2].getOutputData(), 0);
if (envValueLen > 0) {
byte[] output = pgmParmList[0].getOutputData();
value = charConverter.byteArrayToString(output, 0, envValueLen);
}
}
map.put(envvar, value);
} catch (Exception e) {
e.printStackTrace();
throw new RestWAInternalServerErrorException("Unable to process the request due to an internal error. %s", new Object[] { "getenv() failed. " + e.getMessage() });
}
}
return map;
}
public static class SessionSettingsAndJobInfo {
public Session session;
public String jobID;
public Integer ccsid;
public List<SessionAPIImpl.QSYSLibraryInfo> syslibl;
public List<SessionAPIImpl.QSYSLibraryInfo> usrlibl;
public SessionAPIImpl.QSYSLibraryInfo curLib;
public List<String> jobLog;
public Map<String, String> envVariables;
public String homeDirectory;
public SessionSettingsAndJobInfo(Session session, String jobID, Integer ccsid, List<SessionAPIImpl.QSYSLibraryInfo> sysLibl, List<SessionAPIImpl.QSYSLibraryInfo> usrLibl, SessionAPIImpl.QSYSLibraryInfo curLib, List<String> jobLog, Map<String, String> envVariables, String homeDir) {
this.session = session;
this.jobID = jobID;
this.ccsid = ccsid;
this.syslibl = sysLibl;
this.usrlibl = usrLibl;
this.curLib = curLib;
this.jobLog = jobLog;
this.envVariables = envVariables;
this.homeDirectory = homeDir;
}
public String toJSON() {
JSONSerializer json = new JSONSerializer();
json.startObject();
this.session.toJSON(json);
json.startObject("jobRemoteCommand");
json.add("id", this.jobID);
json.add("ccsid", this.ccsid);
json.add("homeDirectory", this.homeDirectory);
if (this.curLib == null) {
json.add("curLib", (String)null);
} else {
this.curLib.toJSON("curLib", json);
}
json.startArray("systemLibl");
if (this.syslibl != null && !this.syslibl.isEmpty())
for (SessionAPIImpl.QSYSLibraryInfo libInfo : this.syslibl)
libInfo.toJSON(null, json);
json.endArray();
json.startArray("userLibl");
if (this.usrlibl != null && !this.usrlibl.isEmpty())
for (SessionAPIImpl.QSYSLibraryInfo libInfo : this.usrlibl)
libInfo.toJSON(null, json);
json.endArray();
json.add("envVariables", this.envVariables);
json.add("jobLog", this.jobLog);
json.endObject();
json.endObject();
return json.toString();
}
}
public static class SetSessionSettingsResults {
public static class ErrorSource {
public String _source;
public String _message;
public ErrorSource(String source, String message) {
this._source = source;
this._message = message;
}
JSONSerializer toJSON(String type, JSONSerializer ser) {
ser.startObject();
ser.add(type, this._source);
ser.add("errorMessage", this._message);
ser.endObject();
return ser;
}
}
private List<ErrorSource> _clCommandErrors = null;
private List<ErrorSource> _sqlStatementErrors = null;
private List<ErrorSource> _libraryListErrors = null;
private List<ErrorSource> _envVariableErrors = null;
private List<String> _sqlInitializationErrors = null;
public SetSessionSettingsResults() {}
public SetSessionSettingsResults(List<ErrorSource> clCommandErrors, List<ErrorSource> sqlStatementErrors, List<ErrorSource> libraryListErrors, List<ErrorSource> envVariableErrors, List<String> sqlInitializationError) {
this._clCommandErrors = clCommandErrors;
this._sqlStatementErrors = sqlStatementErrors;
this._libraryListErrors = libraryListErrors;
this._envVariableErrors = envVariableErrors;
this._sqlInitializationErrors = sqlInitializationError;
}
public boolean sqlInitializationErrors() {
return (this._sqlInitializationErrors != null && !this._sqlInitializationErrors.isEmpty());
}
public boolean errorsOccurred() {
return !((this._clCommandErrors == null || this._clCommandErrors.isEmpty()) && (
this._sqlStatementErrors == null || this._sqlStatementErrors.isEmpty()) && (
this._libraryListErrors == null || this._libraryListErrors.isEmpty()) && (
this._envVariableErrors == null || this._envVariableErrors.isEmpty()) && (
this._sqlInitializationErrors == null || this._sqlInitializationErrors.isEmpty()));
}
public String toJSON() {
JSONSerializer json = new JSONSerializer();
json.startObject();
if (this._clCommandErrors != null && !this._clCommandErrors.isEmpty()) {
json.startArray("clCommandErrors");
for (ErrorSource es : this._clCommandErrors)
es.toJSON("clCommand", json);
json.endArray();
}
if (this._sqlInitializationErrors != null && !this._sqlInitializationErrors.isEmpty())
json.add("sqlInitializationErrors", this._sqlInitializationErrors);
if (this._sqlStatementErrors != null && !this._sqlStatementErrors.isEmpty()) {
json.startArray("sqlStatementErrors");
for (ErrorSource es : this._sqlStatementErrors)
es.toJSON("sqlStatement", json);
json.endArray();
}
if (this._libraryListErrors != null && !this._libraryListErrors.isEmpty()) {
json.startArray("libraryListErrors");
for (ErrorSource es : this._libraryListErrors)
es.toJSON("library", json);
json.endArray();
}
if (this._envVariableErrors != null && !this._envVariableErrors.isEmpty()) {
json.startArray("envVariableErrors");
for (ErrorSource es : this._envVariableErrors)
es.toJSON("variable", json);
json.endArray();
}
json.endObject();
return json.toString();
}
}
public static class QSYSLibraryInfo {
public String name_ = "";
public String attribute_ = "";
public String description_ = "";
public QSYSLibraryInfo(String name, String attribute, String text) {
if (CommonUtil.hasContent(name))
this.name_ = name;
if (CommonUtil.hasContent(attribute))
this.attribute_ = attribute;
if (CommonUtil.hasContent(text))
this.description_ = text;
}
public void toJSON(String identifier, JSONSerializer json) {
if (identifier == null) {
json.startObject();
} else {
json.startObject(identifier);
}
json.add("name", this.name_);
json.add("attribute", this.attribute_);
json.add("description", this.description_);
json.endObject();
}
}
}

View File

@@ -0,0 +1,166 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.AdminAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "Administration Services", description = "Administration Services provide APIs that give information about RSE API and the runtime environment of the RSE API server. To use the APIs, the authenticated user must authenticate to localhost and have the administrator role or have *ALLOBJ special authority.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AdminRESTService extends RESTService {
@Path("/v1/admin/settings")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"adminUsers\": [\"USER1\",\"USER2\"],\n \"includeUsers\": [\"USER1\",\"USER2\"],\n \"excludeUsers\": [],\n \"maxFileSize\": 3072000,\n \"maxSessionInactivity\": 7200,\n \"maxSessionLifetime\": 21600,\n \"maxSessionUseCount\": 1000,\n \"maxSessionWaitTime\": 300,\n \"maxSessions\": 100,\n \"maxSessionsPerUser\": 20,\n \"sessionCleanupInterval\": 300\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get the general settings being used for RSE API. ", description = "The settings are global in nature and include settings for tuning, environment, and administrator override categories. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminGetSettings(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = AdminAPIImpl.adminGetSettings(session);
} catch (Exception e) {
return handleException("getSettings", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/admin/settings")
@POST
@Consumes({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Successful request, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Set settings for RSE API. ", description = "The settings are global in nature and include settings for tuning, environment, and administrator override categories. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminSetSettings(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The settings for RSE API.", required = true, content = {@Content(schema = @Schema(implementation = AdminAPIImpl.RSEAPI_Settings.class), mediaType = "application/json", example = "{\n \"persist\": false,\n \"adminUsers\": [\"USER1\",\"USER2\"],\n \"includeUsers\": [\"USER1\",\"USER2\"],\n \"excludeUsers\": [],\n \"maxFileSize\": 3072000,\n \"maxSessionInactivity\": 7200,\n \"maxSessionLifetime\": 21600,\n \"maxSessionUseCount\": 1000,\n \"maxSessionWaitTime\": 300,\n \"maxSessions\": 100,\n \"maxSessionsPerUser\": 20,\n \"sessionCleanupInterval\": 300\n}")}) AdminAPIImpl.RSEAPI_Settings rseapiSettings) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
AdminAPIImpl.adminSetSettings(session, rseapiSettings);
} catch (Exception e) {
return handleException("setRSEAPISettings", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/admin/sessions")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"totalSessions\": 3,\n \"sessions\": [\n {\n \"userid\": \"USER1\",\n \"sessionCount\": 1\n },\n {\n \"userid\": \"USER2\",\n \"sessionCount\": 2\n }\n ]\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get information about sessions. ", description = "Get information about sessions. The information that is returned applies to active sessions on the server. Active sessions may include sessions that are expired but have not been reclaimed by RSE API.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminGetSessions(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = AdminAPIImpl.adminGetSessions(session);
} catch (Exception e) {
return handleException("getSessions", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/admin/sessions")
@DELETE
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Successful request, no content."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Delete sessions.", description = "Delete sessions. You can delete all active sessions or only sessions tied to a user ID. Sessions that are deleted are marked as expired.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminClearSessions(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "The session(s) to delete. Specify the user ID to delete all sessions created by the user, or the special value of \\*ALL to delete all sessions for all users. ", required = false) @DefaultValue("*ALL") @QueryParam("user") String user) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
AdminAPIImpl.adminClearSessions(session, user);
} catch (Exception e) {
return handleException("clearSessions", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/admin/memory")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"jvmFreeMemory\": 17428920,\n \"jvmMaxMemory\": 4294967296,\n \"jvmTotalMemory\": 78249984\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get information about server memory usage. ", description = "Get information about the JVM memory usage of the server running RSE API. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminGetMemoryUsage(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = AdminAPIImpl.adminGetMemoryUsage(session);
} catch (Exception e) {
return handleException("getMemoryUsage", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/admin/environment")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"rseapiBasepath\": \"rseapi\",\n \"rseapiHostname\": \"UT30P44\",\n \"rseapiPort\": 2012,\n \"rseapiVersion\": \"1.0.6\",\n \"osName\": \"OS/400\",\n \"osVersion\": \"V7R5M0\",\n \"javaVersion\": \"1.8.0_351\"\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get information about server environment. ", description = "Get information about server environment, such as host, operating system, Java version, and port. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response adminGetEnvironment(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = AdminAPIImpl.adminGetEnvironment(session);
} catch (Exception e) {
return handleException("getEnvironment", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,85 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.CLCommandAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "CL Command Services", description = "CL Command Services provide APIs for running CL commands. ")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CLCommandRESTService extends RESTService {
@Path("/v1/cl")
@PUT
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Command(s) issued successfully, error(s) may have occurred.", content = {@Content(mediaType = "application/json", example = "{\n \"totalIssued\": 2,\n \"totalSuccesses\": 1,\n \"totalFailures\": 1,\n \"commandOutputList\": [\n {\n \"success\": false,\n \"command\": \"qsys/crtlib lib1\",\n \"output\": [\n \"CPF2111: Library LIB1 already exists. \"\n ]\n },\n {\n \"success\": true,\n \"command\": \"qsys/crtsrcpf lib1/qrpglesrc\",\n \"output\": [\n \"CPC7301: File QRPGLESRC created in library LIB1. \"\n ]\n }\n ]\n}")}), @APIResponse(responseCode = "204", description = "All command(s) issued successfully, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Run one or more CL commands on the server.", description = "Run one or more CL commands on the server. If a command fails, any messages relating to the error is returned. If the command succeeds, no data is returned. By default, a response payload will be returned if an error occurs that will include the \nfirst level message text. If you want the full message details, set the includeMessageHelpText property to true. If you \nwant messages returned in all cases, set the includeMessageOnSuccess property to true.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response clRunCLCommand(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "List of CL commands to run. If more than one CL command is to be run, you can indicate whether all commands should be run even if an error is encountered while running a CL command.", required = false, content = {@Content(schema = @Schema(implementation = CLCommandAPIImpl.RSEAPI_CLCommands.class), mediaType = "application/json", example = "{\n \"continueOnError\": true,\n \"includeMessageOnSuccess\": true,\n \"includeMessageHelpText\": false,\n \"clCommands\": [\n \"qsys/crtlib lib1\",\n \"qsys/crtsrcpf lib1/qrpglesrc\"\n ]\n}")}) CLCommandAPIImpl.RSEAPI_CLCommands clCommands) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
CLCommandAPIImpl.CLCommandOperationResult output = null;
try {
session = getSession(req, authorization, true);
output = CLCommandAPIImpl.clRunCLCommand(session, clCommands);
payload = output.toJSON();
} catch (Exception e) {
return handleException("runCLCommand", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
if (payload != null)
return Response.status(Response.Status.OK).entity(payload).build();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/cl/{commandname}")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"definition\": \"<QcdCLCmd DTDVersion=\\\"1.0\\\"><Cmd CmdName=\\\"DSPLIBL\\\" CmdLib=\\\"__LIBL\\\" CCSID=\\\"37\\\" HlpPnlGrp=\\\"QHLICMD\\\" HlpPnlGrpLib=\\\"__LIBL\\\" HlpID=\\\"DSPLIBL\\\" MaxPos=\\\"1\\\" Prompt=\\\"Display Library List\\\" MsgF=\\\"QCPFMSG\\\" MsgFLib=\\\"__LIBL\\\" ExecBatch=\\\"YES\\\" ChgCmdExit=\\\"NO\\\" RtvCmdExit=\\\"NO\\\"><Parm Kwd=\\\"OUTPUT\\\" PosNbr=\\\"1\\\" KeyParm=\\\"NO\\\" Type=\\\"CHAR\\\" Min=\\\"0\\\" Max=\\\"1\\\" Prompt=\\\"Output\\\" Len=\\\"1\\\" Rstd=\\\"YES\\\" Dft=\\\"*\\\" AlwUnprt=\\\"YES\\\" AlwVar=\\\"YES\\\" Expr=\\\"YES\\\" Full=\\\"NO\\\" DspInput=\\\"YES\\\" Choice=\\\"*, *PRINT\\\" ><SpcVal><Value Val=\\\"*\\\" MapTo=\\\"*\\\"/><Value Val=\\\"*PRINT\\\" MapTo=\\\"L\\\"/></SpcVal></Parm></Cmd></QcdCLCmd>\"\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Retrieves the command definition for the specified CL command. ", description = "Retrieves the command definition for the specified CL command. The command definition is returned as an XML document. The generated command information XML source is called Command Definition Markup Language or CDML. See the Document Type Definition (DTD) in /QIBM/XML/DTD/QcdCLCmd.dtd for the definition of the CDML tag language returned by this API.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response clGetCommandDefinition(@Context HttpServletRequest req, @HeaderParam("Authorization") String authorization, @Parameter(description = "The library name. Valid values are a specific name, or one of the following special values: \n- \\*CURLIB - The current library is searched. \n- \\*LIBL - The library list is searched. This is the default.", required = false) @DefaultValue("*LIBL") @QueryParam("library") String library, @Parameter(description = "Boolean indicating whether case should be ignored. The default value is 'true'. If set to 'false', the library and command name will not be uppercased.", required = false) @DefaultValue("true") @QueryParam("ignorecase") boolean ignorecase, @Parameter(description = "The CL command name.", required = true) @PathParam("commandname") String commandName) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = CLCommandAPIImpl.clGetCommandDefinition(session, commandName, library, ignorecase);
} catch (Exception e) {
return handleException("getCommandDefinition", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,193 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.api.jaxrs.util.ResponseFormatter;
import dev.alexzaw.rest4i.database.DatabaseManager;
import dev.alexzaw.rest4i.database.QueryResult;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import java.sql.SQLException;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
/**
* REST service for database operations with multiple output format support.
* Follows IBM RSE API patterns and delegates to the integration service implementation.
*
* @author alexzaw
*/
@Path("/")
@Tags({@Tag(name = "Database Services", description = "Database query services with multiple output format support including JSON, XML, CSV, HTML, and Excel formats.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DatabaseRESTService extends RESTService {
public static final String COPYRIGHT = "Licensed Materials - Property of IBM, 5770-SS1 (C) Copyright IBM Corp. 2021, 2021. All rights reserved. US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.";
// Use the database manager directly
private final DatabaseManager databaseManager = new DatabaseManager();
/**
* Database Query Request DTO - mirrors the delegate's structure
*/
public static class DatabaseQueryRequest {
public String sql;
public String format = "json";
public boolean treatWarningsAsErrors = false;
public boolean alwaysReturnSQLStateInformation = false;
}
@Path("/v1/database/query")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8", "application/xml; charset=utf-8",
"text/csv; charset=utf-8", "text/html; charset=utf-8",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"})
@APIResponses({
@APIResponse(responseCode = "200", description = "SQL query executed successfully.",
content = {@Content(mediaType = "application/json")}),
@APIResponse(responseCode = "400", description = "Bad request - invalid SQL or parameters."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "403", description = "Forbidden - dangerous SQL detected."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Execute SQL query with multiple output formats",
description = "Execute a SQL query against the IBM i database with support for multiple output formats including JSON, XML, CSV, HTML, and Excel.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response executeQuery(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "Output format for results (json, xml, csv, html, excel)")
@QueryParam("format") String format,
@RequestBody(description = "The SQL query and formatting options.", required = true,
content = {@Content(schema = @Schema(implementation = DatabaseQueryRequest.class))})
DatabaseQueryRequest queryRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (queryRequest == null || queryRequest.sql == null || queryRequest.sql.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"SQL query is required\"}")
.type("application/json")
.build();
}
// Validate the SQL query for safety
if (!databaseManager.validateQuery(queryRequest.sql)) {
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"error\": \"Dangerous SQL operation detected\"}")
.type("application/json")
.build();
}
// Use format from query param if provided, otherwise use request format
String outputFormat = (format != null) ? format : queryRequest.format;
// Execute the query using DatabaseManager
QueryResult result = databaseManager.executeQuery(session, queryRequest.sql);
// Format and return the result
return ResponseFormatter.formatQueryResult(
result,
outputFormat,
queryRequest.treatWarningsAsErrors,
queryRequest.alwaysReturnSQLStateInformation
);
} catch (Exception e) {
return handleException("executeQuery", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/database/query")
@GET
@Produces({"application/json; charset=utf-8", "application/xml; charset=utf-8",
"text/csv; charset=utf-8", "text/html; charset=utf-8",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"})
@APIResponses({
@APIResponse(responseCode = "200", description = "SQL query executed successfully."),
@APIResponse(responseCode = "400", description = "Bad request - invalid SQL or parameters."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "403", description = "Forbidden - dangerous SQL detected."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Execute SQL query via GET with multiple output formats",
description = "Execute a SQL query via GET request with support for multiple output formats.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response executeQueryGet(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "The SQL query to execute.", required = true)
@QueryParam("sql") String sql,
@Parameter(description = "Output format: json, xml, csv, html, excel")
@QueryParam("format") String format) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (sql == null || sql.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"SQL query parameter is required\"}")
.type("application/json")
.build();
}
// Validate the SQL query for safety
if (!databaseManager.validateQuery(sql)) {
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"error\": \"Dangerous SQL operation detected\"}")
.type("application/json")
.build();
}
// Execute the query using DatabaseManager
QueryResult result = databaseManager.executeQuery(session, sql);
// Format and return the result
return ResponseFormatter.formatQueryResult(
result,
format,
false, // treatWarningsAsErrors defaults to false for GET
false // alwaysReturnSQLStateInformation defaults to false for GET
);
} catch (Exception e) {
return handleException("executeQueryGet", e);
} finally {
releaseSession(session);
}
}
}

View File

@@ -0,0 +1,420 @@
package dev.alexzaw.rest4i.api.jaxrs;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.alexzaw.rest4i.fileops.SMBFileManager;
import jcifs.CIFSContext;
import jcifs.smb.SmbFileInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
/**
* REST service for file operations with SMB/CIFS support.
* Follows IBM RSE API patterns and delegates to the integration service implementation.
*
* @author alexzaw
*/
@Path("/")
@Tags({@Tag(name = "File Operations Services", description = "SMB/CIFS file operations including list, upload, download, zip, and existence checking for network file shares.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FileOperationsRESTService extends RESTService {
public static final String COPYRIGHT = "Licensed Materials - Property of IBM, 5770-SS1 (C) Copyright IBM Corp. 2021, 2021. All rights reserved. US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.";
// Use the SMB file manager directly
private final SMBFileManager fileManager = new SMBFileManager();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* File Operation Request DTO - mirrors the delegate's structure
*/
public static class FileOperationRequest {
public String path;
public String domain;
public String username;
public String password;
}
/**
* Upload Request DTO - mirrors the delegate's structure
*/
public static class UploadRequest {
public String folder;
public String filename;
public String domain;
public String username;
public String password;
public byte[] fileContent;
}
@Path("/v1/files/list")
@GET
@Produces({MediaType.APPLICATION_JSON})
@APIResponses({
@APIResponse(responseCode = "200", description = "Directory listing successful.",
content = {@Content(mediaType = MediaType.APPLICATION_JSON)}),
@APIResponse(responseCode = "400", description = "Bad request - path parameter required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "404", description = "Directory not found."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "List files and directories",
description = "Lists files and directories in the specified SMB path.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response listFiles(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "The SMB path to list.", required = true)
@QueryParam("path") String path,
@Parameter(description = "SMB domain")
@QueryParam("domain") String domain,
@Parameter(description = "SMB username")
@QueryParam("username") String username,
@Parameter(description = "SMB password")
@QueryParam("password") String password) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (path == null || path.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"Path parameter is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Get authentication context
CIFSContext auth = fileManager.getAuthContext(domain, username, password);
// List files
List<Map<String, String>> fileList = fileManager.listFiles(path, auth);
// Transform file list to match API contract
List<Map<String, Object>> transformedFiles = new ArrayList<>();
for (Map<String, String> file : fileList) {
Map<String, Object> transformedFile = new HashMap<>();
transformedFile.put("name", file.get("fileName"));
transformedFile.put("size", Integer.parseInt(file.get("size")));
transformedFile.put("isDirectory", Boolean.parseBoolean(file.get("isDirectory")));
transformedFiles.add(transformedFile);
}
// Format response to match scalar.json API contract
Map<String, Object> data = new HashMap<>();
data.put("path", path);
data.put("files", transformedFiles);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", data);
return Response.ok(objectMapper.writeValueAsString(response))
.type(MediaType.APPLICATION_JSON)
.build();
} catch (Exception e) {
return handleException("listFiles", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/files/download")
@GET
@Produces({MediaType.APPLICATION_OCTET_STREAM})
@APIResponses({
@APIResponse(responseCode = "200", description = "File downloaded successfully."),
@APIResponse(responseCode = "400", description = "Bad request - path parameter required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "404", description = "File not found."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Download a file",
description = "Downloads a file from the specified SMB path.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response downloadFile(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "The SMB path to the file.", required = true)
@QueryParam("path") String path,
@Parameter(description = "SMB domain")
@QueryParam("domain") String domain,
@Parameter(description = "SMB username")
@QueryParam("username") String username,
@Parameter(description = "SMB password")
@QueryParam("password") String password) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (path == null || path.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"Path parameter is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Get authentication context
CIFSContext auth = fileManager.getAuthContext(domain, username, password);
// Read file content
try (SmbFileInputStream fileStream = fileManager.readFile(path, auth)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileStream.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
// Extract filename from path
String fileName = path.substring(path.lastIndexOf('/') + 1);
if (fileName.isEmpty()) {
fileName = "download";
}
return Response.ok(baos.toByteArray())
.type(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
} catch (Exception e) {
return handleException("downloadFile", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/files/upload")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@APIResponses({
@APIResponse(responseCode = "200", description = "File uploaded successfully."),
@APIResponse(responseCode = "400", description = "Bad request - missing required parameters."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Upload a file",
description = "Uploads a file to the specified SMB path.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response uploadFile(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@RequestBody(description = "Upload request with file data.", required = true,
content = {@Content(schema = @Schema(implementation = UploadRequest.class))})
UploadRequest uploadRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (uploadRequest == null || uploadRequest.folder == null || uploadRequest.filename == null || uploadRequest.fileContent == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"Folder, filename, and fileContent are required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Get authentication context
CIFSContext auth = fileManager.getAuthContext(uploadRequest.domain, uploadRequest.username, uploadRequest.password);
// Convert byte array to input stream
try (InputStream inputStream = new ByteArrayInputStream(uploadRequest.fileContent)) {
fileManager.uploadFile(uploadRequest.folder, uploadRequest.filename, inputStream, auth);
}
// Format response
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "File uploaded successfully");
response.put("folder", uploadRequest.folder);
response.put("filename", uploadRequest.filename);
return Response.ok(objectMapper.writeValueAsString(response))
.type(MediaType.APPLICATION_JSON)
.build();
} catch (Exception e) {
return handleException("uploadFile", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/files/zip")
@GET
@Produces({"application/zip"})
@APIResponses({
@APIResponse(responseCode = "200", description = "Directory zipped successfully."),
@APIResponse(responseCode = "400", description = "Bad request - path parameter required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "404", description = "Directory not found."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Zip directory contents",
description = "Creates a ZIP archive of all files in the specified directory.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response zipDirectory(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "The SMB path to the directory.", required = true)
@QueryParam("path") String path,
@Parameter(description = "SMB domain")
@QueryParam("domain") String domain,
@Parameter(description = "SMB username")
@QueryParam("username") String username,
@Parameter(description = "SMB password")
@QueryParam("password") String password) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (path == null || path.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"Path parameter is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Get authentication context
CIFSContext auth = fileManager.getAuthContext(domain, username, password);
// Create ZIP archive
ByteArrayOutputStream zipStream = new ByteArrayOutputStream();
fileManager.zipDirectory(path, zipStream, auth);
// Extract directory name from path for filename
String dirName = path.substring(path.lastIndexOf('/') + 1);
if (dirName.isEmpty()) {
dirName = "archive";
}
return Response.ok(zipStream.toByteArray())
.type("application/zip")
.header("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\"")
.build();
} catch (Exception e) {
return handleException("zipDirectory", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/files/exists")
@GET
@Produces({MediaType.APPLICATION_JSON})
@APIResponses({
@APIResponse(responseCode = "200", description = "Existence check completed."),
@APIResponse(responseCode = "400", description = "Bad request - path parameter required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Check if file or directory exists",
description = "Checks whether a file or directory exists at the specified SMB path.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response checkExists(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@Parameter(description = "The SMB path to check.", required = true)
@QueryParam("path") String path,
@Parameter(description = "SMB domain")
@QueryParam("domain") String domain,
@Parameter(description = "SMB username")
@QueryParam("username") String username,
@Parameter(description = "SMB password")
@QueryParam("password") String password) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (path == null || path.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"Path parameter is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Get authentication context
CIFSContext auth = fileManager.getAuthContext(domain, username, password);
// Check if file/directory exists
boolean exists = fileManager.exists(path, auth);
// Format response
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("path", path);
response.put("exists", exists);
return Response.ok(objectMapper.writeValueAsString(response))
.type(MediaType.APPLICATION_JSON)
.build();
} catch (Exception e) {
return handleException("checkExists", e);
} finally {
releaseSession(session);
}
}
}

View File

@@ -0,0 +1,38 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.Consumes;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.json.Json;
import javax.json.JsonObject;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
@Path("/")
@Tags({@Tag(name = "Health", description = "Simple health/ready checks.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HealthRESTService extends RESTService {
@GET
@Path("/v1/health/ping")
@Operation(summary = "Ping", description = "Lightweight liveness check.")
@APIResponses({
@APIResponse(responseCode = "200", description = "Service is alive")
})
@SecurityRequirement(name = "basicAuth")
public Response ping() {
JsonObject json = Json.createObjectBuilder()
.add("ok", true)
.build();
return Response.ok(json).build();
}
}

View File

@@ -0,0 +1,131 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.IFSAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "IFS Services", description = "Integrated File System (IFS) Services provide APIs for accessing objects in a way that is like personal computer and UNIX operating systems. This includes listing objects in directories, reading from files, and writing to files.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class IFSRESTService extends RESTService {
@Path("/v1/ifs/list")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"objects\": [\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QCLSRC.FILE\",\n \"description\": \"\\\"test\\\"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QCSRC.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QRPGLESRC.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QSQDSRC.FILE\",\n \"description\": \"SQL PROCEDURES\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QSRVSRC.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-DTA\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QWOBJ.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QAUDJR0043.JRNRCV\",\n \"description\": \"\",\n \"isDir\": false,\n \"subType\": \"\"\n }\n ]\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Gets a list of objects in the specified path.", description = "Gets a list of objects in the specified path. The information returned includes the name, whether object is a directory, the description, and the object subtype. For objects that are not in the QSYS.LIB file system, any part of the path may contain an asterisk (\\*) , which is a wildcard that means zero or more instances of any character. For example, a path of '/tmp/am\\*1.txt' will return all objects in directory '/tmp' that have names that begin with 'am' and end with '1.txt'. For QSYS.LIB objects, only generic names may be specified in any part of the path. A generic name is a character string that contains one or more characters followed by an asterisk. For example, '/qsys.lib/am\\*.lib' will return all libraries that have names that start with 'am'. Another example is /qsys.lib/am\\*.\\*, which would return all objects that start with am. This would be equivalent to specifying a path of /qsys.lib/am\\*'.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response ifsListDir(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "The working directory. For example, /u/IBM/test", required = true) @QueryParam("path") String path, @Parameter(description = "Subtype of objects to return. Valid values include a specific object type (*LIB, *FILE, *PGM, *OUTQ, etc.) or *ALL. Note that many file system objects do not have a subtype. For example, any Root, QOpenSys or UDFS object. ", required = false) @QueryParam("subtype") String subtype, @Parameter(description = "Whether to show hidden files.", required = false) @DefaultValue("false") @QueryParam("includehidden") boolean includehidden) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = IFSAPIImpl.ifsListDir(session, path, subtype, includehidden);
} catch (Exception e) {
return handleException("getFiles", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/ifs/{path}")
@GET
@Consumes({"*/*"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"ccsid\": 819,\n \"content\": \"Hello world\\n\"\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get the content of a file.", description = "Get the content of a file. The content is returned in a JSON object. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response ifsGetFileContent(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "Whether to return checksum for the file.", required = false) @HeaderParam("ETag") boolean etag, @Parameter(description = "Path to file for which the data is to be read.", required = true) @PathParam("path") String path) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
IFSAPIImpl.IFSFileOperationResult output = null;
try {
session = getSession(req, authorization, true);
output = IFSAPIImpl.ifsGetFileContent(session, path, etag);
payload = output.fileContentInternal_.toJSON();
} catch (Exception e) {
return handleException("getFileContent", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
if (output.etagHeader_ != null)
return Response.status(Response.Status.OK).header("ETag", output.etagHeader_).entity(payload).build();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/ifs/{path}")
@PUT
@Consumes({"application/json", "text/plain"})
@APIResponses({@APIResponse(responseCode = "204", description = "Successful request, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "412", description = "Precondition failed."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Write a string to a file.", description = "Write a string to a file. The file must exist and its contents will be replaced by the string specified in the request.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response ifsPutFileContent(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "If the If-Match HTTP header is passed, RSE API will check to see if the Etag (MD5 hash of the object content) matches the provided Etag value. If this value matches, the operation will proceed. If the match fails, the system will return a 412 (Precondition Failed) error.", required = false) @HeaderParam("If-Match") String matchEtag, @Parameter(description = "Path to file in which the data will be written.", required = true) @PathParam("path") String path, @RequestBody(description = "File content.", required = true, content = {@Content(schema = @Schema(implementation = IFSAPIImpl.RSEAPI_FileContent.class), mediaType = "application/json", example = "{\n \"content\": \"some data that will be written to file.\"\n}"), @Content(mediaType = "text/plain", example = "some data that will be written to file.\n")}) IFSAPIImpl.RSEAPI_FileContent fileContentInfo) {
CommonUtil._serviceContext.set(req);
Session session = null;
String newChecksum = null;
try {
session = getSession(req, authorization, true);
newChecksum = IFSAPIImpl.ifsPutFileContent(session, path, matchEtag, fileContentInfo.content);
} catch (Exception e) {
return handleException("putFileContent", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
if (matchEtag != null && newChecksum != null)
return Response.status(Response.Status.NO_CONTENT).header("ETag", newChecksum).build();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/ifs/{path}/info")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"path\": \"/qsys.lib/user1.lib\",\n \"description\": \"user1's lib\",\n \"isDir\": true,\n \"subType\": \"PROD\",\n \"owner\": \"USER1\",\n \"ccsid\": 37,\n \"lastModified\": 1680559633,\n \"size\": 401408,\n \"recordLength\": -1,\n \"numberOfRecords\": -1\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Returns information about the object referenced by the path.", description = "Returns information about the object referenced by the path. The information returned includes the name, whether object is a directory, the description, the object subtype, CCSID, size, last modified timestamp, and object subtype.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response ifsGetFileInfo(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "Path to object for which information is to be returned.", required = true) @PathParam("path") String path) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = IFSAPIImpl.ifsGetFileInfo(session, path);
} catch (Exception e) {
return handleException("getFileInfo", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,334 @@
package dev.alexzaw.rest4i.api.jaxrs;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.pdf.PDFManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
/**
* REST service for PDF operations including conversion, merge, rotate, and unprotect.
* Follows IBM RSE API patterns and delegates to the integration service implementation.
*
* @author alexzaw
*/
@Path("/")
@Tags({@Tag(name = "PDF Operations Services", description = "PDF operations including HTML to PDF conversion, merge, rotate, and unprotect functionality with Base64 encoding support.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PDFOperationsRESTService extends RESTService {
public static final String COPYRIGHT = "Licensed Materials - Property of IBM, 5770-SS1 (C) Copyright IBM Corp. 2021, 2021. All rights reserved. US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.";
// Use the PDF manager directly
private final PDFManager pdfManager = new PDFManager();
/**
* HTML to PDF Conversion Request DTO - matches scalar.json API contract
*/
public static class ConvertRequest {
public String html;
public String size = "A4";
public String orientation = "portrait";
}
/**
* PDF Merge Request DTO - mirrors the delegate's structure
*/
public static class MergeRequest {
public List<String> pdfFiles;
}
/**
* PDF Rotation Request DTO - mirrors the delegate's structure
*/
public static class RotateRequest {
public String pdfFile;
public int rotation = 90;
}
/**
* PDF Unprotect Request DTO - mirrors the delegate's structure
*/
public static class UnprotectRequest {
public String pdfFile;
}
@Path("/v1/pdf/convert")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({"application/pdf"})
@APIResponses({
@APIResponse(responseCode = "200", description = "HTML converted to PDF successfully.",
content = {@Content(mediaType = "application/pdf")}),
@APIResponse(responseCode = "400", description = "Bad request - htmlContent is required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Convert HTML to PDF",
description = "Converts HTML content to PDF format with customizable page size and orientation.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response convertHtmlToPdf(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@RequestBody(description = "HTML conversion request.", required = true,
content = {@Content(schema = @Schema(implementation = ConvertRequest.class))})
ConvertRequest convertRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (convertRequest == null || convertRequest.html == null || convertRequest.html.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"HTML content is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Use default values if not provided
String pageSize = (convertRequest.size != null) ? convertRequest.size : "A4";
String orientation = (convertRequest.orientation != null) ? convertRequest.orientation : "portrait";
// Convert HTML to PDF
ByteArrayOutputStream pdfStream = pdfManager.convertHtmlToPdf(
convertRequest.html, pageSize, orientation);
return Response.ok(pdfStream.toByteArray())
.type("application/pdf")
.header("Content-Disposition", "attachment; filename=\"converted.pdf\"")
.build();
} catch (Exception e) {
return handleException("convertHtmlToPdf", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/pdf/merge")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({"application/pdf"})
@APIResponses({
@APIResponse(responseCode = "200", description = "PDFs merged successfully.",
content = {@Content(mediaType = "application/pdf")}),
@APIResponse(responseCode = "400", description = "Bad request - at least one PDF file is required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Merge PDF files",
description = "Merges multiple PDF files into a single PDF document.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response mergePdfs(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@RequestBody(description = "PDF merge request with Base64 encoded PDF files.", required = true,
content = {@Content(schema = @Schema(implementation = MergeRequest.class))})
MergeRequest mergeRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (mergeRequest == null || mergeRequest.pdfFiles == null || mergeRequest.pdfFiles.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"At least one PDF file is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Convert Base64 encoded PDF files to input streams
List<InputStream> pdfStreams = new ArrayList<>();
try {
for (String base64Pdf : mergeRequest.pdfFiles) {
if (base64Pdf == null || base64Pdf.trim().isEmpty()) {
continue;
}
byte[] pdfBytes = Base64.getDecoder().decode(base64Pdf);
pdfStreams.add(new ByteArrayInputStream(pdfBytes));
}
if (pdfStreams.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"No valid PDF files provided\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Merge PDFs
ByteArrayOutputStream mergedStream = pdfManager.mergePdfs(pdfStreams);
return Response.ok(mergedStream.toByteArray())
.type("application/pdf")
.header("Content-Disposition", "attachment; filename=\"merged.pdf\"")
.build();
} finally {
// Close input streams
for (InputStream stream : pdfStreams) {
try {
stream.close();
} catch (IOException e) {
// Log but don't fail the response
}
}
}
} catch (Exception e) {
return handleException("mergePdfs", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/pdf/rotate")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({"application/pdf"})
@APIResponses({
@APIResponse(responseCode = "200", description = "PDF rotated successfully.",
content = {@Content(mediaType = "application/pdf")}),
@APIResponse(responseCode = "400", description = "Bad request - PDF file is required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Rotate PDF pages",
description = "Rotates all pages in a PDF document by the specified angle.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response rotatePdf(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@RequestBody(description = "PDF rotation request with Base64 encoded PDF file.", required = true,
content = {@Content(schema = @Schema(implementation = RotateRequest.class))})
RotateRequest rotateRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (rotateRequest == null || rotateRequest.pdfFile == null || rotateRequest.pdfFile.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"PDF file is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Decode Base64 PDF file
byte[] pdfBytes = Base64.getDecoder().decode(rotateRequest.pdfFile);
// Use default rotation if not provided
int rotation = (rotateRequest.rotation > 0) ? rotateRequest.rotation : 90;
// Rotate PDF
try (InputStream pdfStream = new ByteArrayInputStream(pdfBytes)) {
ByteArrayOutputStream rotatedStream = pdfManager.rotatePdf(pdfStream, rotation);
return Response.ok(rotatedStream.toByteArray())
.type("application/pdf")
.header("Content-Disposition", "attachment; filename=\"rotated.pdf\"")
.build();
}
} catch (Exception e) {
return handleException("rotatePdf", e);
} finally {
releaseSession(session);
}
}
@Path("/v1/pdf/unprotect")
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({"application/pdf"})
@APIResponses({
@APIResponse(responseCode = "200", description = "PDF unprotected successfully.",
content = {@Content(mediaType = "application/pdf")}),
@APIResponse(responseCode = "400", description = "Bad request - PDF file is required."),
@APIResponse(responseCode = "401", description = "Unauthorized request."),
@APIResponse(responseCode = "500", description = "Internal server error.")
})
@Operation(summary = "Remove PDF protection",
description = "Removes protection from a PDF document by converting it to images and back to PDF.")
@SecurityRequirements({
@SecurityRequirement(name = "bearerHttpAuthentication"),
@SecurityRequirement(name = "basicHttpAuthentication")
})
public Response unprotectPdf(@Context HttpServletRequest req,
@Parameter(description = "The authorization HTTP header.")
@HeaderParam("Authorization") String authorization,
@RequestBody(description = "PDF unprotection request with Base64 encoded PDF file.", required = true,
content = {@Content(schema = @Schema(implementation = UnprotectRequest.class))})
UnprotectRequest unprotectRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
if (unprotectRequest == null || unprotectRequest.pdfFile == null || unprotectRequest.pdfFile.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"error\": \"PDF file is required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
// Decode Base64 PDF file
byte[] pdfBytes = Base64.getDecoder().decode(unprotectRequest.pdfFile);
// Unprotect PDF
try (InputStream pdfStream = new ByteArrayInputStream(pdfBytes)) {
ByteArrayOutputStream unprotectedStream = pdfManager.unprotectPdf(pdfStream);
return Response.ok(unprotectedStream.toByteArray())
.type("application/pdf")
.header("Content-Disposition", "attachment; filename=\"unprotected.pdf\"")
.build();
}
} catch (Exception e) {
return handleException("unprotectPdf", e);
} finally {
releaseSession(session);
}
}
}

View File

@@ -0,0 +1,56 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.Consumes;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.QSYSAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "QSYS Services", description = "QSYS Services provide APIs for accessing QSYS objects. ")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class QSYSRESTService extends RESTService {
@Path("/v1/qsys/search/{objectName}")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"objects\": [\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/AXISLIBS.FILE\",\n \"description\": \"\",\n \"isDir\": false,\n \"subType\": \"SAVF\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/DEALER.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-DTA\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/FLGHT400.FILE\",\n \"description\": \"\",\n \"isDir\": false,\n \"subType\": \"SAVF\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/IWSDB1.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-DTA\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QCLSRC.FILE\",\n \"description\": \"\\\"test\\\"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n },\n {\n \"path\": \"/QSYS.LIB/USER1.LIB/QCSRC.FILE\",\n \"description\": \"\",\n \"isDir\": true,\n \"subType\": \"PF-SRC\"\n }\n ]\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Returns a list of QSYS.LIB objects that match the search criteria.", description = "Returns a list of QSYS.LIB objects that match the search criteria. The filter is the object name, and may be a value of \\*ALL, in which case all objects that match the search criteria is returned, or a generic name. A generic name is a character string that contains one or more characters followed by an asterisk (\\*). If a generic name is specified, all objects that have names with the same prefix as the generic name are returned.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response qsysGetQsysObjects(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "The library or set of libraries that are searched for objects. Valid values are a specific name, or one of the following special values: \n- \\*ALL - All libraries are searched. \n- \\*ALLUSR - All user libraries are searched. \n- \\*CURLIB - The current library is searched. \n- \\*LIBL - The library list is searched. \n- \\*USRLIBL - The user portion of the library list is searched. ", required = false) @DefaultValue("*USRLIBL") @QueryParam("objectLibrary") String objectLibrary, @Parameter(description = "The type of object to search. Valid values include: \\*FILE, \\*PGM, \\*LIB, etc., or \\*ALL. ", required = false) @DefaultValue("*ALL") @QueryParam("objectType") String objectType, @Parameter(description = "Object subtype. For example, PF-SRC, PF-DTA, SAVF, etc., or \\*ALL. Note that not all objects have subtypes.", required = false) @DefaultValue("*ALL") @QueryParam("objectSubtype") String objectSubtype, @Parameter(description = "The member name to match for objects of type PF-SRC or PF-DTA. Valid values are a specific name, or an extended generic name where the asterisk (\\*) may be placed in any part of the name, or \\*ALL. Note that members are not searched for unless the object subtype that starts with the prefix PF. For example, PF, or PF-SRC.", required = false) @DefaultValue("") @QueryParam("memberName") String memberName, @Parameter(description = "The member type to match for members of objects of type PF-SRC or PF-DTA. Valid values are a specific name, such as C, CBLLE, RPGLE, SQLRPGLE, etc., or \\*ALL. ", required = false) @DefaultValue("*ALL") @QueryParam("memberType") String memberType, @Parameter(description = "The object name. Valid values are a specific name, a generic name (for example, AM\\*), or one of the following special values: \n- \\*ALL - All object names are searched. \n- \\*ALLUSR - All objects that are libraries in QSYS or the library list are searched. The object library must either be \\*ALL, \\*LIBL or QSYS. The object type must be \\*LIB. A list of user libraries is returned. \n", required = true) @PathParam("objectName") String objectName) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
QSYSAPIImpl.QSYSOperationResult output = QSYSAPIImpl.qsysGetQsysObjects(session, objectName, objectLibrary, objectType, objectSubtype, memberName, memberType);
payload = output.objectList_.toJSON();
} catch (Exception e) {
return handleException("getQsysObjects", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,124 @@
package dev.alexzaw.rest4i.api.jaxrs;
import dev.alexzaw.rest4i.api.SessionAPIImpl;
import dev.alexzaw.rest4i.exception.RestWAForbiddenException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedException;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.session.SessionRepository;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import java.io.IOException;
import java.util.Base64;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
public abstract class RESTService {
protected static final String VERSION_V1 = "/v1";
protected static final String VERSION_V1_ADMIN = "/v1/admin";
protected static final String VERSION_V1_CL = "/v1/cl";
protected static final String VERSION_V1_IFS = "/v1/ifs";
protected static final String VERSION_V1_QSYS = "/v1/qsys";
protected static final String VERSION_V1_INFO = "/v1/info";
protected static final String VERSION_V1_SESSION = "/v1/session";
protected static final String VERSION_V1_SQL = "/v1/sql";
protected static final String VERSION_V1_SECURITY = "/v1/security";
protected static final String VERSION_V1_DATABASE = "/v1/database";
protected static final String VERSION_V1_FILES = "/v1/files";
protected static final String VERSION_V1_PDF = "/v1/pdf";
protected static Response handleException(String classSignature, Exception e, int httpStatus, String s) {
Response errorResponse = null;
if (e instanceof WebApplicationException) {
errorResponse = ((WebApplicationException)e).getResponse();
} else {
String errorMsg = e.getMessage();
AsyncLogger.traceException(classSignature, e, s, new Object[0]);
if (s != null && !s.trim().isEmpty())
errorMsg = s;
errorResponse = (new RestWAInternalServerErrorException(errorMsg, new Object[0])).getResponse();
CommonUtil._serviceContext.remove();
}
return errorResponse;
}
protected static Response handleException(String classSignature, Exception e) {
return handleException(classSignature, e, 500, "");
}
protected static Session getSession(HttpServletRequest req, String authorization, boolean allowBasicAuth) {
checkSecureConnection(req);
String token = null;
SessionAPIImpl.RSEAPI_LoginCredentials creds = null;
Session session = null;
if (authorization != null)
if (authorization.startsWith("Bearer")) {
token = authorization.replaceFirst("Bearer ", "").trim();
session = SessionAPIImpl.getSession(token);
} else if (allowBasicAuth && authorization.startsWith("Basic")) {
String encodedUserPassword = authorization.replaceFirst("Basic ", "").trim();
String usernameAndPassword = null;
try {
byte[] decodedBytes = Base64.getDecoder().decode(encodedUserPassword);
usernameAndPassword = new String(decodedBytes, "UTF-8");
} catch (IOException e) {
AsyncLogger.traceDebug("getSession", "Base64 decoder error: " + e.getMessage(), new Object[0]);
}
int seperatorIndex;
if (usernameAndPassword != null && (seperatorIndex = usernameAndPassword.indexOf(':')) != -1) {
creds = new SessionAPIImpl.RSEAPI_LoginCredentials();
creds.userid = usernameAndPassword.substring(0, seperatorIndex);
creds.password = usernameAndPassword.substring(seperatorIndex + 1);
session = SessionAPIImpl.sessionLogin(creds);
}
}
if (session == null)
throw new RestWAUnauthorizedException("User ID or password is not set or not valid.", new Object[0]);
session.lock();
try {
session.setLastUsed();
if (creds != null)
session.setSingleUse(true);
} finally {
session.unlock();
}
return session;
}
protected static void releaseSession(Session session) {
if (session != null) {
if (session.getSingleUse())
SessionAPIImpl.sessionLogout(session);
session.unlock();
}
}
protected static void checkSecureConnection(HttpServletRequest req) {
if (GlobalProperties.useSecureConnections() && !req.isSecure())
throw new RestWAForbiddenException("Resource must be accessed with a secure connection.", new Object[0]);
}
@PostConstruct
private void restService_PostConstruct() {}
@PreDestroy
private void restService_PreDestroy() {
SessionRepository.destroy();
}
}

View File

@@ -0,0 +1,68 @@
package dev.alexzaw.rest4i.api.jaxrs;
import com.ibm.as400.access.IFSFileInputStream;
import dev.alexzaw.rest4i.api.APIImpl;
import dev.alexzaw.rest4i.api.IFSAPIImpl;
import dev.alexzaw.rest4i.api.SQLAPIImpl;
import dev.alexzaw.rest4i.util.AsyncLogger;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
@Provider
public class RSEMessageBodyWriter implements MessageBodyWriter<APIImpl.RSEResult> {
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return !(type != APIImpl.RSEResult.class &&
type != IFSAPIImpl.IFSFileOperationResult.class &&
type != SQLAPIImpl.SQLOperationResult.class);
}
public long getSize(APIImpl.RSEResult rseResult, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1L;
}
public void writeTo(APIImpl.RSEResult rseResult, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException, WebApplicationException {
Writer writer = null;
IFSFileInputStream iStream = null;
int bytesRead = 0;
byte[] buffer = null;
try {
if (rseResult instanceof IFSAPIImpl.IFSFileOperationResult) {
IFSAPIImpl.IFSFileOperationResult filePayload = (IFSAPIImpl.IFSFileOperationResult)rseResult;
iStream = filePayload.iStream_;
buffer = new byte[131072];
if (filePayload.fileContentInternal_ != null && filePayload.fileContentInternal_.content != null) {
AsyncLogger.traceDebug("MBWwriteTo", "Writing IFS string data", new Object[0]);
writer = new PrintWriter(out);
writer.write(filePayload.fileContentInternal_.content);
writer.flush();
} else if (iStream != null) {
AsyncLogger.traceDebug("MBWwriteTo", "Writing IFS binary data", new Object[0]);
while ((bytesRead = iStream.read(buffer)) > 0)
out.write(buffer, 0, bytesRead);
out.flush();
}
} else if (rseResult instanceof SQLAPIImpl.SQLOperationResult) {
AsyncLogger.traceDebug("MBWwriteTo", "Writing SQL string data", new Object[0]);
SQLAPIImpl.SQLOperationResult sqlResults = (SQLAPIImpl.SQLOperationResult)rseResult;
writer = new PrintWriter(out);
writer.write(sqlResults.toJSON());
writer.flush();
}
} finally {
try {
if (iStream != null)
iStream.close();
} catch (Exception exception) {}
}
}
}

View File

@@ -0,0 +1,39 @@
package dev.alexzaw.rest4i.api.jaxrs;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import org.eclipse.microprofile.openapi.annotations.Components;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
@ApplicationPath("/api")
@OpenAPIDefinition(info = @Info(title = "Remote System Explorer API (RSE API) Documentation", description = "IBM Remote System Explorer API is a collection of REST APIs that allow a client to work with various components on an IBM i host system, including QSYS objects, IFS files, CL Commands, database operations, file operations, and PDF processing. Enhanced with integration services by alexzaw.", version = "1.0.8"), components = @Components(securitySchemes = {@SecurityScheme(securitySchemeName = "bearerHttpAuthentication", description = "Bearer token authentication.", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "Bearer [token]"), @SecurityScheme(securitySchemeName = "basicHttpAuthentication", description = "Basic authentication.", type = SecuritySchemeType.HTTP, scheme = "basic")}))
public class RSEResourceConfig extends Application {
private final Set<Object> singletons = new HashSet<Object>();
public RSEResourceConfig() {
singletons.add(new AdminRESTService());
singletons.add(new SessionRESTService());
singletons.add(new CLCommandRESTService());
singletons.add(new IFSRESTService());
singletons.add(new QSYSRESTService());
singletons.add(new ServerInfoRESTService());
singletons.add(new SQLRESTService());
singletons.add(new SecurityRESTService());
singletons.add(new DatabaseRESTService());
singletons.add(new FileOperationsRESTService());
singletons.add(new PDFOperationsRESTService());
singletons.add(new HealthRESTService());
singletons.add(new RSEMessageBodyWriter());
}
@Override
public Set<Object> getSingletons() {
return singletons;
}
}

View File

@@ -0,0 +1,57 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.SQLAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "SQL Services", description = "SQL Services provide APIs associated with performing SQL operations. ")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SQLRESTService extends RESTService {
@Path("/v1/sql")
@PUT
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "SQL statements(s) issued.", content = {@Content(mediaType = "application/json", example = "{\n \"resultSet\": [\n {\n \"CUSNUM\": 938472,\n \"LSTNAM\": \"Henning\",\n \"INIT\": \"G K\",\n \"STREET\": \"4859 Elm Ave\",\n \"CITY\": \"Dallas\",\n \"STATE\": \"TX\",\n \"ZIPCOD\": 75217,\n \"CDTLMT\": 5000,\n \"CHGCOD\": 3,\n \"BALDUE\": 37,\n \"CDTDUE\": 0\n },\n {\n \"CUSNUM\": 839283,\n \"LSTNAM\": \"Jones\",\n \"INIT\": \"B D\",\n \"STREET\": \"21B NW 135 St\",\n \"CITY\": \"Clay\",\n \"STATE\": \"NY\",\n \"ZIPCOD\": 13041,\n \"CDTLMT\": 400,\n \"CHGCOD\": 1,\n \"BALDUE\": 100,\n \"CDTDUE\": 0\n }\n ]\n}")}), @APIResponse(responseCode = "204", description = "SQL statement(s) issued successfully, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Run a SQL statement on the server.", description = "Run a SQL statement on the server. If SQL statement fails, any messages relating to the error is returned. \n\nSQL state information is returned only if errors are detected. You have the option of indicating whether the state information is returned on all responses. By default, if a SQL statement is run successfully and there is no result set to return, no data is returned in the response.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response runSQLStatements(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The SQL statement to be run on the server.", required = true, content = {@Content(schema = @Schema(implementation = SQLAPIImpl.RSEAPI_SQLRequest.class), mediaType = "application/json", example = "{\n \"alwaysReturnSQLStateInformation\": false,\n \"treatWarningsAsErrors\": false,\n \"sqlStatement\": \"select * from QIWS.QCUSTCDT\"\n}")}) SQLAPIImpl.RSEAPI_SQLRequest sqlRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
SQLAPIImpl.SQLOperationResult output = null;
try {
session = getSession(req, authorization, true);
output = SQLAPIImpl.runSQLStatement(session, sqlRequest);
} catch (Exception e) {
return handleException("runSQLStatements", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
if (!output.processResults())
return Response.status(Response.Status.NO_CONTENT).build();
return Response.status(Response.Status.OK).entity(output).build();
}
}

View File

@@ -0,0 +1,323 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.SecurityAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "Security Services", description = "Security Services provide APIs relating to security, such as the mangement of digital certificates and the retrieval of TLS system information. \n\nAll the digital certificate management APIs require the Digital Certificate Manager, option 34 of the IBM i licensed program (5761-SS1), be installed. In addition, the authenticated user must have the *ALLOBJ and *SECADM special authorities.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SecurityRESTService extends RESTService {
@Path("/v1/security/dcm/certstore/changepassword")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Change digital certificate store password.", description = "Change digital certificate store password. For system certificate stores of type CMS, if the current password is omitted, the system stash file will be used.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMChangeCertificateStorePassword(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to change a digital certificate store password.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertStoreChangePasswordRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": null,\n \"certStorePasswordNew\": \"myNewPassw0rd\",\n \"daysToExpiration\": 0\n}")}) SecurityAPIImpl.RSEAPI_DCMCertStoreChangePasswordRequest dcmResetCertStorePasswordRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMChangeCertificateStorePassword(session, dcmResetCertStorePasswordRequest);
} catch (Exception e) {
return handleException("securityDCMResetCertificateStorePassword", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/cert/list")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"certificates\": [\n {\n \"certAlias\": \"GLOBAL-MULTISAN\",\n \"commonName\": \"myserver.ibm.com\",\n \"type\": \"SERVER_CLIENT\",\n \"daysBeforeExpiration\": 1831,\n \"keyAlgorithm\": \"ECDSA\",\n \"keySize\": 256,\n \"keyStorageLocation\": \"SOFTWARE\",\n \"signatureAlgorithm\": \"ECDSA_SHA256\"\n },\n {\n \"certAlias\": \"LOCAL_CERTIFICATE_AUTHORITY_106F947K(5)\",\n \"commonName\": \"Local CA for myserver on July 17\",\n \"type\": \"CA\",\n \"daysBeforeExpiration\": 6807,\n \"keyAlgorithm\": \"ECDSA\",\n \"keySize\": 256,\n \"signatureAlgorithm\": \"ECDSA_SHA256\"\n }\n ]\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Retrieve a list of certificates in a certificate store.", description = "Retrieve a list of certificates in a certificate store. You can filter what is returned by alias, certificate type, days until expiration, and whether to include expired certificates. When filtering by alias, you can specify a generic alias for alias. A generic alias is a character string that contains one or more characters followed by an asterisk (\\*). If a generic alias is specified, all certificates that have an alias with the same prefix as the generic alias are returned. Note that if the daysUntilExpiration filter is specified, CSR certificates will not be returned in the response since CSR certificates do not expire.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMListCertificates(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to list certificates in a certificate store.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertListRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": \"passw0rd\",\n \"filters\": { \n \"certAlias\": \"*\",\n \"certTypes\": [\"SERVER_CLIENT\", \"CA\", \"CSR\"],\n \"daysUntilExpiration\": 5000,\n \"excludeExpired\": false\n }}")}) SecurityAPIImpl.RSEAPI_DCMCertListRequest dcmCertListRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityDCMListCertificates(session, dcmCertListRequest);
} catch (Exception e) {
return handleException("securityDCMListCertificates", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/security/dcm/cert/export")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n\"certFormat\": \"PKCS12\",\n \"certData\": \"BASE64-BLOB\"\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Export a digital certificate.", description = "Export a digital certificate. Only server/client and CA certificates can be exported. Certificates can be exported in the DER, PEM, or PKCS12 formats. A server/client or CA certificate that is to include the private key must be exported in the PKCS12 format. CA certificates without private keys cannot be exported in the PKCS12 format. \n\nWhen exporting certificates in the PKCS12 format, a password for the exported certificate must be specified. \n\nThe certificate data in the response will be encoded in Base64, even for certificate data returned in the PEM format. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMExportCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to export a digital certificate.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertExportRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": \"passw0rd\",\n \"certFormat\": \"PKCS12\",\n \"certAlias\": \"mylabel\",\n \"certDataPassword\": \"myPassw0rd\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertExportRequest dcmCertExportRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityDCMExportCertificate(session, dcmCertExportRequest);
} catch (Exception e) {
return handleException("securityDCMExportCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/security/dcm/cert/import")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Import a digital certificate.", description = "Import a digital certificate. Only server/client or CA certificates can be imported. A certificate can be imported in the following formats: PKCS12, DER, or PEM. If the certificate to be imported includes a private key, then the PKCS12 format must be used. \n\nIf importing a CA certificate and the certificate includes a private key, the PKCS12 format must be used and the certificate type must be set to SERVER_CLIENT. When importing CA certificate that do not contain a private key, the PEM or DER format must be used. \n\nThe certificate data in the request must be encoded in Base64, which includes certificate data in the PEM format. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMImportCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to import a digital certificate. ", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertImportRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": \"passw0rd\",\n \"certType\": \"SERVER_CLIENT\",\n \"certFormat\": \"PKCS12\",\n \"certAlias\": \"mylabel\",\n \"certData\": \"BASE64-BLOB\",\n \"certDataPassword\": \"myPassw0rd\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertImportRequest dcmCertImportRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMImportCertificate(session, dcmCertImportRequest);
} catch (Exception e) {
return handleException("securityDCMImportCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/cert/info")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"alias\": \"UNIQUE-SAN\",\n \"trusted\": true, \"subject\": \"C=US,SP=Minnesota,O=IBM,CN=UniQue\", \"issuer\": \"C=US,SP=Any,O=IBM Web Administration for i,CN=mysystem_CERTIFICATE_AUTHORITY\", \"keyAlgorithm\": \"ECDSA\", \"keySize\": 256, \"hasPrivateKey\": true, \"signatureAlgorithm\": \"RSA_SHA256\", \"keyStorageLocation\": \"SOFTWARE\", \"serialNumber\": \"6526CFBC0601B8\", \"effectiveDate\": \"10/10/23 11:39:24\", \"expirationDate\": \"10/10/24 11:39:24\", \"subjectAlternativeNames\": [\n \"booboo.ibm.com\",\n \"9.9.9.9\"\n ],\n \"keyUsages\": [\n \"DIGITAL_SIGNATURE\",\n \"NONREPUDIATION\",\n \"KEY_ENCIPHERMENT\",\n \"KEY_AGREEMENT\"\n ]\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get detailed certificate information.", description = "Get detailed certificate information, such as subject, issuer, subject alternative names, and serial number.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMGetCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to get detailed information about a digital certificate.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": \"passw0rd\",\n \"certAlias\": \"mylabel\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertRequest dcmCertInfoRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityDCMGetCertificate(session, dcmCertInfoRequest);
} catch (Exception e) {
return handleException("getCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/security/dcm/cert/delete")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Delete a digital certificate.", description = "Delete a digital certificate.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMDeleteCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to delete a digital certificate.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertRequest.class), mediaType = "application/json", example = "{\n \"certStoreType\": \"CMS\",\n \"certStorePath\": \"*SYSTEM\",\n \"certStorePassword\": \"passw0rd\",\n \"certAlias\": \"mylabel\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertRequest dcmCertDeleteRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMDeleteCertificate(session, dcmCertDeleteRequest);
} catch (Exception e) {
return handleException("deleteCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/appdef/list")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"appDefinitions\": [\n {\n \"appDefinitionID\": \"QIBM_OS400_QRW_SVR_DDM_DRDA\",\n \"appType\": \"SERVER\",\n \"description\": \"IBM i DDM/DRDA Server - TCP/IP\",\n \"certAliases\": [ ]\n }\n ]\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "List application definitions.", description = "Retrieve a list of application definitions. You can filter what is returned by application definition ID and application type. When filtering by application definition ID, you can specify a generic ID. A generic ID is a character string that contains one or more characters followed by an asterisk (\\*). If a generic ID is specified, all application definition IDs that have an ID with the same prefix as the generic ID are returned. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMAppidList(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "Application definition ID filter. ", required = false) @QueryParam("idFilter") String idFilter, @Parameter(description = "Application type filter. Possible values: SERVER, CLIENT, SERVER_CLIENT, OBJECT_SIGNING. If not specifed, all server and client application definitions are returned.", required = false) @QueryParam("typeFilter") String typeFilter) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityDCMAppidList(session, idFilter, typeFilter);
} catch (Exception e) {
return handleException("securityDCMAssignAppidCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/security/dcm/appdef/associate")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Associate digital certificates to an application definition.", description = "Associate digital certificates to an application definition. A maximum of 4 certificates can be specified. \n\nOn successful completion, the specified certificates will replace any pre-existing certificates associated with the application definition.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMAppDefAssociateCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to assign digital certificates to an application definition.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertAppDefAssociateRequest.class), mediaType = "application/json", example = "{\n \"appDefinitionID\": \"myappdef\",\n \"certAliases\": [\"mylabel1\",\"mylabel2\"]\n}")}) SecurityAPIImpl.RSEAPI_DCMCertAppDefAssociateRequest dcmCertAppDefIDRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMAppDefAssociateCertificate(session, dcmCertAppDefIDRequest);
} catch (Exception e) {
return handleException("securityDCMAssignAppidCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/appdef/disassociate")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Disassociate digital certificates from an application definition.", description = "Disassociate digital certificates from an application definition. All certificates associated with the application definition will be disassociated.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMAppDefDisassociateCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to disassociate digital certificates from an application definition.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertAppDefDisassociateRequest.class), mediaType = "application/json", example = "{\n \"appDefinitionID\": \"myappdef\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertAppDefDisassociateRequest dcmCertAppDefIDRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMAppDefDisassociateCertificate(session, dcmCertAppDefIDRequest);
} catch (Exception e) {
return handleException("securityDCMAppDefDisassociateCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/appdef/trust")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Add certificate authority (CA) digital certificate to the application definition CA trust list.", description = "Add a CA certificate to the application definition CA trust list. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMAppDefTrustCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to add an CA to the application definition CA trust list.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertAppDefTrustRequest.class), mediaType = "application/json", example = "{\n \"appDefinitionID\": \"myappdef\",\n \"certAlias\": \"mylabel1\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertAppDefTrustRequest dcmCertAppDefTrustRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMAppDefTrustCertificate(session, dcmCertAppDefTrustRequest);
} catch (Exception e) {
return handleException("securityDCMAppDefTrustCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/dcm/appdef/untrust")
@POST
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Request successful, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Remove a certificate authority (CA) digital certificate from the application definition CA trust list.", description = "Remove a certificate authority (CA) digital certificate from the application definition CA trust list. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityDCMAppDefUntrustCertificate(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The API properties required to remove a CA digital certificate from an application definition CA trust list.", required = true, content = {@Content(schema = @Schema(implementation = SecurityAPIImpl.RSEAPI_DCMCertAppDefTrustRequest.class), mediaType = "application/json", example = "{\n \"appDefinitionID\": \"myappdef\",\n \"certAlias\": \"mylabel1\"\n}")}) SecurityAPIImpl.RSEAPI_DCMCertAppDefTrustRequest dcmCertAppDefTrustRequest) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, true);
SecurityAPIImpl.securityDCMAppDefUntrustCertificate(session, dcmCertAppDefTrustRequest);
} catch (Exception e) {
return handleException("securityDCMAppDefUntrustCertificate", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/security/tls")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"supportedProtocols\": [\n \"TLSv1.3\",\n \"TLSv1.2\"\n ],\n \"eligibleDefaultProtocols\": [\n \"TLSv1.3\",\n \"TLSv1.2\"\n ], \"defaultProtocols\": [\n \"TLSv1.3\",\n \"TLSv1.2\"\n ], \"supportedCipherSuites\": [\n \"AES_128_GCM_SHA256\",\n \"AES_256_GCM_SHA384\",\n \"CHACHA20_POLY1305_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n \"ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\"\n ], \"eligibleDefaultCipherSuites\": [\n \"AES_128_GCM_SHA256\",\n \"AES_256_GCM_SHA384\",\n \"CHACHA20_POLY1305_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n \"ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\"\n ], \"defaultCipherSuites\": [\n \"AES_128_GCM_SHA256\",\n \"AES_256_GCM_SHA384\",\n \"CHACHA20_POLY1305_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n \"ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n \"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n \"ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\"\n ], \"supportedSignatureAlgorithms\": [\n \"ECDSA_SHA512\",\n \"ECDSA_SHA384\",\n \"ECDSA_SHA256\",\n \"RSA_PSS_SHA512\",\n \"RSA_PSS_SHA384\",\n \"RSA_PSS_SHA256\",\n \"RSA_SHA512\",\n \"RSA_SHA384\",\n \"RSA_SHA256\"\n ], \"defaultSignatureAlgorithms\": [\n \"ECDSA_SHA512\",\n \"ECDSA_SHA384\",\n \"ECDSA_SHA256\",\n \"RSA_PSS_SHA512\",\n \"RSA_PSS_SHA384\",\n \"RSA_PSS_SHA256\",\n \"RSA_SHA512\",\n \"RSA_SHA384\",\n \"RSA_SHA256\"\n ], \"supportedSignatureAlgorithmCertificates\": [\n \"ECDSA_SHA512\",\n \"ECDSA_SHA384\",\n \"ECDSA_SHA256\",\n \"ECDSA_SHA224\",\n \"ECDSA_SHA1\",\n \"RSA_PSS_SHA512\",\n \"RSA_PSS_SHA384\",\n \"RSA_PSS_SHA256\",\n \"RSA_SHA512\",\n \"RSA_SHA384\",\n \"RSA_SHA256\",\n \"RSA_SHA224\",\n \"RSA_SHA1\",\n \"RSA_MD5\"\n ], \"defaultSignatureAlgorithmCertificates\": [\n \"ECDSA_SHA512\",\n \"ECDSA_SHA384\",\n \"ECDSA_SHA256\",\n \"RSA_PSS_SHA512\",\n \"RSA_PSS_SHA384\",\n \"RSA_PSS_SHA256\",\n \"RSA_SHA512\",\n \"RSA_SHA384\",\n \"RSA_SHA256\"\n ], \"supportedNamedCurves\": [\n \"x25519\",\n \"x448\",\n \"Secp256r1\",\n \"Secp384r1\",\n \"Secp521r1\"\n ], \"defaultNamedCurves\": [\n \"Secp256r1\",\n \"Secp384r1\",\n \"x25519\",\n \"Secp521r1\",\n \"x448\"\n ], \"defaultMinimumRSAKeySize\": 0, \"handshakeConnectionCounts\": false, \"secureSessionCaching\": true, \"auditSecureTelnetHandshakes\": false}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Retrieve system transport layer security (TLS) attributes.", description = "The API retrieves TLS attributes for the system. The system level settings are based on TLS System Values and System Service Tools (SST) Advanced Analysis command TLSCONFIG that allows viewing or altering of system-wide system TLS default properties.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityTLSGetAttributes(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityTLSGetAttributes(session);
} catch (Exception e) {
return handleException("listCertificates", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/security/tls/stats")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"protocolCounters\": {\n \"TLSv13\": 5,\n \"TLSv12\": 10,\n \"TLSv11\": 0,\n \"TLSv10\": 0,\n \"SSLv3\": 0 },\n \"cipherSuiteCounters\": {\n \"AES_128_GCM_SHA256\": 0,\n \"AES_256_GCM_SHA384\": 0,\n \"CHACHA20_POLY1305_SHA256\": 0,\n \"ECDHE_ECDSA_AES_128_GCM_SHA256\": 0,\n \"ECDHE_ECDSA_AES_256_GCM_SHA384\": 0,\n \"ECDHE_ECDSA_CHACHA20_POLY1305_SHA256\": 0,\n \"ECDHE_RSA_AES_128_GCM_SHA256\": 0,\n \"ECDHE_RSA_AES_256_GCM_SHA384\": 0,\n \"ECDHE_RSA_CHACHA20_POLY1305_SHA256\": 0,\n \"RSA_AES_128_GCM_SHA256\": 15,\n \"RSA_AES_256_GCM_SHA384\": 0,\n \"ECDHE_ECDSA_AES_128_CBC_SHA256\": 0,\n \"ECDHE_ECDSA_AES_256_CBC_SHA384\": 0,\n \"ECDHE_RSA_AES_128_CBC_SHA256\": 0,\n \"ECDHE_RSA_AES_256_CBC_SHA384\": 0,\n \"RSA_AES_128_CBC_SHA256\": 0,\n \"RSA_AES_128_CBC_SHA\": 0,\n \"RSA_AES_256_CBC_SHA256\": 0,\n \"RSA_AES_256_CBC_SHA\": 0,\n \"ECDHE_ECDSA_3DES_EDE_CBC_SHA\": 0,\n \"ECDHE_RSA_3DES_EDE_CBC_SHA\": 0,\n \"RSA_3DES_EDE_CBC_SHA\": 0,\n \"ECDHE_ECDSA_RC4_128_SHA\": 0,\n \"ECDHE_RSA_RC4_128_SHA\": 0,\n \"RSA_RC4_128_SHA\": 0,\n \"RSA_RC4_128_MD5\": 0,\n \"RSA_DES_CBC_SHA\": 0,\n \"RSA_EXPORT_RC4_40_MD5\": 0,\n \"RSA_EXPORT_RC2_CBC_40_MD5\": 0,\n \"ECDHE_ECDSA_NULL_SHA\": 0,\n \"ECDHE_RSA_NULL_SHA\": 0,\n \"RSA_NULL_SHA256\": 0,\n \"RSA_NULL_SHA\": 0,\n \"RSA_NULL_MD5\": 0\n }\n}")}), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "404", description = "The specified resource was not found."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Retrieve system transport layer security (TLS) statistics.", description = "The API retrieves TLS statistics. The information returned includes TLS handshake connection counts by protocol type and cipher suite on the system since the last reset for the system. The System Service Tools (SST) Advanced Analysis command TLSCONFIG connectionCounts option identifies the system level setting to enable handshake connection counting. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response securityTLSGetStatistics(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
payload = SecurityAPIImpl.securityTLSGetStatistics(session);
} catch (Exception e) {
return handleException("listCertificates", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,60 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.Consumes;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JSONSerializer;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "Server Information Services", description = "Server Information Services provide APIs about RSE API. ")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ServerInfoRESTService extends RESTService {
@Path("/v1/info/serverdetails")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"rseapiBasepath\": \"rseapi\",\n \"rseapiHostname\": \"UT30P44\",\n \"rseapiPort\": 2012,\n \"rseapiVersion\": \"1.0.6\"\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get information about the RSE API.", description = "Get information about the RSE API.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication"), @SecurityRequirement(name = "basicHttpAuthentication")})
public Response serverInfoGetVersionInfo(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, true);
JSONSerializer json = new JSONSerializer();
json.startObject();
json.add("rseapiBasepath", GlobalProperties.getRSEAPIBasePath());
json.add("rseapiHostname", GlobalProperties.getHostName());
json.add("rseapiPort", Integer.valueOf(CommonUtil.getServerPort()));
json.add("rseapiVersion", GlobalProperties.getRSEAPIVersion());
json.endObject();
payload = json.toString();
} catch (Exception e) {
return handleException("getVersionInfo", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
}

View File

@@ -0,0 +1,125 @@
package dev.alexzaw.rest4i.api.jaxrs;
import javax.ws.rs.core.MediaType;
import dev.alexzaw.rest4i.api.SessionAPIImpl;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.CommonUtil;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
@Path("/")
@Tags({@Tag(name = "Session Services", description = "Session Services provide APIs for authenticating a user and managing sessions that are tied to an authenticated user. The user must have a user profile on the IBM i server to be accessed. Once authenticated, a bearer token is returned and must be submitted on requests when invoking protected APIs in an HTTP authorization header.")})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SessionRESTService extends RESTService {
@Path("/v1/session")
@POST
@Consumes({"application/json"})
@APIResponses({@APIResponse(responseCode = "201", description = "Successful request, new resource created."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Authenticate with user credentials and return an embedded token.", description = "Authenticate with user credentials and return an embedded token to access different RSE APIs. On succesful authentication, a token is returned in the Authorization HTTP header. The client must send this token in the Authorization HTTP header when making requests to protected RSE APIs. For example: \n> Authorization: Bearer 4eaa14e6-ea9c-4dde-b6f7-f542b34d5309-60da75d3-3132 \n\nFor optimal performance, specify localhost to access objects located on the same system in which the RSE APIs are hosted on. If accessing objects on a remote system using the RSE APIs, the host servers on the remote system must be enabled for secure communications. \n\nAlternatively, the non-session APIs may be invoked using basic authentication. In this case, the credentials must always be sent on every request.")
public Response sessionLogin(@Context HttpServletRequest req, @RequestBody(description = "The user credentials to be authenticated.", required = true, content = {@Content(schema = @Schema(implementation = SessionAPIImpl.RSEAPI_LoginCredentials.class), mediaType = "application/json", example = "{\n \"host\": \"localhost\",\n \"userid\": \"user\",\n \"password\": \"pwd\"\n}")}) SessionAPIImpl.RSEAPI_LoginCredentials loginCredentials) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
checkSecureConnection(req);
session = SessionAPIImpl.sessionLogin(loginCredentials);
} catch (Exception e) {
return handleException("authLogin", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.CREATED).header("Authorization", "Bearer " + session.getToken()).build();
}
@Path("/v1/session")
@DELETE
@APIResponses({@APIResponse(responseCode = "204", description = "Successful request, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Logout, releasing resources tied to the session.", description = "Logout, releasing resources tied to the session. If a logout is not performed, it will be discarded after a period of idle time (default is 2 hours).")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication")})
public Response sessionLogout(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization) {
CommonUtil._serviceContext.set(req);
Session session = null;
try {
session = getSession(req, authorization, false);
SessionAPIImpl.sessionLogout(session);
} catch (Exception e) {
return handleException("authLogout", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("/v1/session")
@GET
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "200", description = "Successful request.", content = {@Content(mediaType = "application/json", example = "{\n \"sessionInfo\": {\n \"userID\": \"user1\",\n \"host\": \"localhost\",\n \"expiration\": \"2023-04-04T00:56:35Z\",\n \"creation\": \"2023-04-03T22:04:50Z\",\n \"lastUsed\": \"2023-04-03T22:56:35Z\",\n \"domain\": \"rseapi\",\n \"expired\": false\n },\n \"sessionSettings\": {\n \"libraryList\": [],\n \"clCommands\": [],\n \"envVariables\": {},\n \"sqlDefaultSchema\": null,\n \"sqlTreatWarningsAsErrors\": false,\n \"sqlProperties\": {\n \"XA loosely coupled support\": \"0\",\n \"access\": \"all\",\n \"auto commit\": \"true\",\n \"autocommit exception\": \"false\",\n \"bidi implicit reordering\": \"true\",\n \"bidi numeric ordering\": \"false\",\n \"bidi string type\": \"5\",\n \"big decimal\": \"true\",\n \"block criteria\": \"2\",\n \"block size\": \"32\",\n \"character truncation\": \"true\",\n \"concurrent access resolution\": \"2\",\n \"cursor hold\": \"true\",\n \"cursor sensitivity\": \"asensitive\",\n \"data compression\": \"true\",\n \"data truncation\": \"true\",\n \"database name\": \"\",\n \"date format\": \"iso\",\n \"date separator\": \"\",\n \"decfloat rounding mode\": \"half even\",\n \"decimal separator\": \"\",\n \"driver\": \"toolbox\",\n \"errors\": \"basic\",\n \"extended dynamic\": \"false\",\n \"extended metadata\": \"false\",\n \"full open\": \"false\",\n \"hold input locators\": \"true\",\n \"hold statements\": \"false\",\n \"ignore warnings\": \"01003,0100C,01567\",\n \"lazy close\": \"false\",\n \"libraries\": \"*LIBL\",\n \"lob threshold\": \"32768\",\n \"maximum blocked input rows\": \"32000\",\n \"maximum precision\": \"31\",\n \"maximum scale\": \"31\",\n \"metadata source\": \"1\",\n \"minimum divide scale\": \"0\",\n \"naming\": \"system\",\n \"numeric range error\": \"true\",\n \"package\": \"\",\n \"package add\": \"true\",\n \"package cache\": \"false\",\n \"package ccsid\": \"13488\",\n \"package criteria\": \"default\",\n \"package error\": \"warning\",\n \"package library\": \"QGPL\",\n \"portNumber\": \"0\",\n \"prefetch\": \"true\",\n \"proxy server\": \"\",\n \"qaqqinilib\": \"\",\n \"query optimize goal\": \"0\",\n \"query replace truncated parameter\": \"\",\n \"query storage limit\": \"-1\",\n \"query timeout mechanism\": \"qqrytimlmt\",\n \"remarks\": \"system\",\n \"secondary URL\": \"\",\n \"server trace\": \"0\",\n \"sort\": \"hex\",\n \"sort language\": \"\",\n \"sort table\": \"\",\n \"sort weight\": \"shared\",\n \"time format\": \"iso\",\n \"time separator\": \":\",\n \"trace\": \"false\",\n \"transaction isolation\": \"read uncommitted\",\n \"translate binary\": \"false\",\n \"translate boolean\": \"true\",\n \"translate hex\": \"character\",\n \"true autocommit\": \"false\",\n \"use block update\": \"false\",\n \"variable field compression\": \"all\"\n },\n \"sqlStatements\": []\n },\n \"jobRemoteCommand\": {\n \"id\": \"094268/QUSER/QZRCSRVS\",\n \"ccsid\": 37,\n \"homeDirectory\": \"/home/USER1\",\n \"curLib\": null,\n \"systemLibl\": [\n {\n \"name\": \"QSYS\",\n \"attribute\": \"PROD\",\n \"description\": \"System Library\"\n },\n {\n \"name\": \"QSYS2\",\n \"attribute\": \"PROD\",\n \"description\": \"System Library for CPI's\"\n },\n {\n \"name\": \"QHLPSYS\",\n \"attribute\": \"PROD\",\n \"description\": \"\"\n },\n {\n \"name\": \"QUSRSYS\",\n \"attribute\": \"PROD\",\n \"description\": \"System Library for Users\"\n }\n ],\n \"userLibl\": [\n {\n \"name\": \"QGPL\",\n \"attribute\": \"PROD\",\n \"description\": \"General Purpose Library\"\n },\n {\n \"name\": \"QTEMP\",\n \"attribute\": \"TEST\",\n \"description\": \"\"\n }\n ],\n \"envVariables\": {},\n \"jobLog\": []\n }\n}")}), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Get information about the session. ", description = "Get information about the session. The information returned includes session settings in addition to information about any host server jobs tied to the session.")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication")})
public Response sessionQuery(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @Parameter(description = "Comma seperated list of environment variables to return from the remote command host server job.", required = false) @QueryParam("envvars") String envvars, @Parameter(description = "Maximum number of message log records to return from the remote command host server job.", required = false) @DefaultValue("0") @QueryParam("maxjoblogrecords") Integer maxJobLogMessages, @Parameter(description = "Comma seperated list of message IDs. Only remote command host server job log messages that do not match the filter message IDs will be returned.", required = false) @QueryParam("joblogfilter") String jobLogFilter) {
CommonUtil._serviceContext.set(req);
Session session = null;
String payload = null;
try {
session = getSession(req, authorization, false);
SessionAPIImpl.SessionSettingsAndJobInfo sessionInfo = SessionAPIImpl.sessionQuery(session, maxJobLogMessages, envvars, jobLogFilter);
payload = sessionInfo.toJSON();
} catch (Exception e) {
return handleException("authQuery", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
return Response.status(Response.Status.OK).entity(payload).build();
}
@Path("/v1/session")
@PUT
@Consumes({"application/json"})
@Produces({"application/json; charset=utf-8"})
@APIResponses({@APIResponse(responseCode = "204", description = "Successful request, no content."), @APIResponse(responseCode = "400", description = "Bad request."), @APIResponse(responseCode = "401", description = "Unauthorized request was made."), @APIResponse(responseCode = "403", description = "The request is forbidden."), @APIResponse(responseCode = "500", description = "Unable to process the request due to an internal server error.")})
@Operation(summary = "Refresh session settings.", description = "Refresh session settings. The settings affect the remote command and database host server jobs that are tied to the session. Refreshing sesson settings may result in the ending of existing host server jobs. ")
@SecurityRequirements({@SecurityRequirement(name = "bearerHttpAuthentication")})
public Response sessionRefresh(@Context HttpServletRequest req, @Parameter(description = "The authorization HTTP header.") @HeaderParam("Authorization") String authorization, @RequestBody(description = "The settings for the session.", required = true, content = {@Content(schema = @Schema(implementation = SessionAPIImpl.RSEAPI_SessionSettings.class), mediaType = "application/json", example = "{\n \"resetSettings\": true,\n \"continueOnError\": true,\n \"libraryList\": [\n \"lib1\",\n \"lib2\"\n ],\n \"clCommands\": [\n \"QSYS/CLRLIB LIB(BUILD)\"\n ],\n \"envVariables\": {\n \"var1\": \"var1val\",\n \"var2\": \"var2val\"\n },\n \"sqlDefaultSchema\": \"lib1\",\n \"sqlProperties\": {\n \"auto commit\": \"true\",\n \"ignore warnings\": \"01003,0100C,01567\"\n },\n \"sqlStatements\": [\n \"SET PATH = LIB1, LIB2\"\n ]\n}")}) SessionAPIImpl.RSEAPI_SessionSettings settings) {
CommonUtil._serviceContext.set(req);
Session session = null;
SessionAPIImpl.SetSessionSettingsResults ssresults = null;
try {
session = getSession(req, authorization, false);
ssresults = SessionAPIImpl.sessionRefresh(session, settings);
} catch (Exception e) {
return handleException("authRefresh", e);
} finally {
releaseSession(session);
}
CommonUtil._serviceContext.remove();
if (ssresults != null && ssresults.errorsOccurred())
return Response.status(Response.Status.OK).entity(ssresults.toJSON()).build();
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -0,0 +1,296 @@
package dev.alexzaw.rest4i.api.jaxrs.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.alexzaw.rest4i.database.QueryResult;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class for formatting database query results into various output formats.
* Supports JSON, XML, CSV, HTML, and Excel formats.
*
* @author alexzaw
*/
public class ResponseFormatter {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
/**
* Formats a QueryResult into the requested format and returns a JAX-RS Response.
*
* @param queryResult The query result to format
* @param format The requested format (json, xml, csv, html, excel)
* @param treatWarningsAsErrors Whether to treat warnings as errors
* @param alwaysReturnSQLStateInformation Whether to always include SQL state info
* @return JAX-RS Response with formatted content
*/
public static Response formatQueryResult(QueryResult queryResult, String format,
boolean treatWarningsAsErrors,
boolean alwaysReturnSQLStateInformation) {
try {
String outputFormat = (format != null) ? format.toLowerCase() : "json";
if (!queryResult.isQuery()) {
// For non-query operations, return update count
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("rowsAffected", queryResult.getUpdateCount());
result.put("message", "Operation completed successfully");
return Response.ok(JSON_MAPPER.writeValueAsString(result))
.type(MediaType.APPLICATION_JSON)
.build();
}
List<Map<String, Object>> data = queryResult.getData();
List<String> columnNames = queryResult.getColumnNames();
switch (outputFormat) {
case "xml":
return formatAsXml(data, columnNames);
case "csv":
return formatAsCsv(data, columnNames);
case "html":
return formatAsHtml(data, columnNames);
case "excel":
return formatAsExcel(data, columnNames);
case "json":
default:
return formatAsJson(data, columnNames);
}
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"error\": \"Error formatting result: " + e.getMessage() + "\"}")
.type(MediaType.APPLICATION_JSON)
.build();
} finally {
try {
queryResult.close();
} catch (SQLException e) {
// Log warning but don't fail the response
}
}
}
/**
* Formats data as JSON response.
*/
private static Response formatAsJson(List<Map<String, Object>> data, List<String> columnNames) throws IOException {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("rowCount", data.size());
result.put("columnNames", columnNames);
result.put("data", data);
String jsonResult = JSON_MAPPER.writeValueAsString(result);
return Response.ok(jsonResult)
.type(MediaType.APPLICATION_JSON)
.build();
}
/**
* Formats data as XML response.
*/
private static Response formatAsXml(List<Map<String, Object>> data, List<String> columnNames) throws IOException {
StringBuilder xml = new StringBuilder();
xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.append("<result>\n");
xml.append(" <success>true</success>\n");
xml.append(" <rowCount>").append(data.size()).append("</rowCount>\n");
xml.append(" <columnNames>\n");
for (String columnName : columnNames) {
xml.append(" <column>").append(escapeXml(columnName)).append("</column>\n");
}
xml.append(" </columnNames>\n");
xml.append(" <data>\n");
for (Map<String, Object> row : data) {
xml.append(" <row>\n");
for (String columnName : columnNames) {
Object value = row.get(columnName);
String cellValue = (value != null) ? escapeXml(value.toString()) : "";
xml.append(" <").append(escapeXml(columnName)).append(">")
.append(cellValue)
.append("</").append(escapeXml(columnName)).append(">\n");
}
xml.append(" </row>\n");
}
xml.append(" </data>\n");
xml.append("</result>");
return Response.ok(xml.toString())
.type(MediaType.APPLICATION_XML)
.build();
}
/**
* Formats data as CSV response.
*/
private static Response formatAsCsv(List<Map<String, Object>> data, List<String> columnNames) {
StringBuilder csv = new StringBuilder();
// Add header row
if (!columnNames.isEmpty()) {
csv.append(String.join(",", columnNames));
csv.append("\n");
}
// Add data rows
for (Map<String, Object> row : data) {
for (int i = 0; i < columnNames.size(); i++) {
if (i > 0) csv.append(",");
Object value = row.get(columnNames.get(i));
String cellValue = (value != null) ? escapeCsvValue(value.toString()) : "";
csv.append(cellValue);
}
csv.append("\n");
}
return Response.ok(csv.toString())
.type("text/csv")
.header("Content-Disposition", "attachment; filename=\"query_result.csv\"")
.build();
}
/**
* Formats data as HTML table response.
*/
private static Response formatAsHtml(List<Map<String, Object>> data, List<String> columnNames) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html><head><title>Query Results</title>");
html.append("<style>");
html.append("table { border-collapse: collapse; width: 100%; }");
html.append("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }");
html.append("th { background-color: #f2f2f2; font-weight: bold; }");
html.append("tr:nth-child(even) { background-color: #f9f9f9; }");
html.append("</style></head><body>");
html.append("<h2>Query Results</h2>");
html.append("<p>Total rows: ").append(data.size()).append("</p>");
html.append("<table>");
// Add header row
if (!columnNames.isEmpty()) {
html.append("<thead><tr>");
for (String columnName : columnNames) {
html.append("<th>").append(escapeHtml(columnName)).append("</th>");
}
html.append("</tr></thead>");
}
// Add data rows
html.append("<tbody>");
for (Map<String, Object> row : data) {
html.append("<tr>");
for (String columnName : columnNames) {
Object value = row.get(columnName);
String cellValue = (value != null) ? escapeHtml(value.toString()) : "";
html.append("<td>").append(cellValue).append("</td>");
}
html.append("</tr>");
}
html.append("</tbody></table>");
html.append("</body></html>");
return Response.ok(html.toString())
.type(MediaType.TEXT_HTML)
.build();
}
/**
* Formats data as Excel spreadsheet response.
*/
private static Response formatAsExcel(List<Map<String, Object>> data, List<String> columnNames) throws IOException {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Query Results");
// Create header row
if (!columnNames.isEmpty()) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < columnNames.size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(columnNames.get(i));
}
}
// Create data rows
for (int rowIndex = 0; rowIndex < data.size(); rowIndex++) {
Row row = sheet.createRow(rowIndex + 1);
Map<String, Object> rowData = data.get(rowIndex);
for (int colIndex = 0; colIndex < columnNames.size(); colIndex++) {
Cell cell = row.createCell(colIndex);
Object value = rowData.get(columnNames.get(colIndex));
if (value != null) {
if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value.toString());
}
}
}
}
// Auto-size columns
for (int i = 0; i < columnNames.size(); i++) {
sheet.autoSizeColumn(i);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
workbook.write(baos);
return Response.ok(baos.toByteArray())
.type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
.header("Content-Disposition", "attachment; filename=\"query_result.xlsx\"")
.build();
}
}
/**
* Escapes CSV values that contain commas, quotes, or newlines.
*/
private static String escapeCsvValue(String value) {
if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}
/**
* Escapes HTML special characters.
*/
private static String escapeHtml(String value) {
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
}
/**
* Escapes XML special characters.
*/
private static String escapeXml(String value) {
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
}

View File

@@ -0,0 +1,540 @@
package dev.alexzaw.rest4i.config;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JsonUtils;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Enhanced Configuration Manager based on REST4i's ConfigurationManager.
*
* Provides configuration management with multiple search paths, singleton pattern,
* concurrent caching, and hot reload capability while maintaining compatibility
* with existing RSE session authentication.
*
* Features:
* - Multiple configuration search paths
* - Singleton pattern with thread-safe operations
* - Hot reload capability with file watchers
* - JSON and Properties file support
* - Integration with existing GlobalProperties where needed
*
* @author alexzaw
*/
public class EnhancedConfigurationManager {
private static final Logger logger = Logger.getLogger(EnhancedConfigurationManager.class.getName());
// Singleton instance
private static volatile EnhancedConfigurationManager instance;
private static final Object INSTANCE_LOCK = new Object();
// Configuration search paths (in order of precedence)
private static final String[] CONFIG_SEARCH_PATHS = {
"/QIBM/UserData/OS/RSEAPI/",
"/etc/misc/configs/",
"/tmp/Rest4i-configs/"
};
// Configuration file names to search for
private static final String[] CONFIG_FILE_NAMES = {
"enhanced-config.properties",
"enhanced-config.json",
"rest4i-config.properties",
"rest4i-config.json"
};
// Internal storage
private final Map<String, Object> configCache = new ConcurrentHashMap<>();
private final Map<String, Long> fileLastModified = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
// File watching
private WatchService watchService;
private Thread watchThread;
private volatile boolean watchingEnabled = true;
// Default configuration values
private static final Map<String, Object> DEFAULT_CONFIG = createDefaultConfig();
/**
* Creates the default configuration map.
* Using a method instead of Map.of() to support more than 10 key-value pairs in Java 11.
*
* @return The default configuration map
*/
private static Map<String, Object> createDefaultConfig() {
Map<String, Object> config = new HashMap<>();
config.put("connection.pool.maxActive", 20);
config.put("connection.pool.maxIdle", 5);
config.put("connection.pool.minIdle", 2);
config.put("connection.pool.maxWait", 30000);
config.put("connection.pool.validationQuery", "SELECT 1 FROM SYSIBM.SYSDUMMY1");
config.put("connection.pool.testOnBorrow", true);
config.put("connection.pool.testWhileIdle", true);
config.put("api.response.includeTimestamps", true);
config.put("api.response.includeDebugInfo", false);
config.put("security.allowApiTokens", true);
config.put("validation.strictMode", false);
// Additional pool-specific configuration that matches AS400ConnectionPool API
config.put("connection.pool.maxConnections", 20);
config.put("connection.pool.maxInactivityTime", 300000); // 5 minutes
config.put("connection.pool.maxLifetime", 1800000); // 30 minutes
config.put("connection.pool.maxUseCount", 1000);
config.put("connection.pool.pretestConnections", true);
config.put("connection.pool.runMaintenance", true);
config.put("connection.pool.cleanupInterval", 60000); // 1 minute
return Collections.unmodifiableMap(config);
}
/**
* Private constructor for singleton pattern.
*/
private EnhancedConfigurationManager() {
initializeConfiguration();
startFileWatcher();
}
/**
* Gets the singleton instance of EnhancedConfigurationManager.
*
* @return The singleton instance
*/
public static EnhancedConfigurationManager getInstance() {
if (instance == null) {
synchronized (INSTANCE_LOCK) {
if (instance == null) {
instance = new EnhancedConfigurationManager();
}
}
}
return instance;
}
/**
* Initializes the configuration by loading from all available sources.
*/
private void initializeConfiguration() {
cacheLock.writeLock().lock();
try {
// Start with default configuration
configCache.putAll(DEFAULT_CONFIG);
// Load configuration from files in search paths
for (String searchPath : CONFIG_SEARCH_PATHS) {
loadConfigurationsFromPath(searchPath);
}
// Load from GlobalProperties for backwards compatibility
loadFromGlobalProperties();
logger.info("Configuration initialized with " + configCache.size() + " properties");
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* Loads configuration files from a specific path.
*
* @param searchPath The path to search for configuration files
*/
private void loadConfigurationsFromPath(String searchPath) {
Path path = Paths.get(searchPath);
if (!Files.exists(path) || !Files.isDirectory(path)) {
logger.fine("Configuration path does not exist: " + searchPath);
return;
}
for (String configFileName : CONFIG_FILE_NAMES) {
Path configFile = path.resolve(configFileName);
if (Files.exists(configFile) && Files.isRegularFile(configFile)) {
try {
loadConfigurationFile(configFile);
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to load configuration file: " + configFile, e);
}
}
}
}
/**
* Loads a specific configuration file.
*
* @param configFile The configuration file to load
* @throws IOException If the file cannot be read
*/
private void loadConfigurationFile(Path configFile) throws IOException {
String fileName = configFile.getFileName().toString().toLowerCase();
long lastModified = Files.getLastModifiedTime(configFile).toMillis();
// Check if file has been modified since last load
String fileKey = configFile.toString();
if (fileLastModified.containsKey(fileKey) && fileLastModified.get(fileKey) == lastModified) {
return; // File hasn't changed, skip loading
}
logger.info("Loading configuration from: " + configFile);
if (fileName.endsWith(".json")) {
loadJsonConfiguration(configFile);
} else if (fileName.endsWith(".properties")) {
loadPropertiesConfiguration(configFile);
}
fileLastModified.put(fileKey, lastModified);
}
/**
* Loads configuration from a JSON file.
*
* @param configFile The JSON configuration file
* @throws IOException If the file cannot be read or parsed
*/
private void loadJsonConfiguration(Path configFile) throws IOException {
try (InputStream inputStream = Files.newInputStream(configFile)) {
@SuppressWarnings("unchecked")
Map<String, Object> jsonConfig = JsonUtils.parseJsonToMap(inputStream);
flattenAndAddToCache("", jsonConfig);
}
}
/**
* Loads configuration from a Properties file.
*
* @param configFile The Properties configuration file
* @throws IOException If the file cannot be read
*/
private void loadPropertiesConfiguration(Path configFile) throws IOException {
Properties props = new Properties();
try (InputStream inputStream = Files.newInputStream(configFile)) {
props.load(inputStream);
}
for (String key : props.stringPropertyNames()) {
configCache.put(key, props.getProperty(key));
}
}
/**
* Flattens a nested JSON structure and adds it to the cache.
*
* @param prefix The current key prefix
* @param map The map to flatten
*/
private void flattenAndAddToCache(String prefix, Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nestedMap = (Map<String, Object>) value;
flattenAndAddToCache(key, nestedMap);
} else {
configCache.put(key, value);
}
}
}
/**
* Loads configuration from GlobalProperties for backwards compatibility.
*/
private void loadFromGlobalProperties() {
// Add any relevant GlobalProperties values
// This maintains compatibility with existing RSE configuration
String rseApiPath = "/QIBM/UserData/OS/RSEAPI";
if (Files.exists(Paths.get(rseApiPath))) {
configCache.put("rse.config.path", rseApiPath);
}
// Add other RSE-related configuration as needed
configCache.put("rse.version", "1.0.7"); // From GlobalProperties.RSEAPI_VERSION
}
/**
* Starts the file watcher for hot reload capability.
*/
private void startFileWatcher() {
try {
watchService = FileSystems.getDefault().newWatchService();
// Register watch keys for all configuration directories
for (String searchPath : CONFIG_SEARCH_PATHS) {
Path path = Paths.get(searchPath);
if (Files.exists(path) && Files.isDirectory(path)) {
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
logger.fine("Registered file watcher for: " + searchPath);
}
}
// Start the watch thread
watchThread = new Thread(this::processWatchEvents);
watchThread.setName("ConfigurationWatcher");
watchThread.setDaemon(true);
watchThread.start();
logger.info("Configuration file watcher started");
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to start configuration file watcher", e);
}
}
/**
* Processes file system watch events for configuration hot reload.
*/
private void processWatchEvents() {
while (watchingEnabled && watchService != null) {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path fileName = pathEvent.context();
if (isConfigurationFile(fileName.toString())) {
logger.info("Configuration file changed: " + fileName + ", reloading...");
reloadConfiguration();
}
}
boolean valid = key.reset();
if (!valid) {
break; // Directory no longer accessible
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.log(Level.WARNING, "Error processing configuration file changes", e);
}
}
}
/**
* Checks if a file is a configuration file we care about.
*
* @param fileName The file name to check
* @return true if it's a configuration file
*/
private boolean isConfigurationFile(String fileName) {
String lowerFileName = fileName.toLowerCase();
return Arrays.stream(CONFIG_FILE_NAMES)
.anyMatch(configName -> lowerFileName.equals(configName.toLowerCase()));
}
/**
* Reloads the configuration from all sources.
*/
public void reloadConfiguration() {
cacheLock.writeLock().lock();
try {
configCache.clear();
fileLastModified.clear();
initializeConfiguration();
logger.info("Configuration reloaded successfully");
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* Gets a string configuration value.
*
* @param key The configuration key
* @param defaultValue The default value if key is not found
* @return The configuration value
*/
public String getString(String key, String defaultValue) {
cacheLock.readLock().lock();
try {
Object value = configCache.get(key);
return value != null ? value.toString() : defaultValue;
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Gets an integer configuration value.
*
* @param key The configuration key
* @param defaultValue The default value if key is not found
* @return The configuration value
*/
public int getInt(String key, int defaultValue) {
cacheLock.readLock().lock();
try {
Object value = configCache.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
} else if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
logger.warning("Invalid integer value for key '" + key + "': " + value);
}
}
return defaultValue;
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Gets a boolean configuration value.
*
* @param key The configuration key
* @param defaultValue The default value if key is not found
* @return The configuration value
*/
public boolean getBoolean(String key, boolean defaultValue) {
cacheLock.readLock().lock();
try {
Object value = configCache.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
} else if (value instanceof String) {
return Boolean.parseBoolean((String) value);
}
return defaultValue;
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Gets a long configuration value.
*
* @param key The configuration key
* @param defaultValue The default value if key is not found
* @return The configuration value
*/
public long getLong(String key, long defaultValue) {
cacheLock.readLock().lock();
try {
Object value = configCache.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
} else if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
logger.warning("Invalid long value for key '" + key + "': " + value);
}
}
return defaultValue;
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Sets a configuration value (runtime only, not persisted).
*
* @param key The configuration key
* @param value The configuration value
*/
public void setProperty(String key, Object value) {
cacheLock.writeLock().lock();
try {
configCache.put(key, value);
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* Gets all configuration properties as a map.
*
* @return A copy of all configuration properties
*/
public Map<String, Object> getAllProperties() {
cacheLock.readLock().lock();
try {
return new HashMap<>(configCache);
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Gets all properties with a specific prefix.
*
* @param prefix The prefix to filter by
* @return A map of matching properties (with prefix removed from keys)
*/
public Map<String, Object> getPropertiesWithPrefix(String prefix) {
cacheLock.readLock().lock();
try {
Map<String, Object> result = new HashMap<>();
String searchPrefix = prefix.endsWith(".") ? prefix : prefix + ".";
for (Map.Entry<String, Object> entry : configCache.entrySet()) {
String key = entry.getKey();
if (key.startsWith(searchPrefix)) {
String newKey = key.substring(searchPrefix.length());
result.put(newKey, entry.getValue());
}
}
return result;
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Checks if a configuration key exists.
*
* @param key The configuration key
* @return true if the key exists
*/
public boolean hasProperty(String key) {
cacheLock.readLock().lock();
try {
return configCache.containsKey(key);
} finally {
cacheLock.readLock().unlock();
}
}
/**
* Shuts down the configuration manager and releases resources.
*/
public void shutdown() {
watchingEnabled = false;
if (watchThread != null) {
watchThread.interrupt();
}
if (watchService != null) {
try {
watchService.close();
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing watch service", e);
}
}
logger.info("Configuration manager shut down");
}
}

View File

@@ -0,0 +1,164 @@
package dev.alexzaw.rest4i.database;
import dev.alexzaw.rest4i.session.Session;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.*;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Database Manager for handling database connections and queries
* using HikariCP connection pooling with IBM i integration.
*
* @author alexzaw
*/
public class DatabaseManager {
private static final ConcurrentMap<String, HikariDataSource> CONNECTION_POOLS = new ConcurrentHashMap<>();
/**
* Executes a SQL query using the session's database connection information.
*
* @param session The authenticated session containing connection details
* @param sqlQuery The SQL query to execute
* @return QueryResult containing the results
* @throws SQLException if query execution fails
*/
public QueryResult executeQuery(Session session, String sqlQuery) throws SQLException {
Connection connection = getConnection(session);
try (PreparedStatement stmt = connection.prepareStatement(sqlQuery)) {
boolean hasResultSet = stmt.execute();
if (hasResultSet) {
ResultSet rs = stmt.getResultSet();
return new QueryResult(rs, stmt);
} else {
int updateCount = stmt.getUpdateCount();
return new QueryResult(updateCount);
}
}
}
/**
* Executes a SQL query using an existing database connection.
*
* @param connection The database connection to use
* @param sqlQuery The SQL query to execute
* @return QueryResult containing the results
* @throws SQLException if query execution fails
*/
public QueryResult executeQueryWithConnection(Connection connection, String sqlQuery) throws SQLException {
try (PreparedStatement stmt = connection.prepareStatement(sqlQuery)) {
boolean hasResultSet = stmt.execute();
if (hasResultSet) {
ResultSet rs = stmt.getResultSet();
return new QueryResult(rs, stmt);
} else {
int updateCount = stmt.getUpdateCount();
return new QueryResult(updateCount);
}
}
}
/**
* Gets a database connection from the connection pool for the given session.
*
* @param session The session containing connection details
* @return Database connection
* @throws SQLException if connection cannot be established
*/
private Connection getConnection(Session session) throws SQLException {
String poolKey = getPoolKey(session);
HikariDataSource dataSource = CONNECTION_POOLS.computeIfAbsent(poolKey, k -> {
try {
return createDataSource(session);
} catch (Exception e) {
throw new RuntimeException("Failed to create data source", e);
}
});
return dataSource.getConnection();
}
/**
* Creates a HikariCP data source for the session.
*
* @param session The session containing connection details
* @return HikariDataSource configured for the session
*/
private HikariDataSource createDataSource(Session session) {
HikariConfig config = new HikariConfig();
// Get connection details from session
String server = session.getHost();
String userId = session.getUserid();
// Note: Password would need to be handled securely in production
// IBM i JDBC URL
String jdbcUrl = String.format("jdbc:as400://%s;libraries=*LIBL;prompt=false;date format=iso;naming=system;translate binary=true;translate hex=character", server);
config.setJdbcUrl(jdbcUrl);
config.setUsername(userId);
config.setDriverClassName("com.ibm.as400.access.AS400JDBCDriver");
// Connection pool settings
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setLeakDetectionThreshold(60000);
// Pool name for monitoring
config.setPoolName("IBMi-Pool-" + server + "-" + userId);
return new HikariDataSource(config);
}
/**
* Generates a unique pool key for the session.
*
* @param session The session
* @return Unique pool key
*/
private String getPoolKey(Session session) {
return session.getHost() + ":" + session.getUserid();
}
/**
* Validates a SQL query to prevent dangerous operations.
*
* @param sqlQuery The query to validate
* @return true if query is safe, false otherwise
*/
public boolean validateQuery(String sqlQuery) {
if (sqlQuery == null || sqlQuery.trim().isEmpty()) {
return false;
}
String lowercaseQuery = sqlQuery.toLowerCase();
// Block dangerous operations
return !(lowercaseQuery.contains("truncate") ||
lowercaseQuery.contains("drop") ||
lowercaseQuery.contains("delete from") ||
lowercaseQuery.contains("alter") ||
lowercaseQuery.contains("create") ||
lowercaseQuery.contains("grant") ||
lowercaseQuery.contains("revoke"));
}
/**
* Closes all connection pools. Should be called on application shutdown.
*/
public static void closeAllPools() {
CONNECTION_POOLS.values().forEach(HikariDataSource::close);
CONNECTION_POOLS.clear();
}
}

View File

@@ -0,0 +1,205 @@
package dev.alexzaw.rest4i.database;
import java.sql.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Container for SQL query results that can be formatted into various output types.
*
* @author alexzaw
*/
public class QueryResult implements AutoCloseable {
private final ResultSet resultSet;
private final PreparedStatement statement;
private final int updateCount;
private final boolean isQuery;
private List<Map<String, Object>> data;
private ResultSetMetaData metaData;
/**
* Constructor for query results (SELECT statements).
*
* @param resultSet The result set
* @param statement The statement (kept open for proper resource management)
*/
public QueryResult(ResultSet resultSet, PreparedStatement statement) throws SQLException {
this.resultSet = resultSet;
this.statement = statement;
this.updateCount = -1;
this.isQuery = true;
this.metaData = resultSet.getMetaData();
this.data = null; // Lazy loading
}
/**
* Constructor for non-query results (INSERT, UPDATE, DELETE statements).
*
* @param updateCount The number of affected rows
*/
public QueryResult(int updateCount) {
this.resultSet = null;
this.statement = null;
this.updateCount = updateCount;
this.isQuery = false;
this.metaData = null;
this.data = new ArrayList<>();
}
/**
* Returns whether this result has a result set (from SELECT query).
*
* @return true if this is a query result with data
*/
public boolean isQuery() {
return isQuery;
}
/**
* Gets the update count for non-query operations.
*
* @return number of affected rows, or -1 if this is a query result
*/
public int getUpdateCount() {
return updateCount;
}
/**
* Gets the result set data as a list of maps.
* Each map represents a row with column names as keys.
*
* @return List of row data
* @throws SQLException if data reading fails
*/
public List<Map<String, Object>> getData() throws SQLException {
if (!isQuery) {
return data;
}
if (data == null) {
loadData();
}
return data;
}
/**
* Gets the column names from the result set.
*
* @return List of column names
* @throws SQLException if metadata reading fails
*/
public List<String> getColumnNames() throws SQLException {
if (!isQuery || metaData == null) {
return new ArrayList<>();
}
List<String> columnNames = new ArrayList<>();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
columnNames.add(metaData.getColumnName(i));
}
return columnNames;
}
/**
* Gets the column types from the result set.
*
* @return List of column type names
* @throws SQLException if metadata reading fails
*/
public List<String> getColumnTypes() throws SQLException {
if (!isQuery || metaData == null) {
return new ArrayList<>();
}
List<String> columnTypes = new ArrayList<>();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
columnTypes.add(metaData.getColumnTypeName(i));
}
return columnTypes;
}
/**
* Gets the number of columns in the result set.
*
* @return column count
* @throws SQLException if metadata reading fails
*/
public int getColumnCount() throws SQLException {
if (!isQuery || metaData == null) {
return 0;
}
return metaData.getColumnCount();
}
/**
* Lazy loads the data from the result set.
*
* @throws SQLException if data reading fails
*/
private void loadData() throws SQLException {
data = new ArrayList<>();
if (resultSet == null) {
return;
}
List<String> columnNames = getColumnNames();
while (resultSet.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 0; i < columnNames.size(); i++) {
String columnName = columnNames.get(i);
Object value = resultSet.getObject(i + 1);
// Handle SQL NULL values
if (resultSet.wasNull()) {
value = null;
}
row.put(columnName, value);
}
data.add(row);
}
}
/**
* Gets the number of rows in the result set.
* Note: This will load all data if not already loaded.
*
* @return number of rows
* @throws SQLException if data reading fails
*/
public int getRowCount() throws SQLException {
if (!isQuery) {
return 0;
}
return getData().size();
}
@Override
public void close() throws SQLException {
try {
if (resultSet != null && !resultSet.isClosed()) {
resultSet.close();
}
} finally {
if (statement != null && !statement.isClosed()) {
statement.close();
}
}
}
}

View File

@@ -0,0 +1,350 @@
package dev.alexzaw.rest4i.database.formatter;
import dev.alexzaw.rest4i.database.QueryResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonGenerator;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.ByteArrayOutputStream;
import java.sql.SQLException;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
/**
* Output formatter for database query results.
* Supports multiple output formats: JSON, XML, CSV, HTML, Excel.
*
* @author alexzaw
*/
public class OutputFormatter {
private final ObjectMapper objectMapper;
public OutputFormatter() {
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, false);
}
/**
* Outputs query results in JSON format.
*
* @param response HTTP response
* @param result Query result to format
* @throws IOException if writing fails
* @throws SQLException if data reading fails
*/
public void outputJson(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
if (!result.isQuery()) {
// Non-query result
Map<String, Object> responseObj = new HashMap<>();
responseObj.put("success", true);
responseObj.put("message", "SQL executed successfully");
responseObj.put("rowsAffected", result.getUpdateCount());
objectMapper.writeValue(response.getWriter(), responseObj);
} else {
// Query result
List<Map<String, Object>> data = result.getData();
Map<String, Object> responseObj = new HashMap<>();
responseObj.put("success", true);
responseObj.put("rowCount", data.size());
responseObj.put("data", data);
objectMapper.writeValue(response.getWriter(), responseObj);
}
}
/**
* Outputs query results in XML format.
*
* @param response HTTP response
* @param result Query result to format
* @throws IOException if writing fails
* @throws SQLException if data reading fails
*/
public void outputXml(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
response.setContentType("application/xml; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
if (!result.isQuery()) {
writer.println("<result>");
writer.println(" <success>true</success>");
writer.println(" <message>SQL executed successfully</message>");
writer.println(" <rowsAffected>" + result.getUpdateCount() + "</rowsAffected>");
writer.println("</result>");
} else {
List<Map<String, Object>> data = result.getData();
writer.println("<result>");
writer.println(" <success>true</success>");
writer.println(" <rowCount>" + data.size() + "</rowCount>");
writer.println(" <data>");
for (Map<String, Object> row : data) {
writer.println(" <row>");
for (Map.Entry<String, Object> entry : row.entrySet()) {
String value = entry.getValue() != null ? escapeXml(entry.getValue().toString()) : "";
writer.println(" <" + entry.getKey() + ">" + value + "</" + entry.getKey() + ">");
}
writer.println(" </row>");
}
writer.println(" </data>");
writer.println("</result>");
}
}
/**
* Outputs query results in CSV format.
*
* @param response HTTP response
* @param result Query result to format
* @throws IOException if writing fails
* @throws SQLException if data reading fails
*/
public void outputCsv(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
response.setContentType("text/csv; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"query_result.csv\"");
PrintWriter writer = response.getWriter();
if (!result.isQuery()) {
writer.println("Success,Message,RowsAffected");
writer.println("true,\"SQL executed successfully\"," + result.getUpdateCount());
} else {
List<Map<String, Object>> data = result.getData();
if (!data.isEmpty()) {
// Write header
Map<String, Object> firstRow = data.get(0);
boolean first = true;
for (String key : firstRow.keySet()) {
if (!first) writer.print(",");
writer.print("\"" + escapeCsv(key) + "\"");
first = false;
}
writer.println();
// Write data rows
for (Map<String, Object> row : data) {
first = true;
for (Object value : row.values()) {
if (!first) writer.print(",");
String valueStr = value != null ? value.toString() : "";
writer.print("\"" + escapeCsv(valueStr) + "\"");
first = false;
}
writer.println();
}
}
}
}
/**
* Outputs query results in HTML format.
*
* @param response HTTP response
* @param result Query result to format
* @throws IOException if writing fails
* @throws SQLException if data reading fails
*/
public void outputHtml(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
response.setContentType("text/html; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html>");
writer.println("<head>");
writer.println(" <title>Query Results</title>");
writer.println(" <style>");
writer.println(" table { border-collapse: collapse; width: 100%; }");
writer.println(" th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }");
writer.println(" th { background-color: #f2f2f2; font-weight: bold; }");
writer.println(" tr:nth-child(even) { background-color: #f9f9f9; }");
writer.println(" </style>");
writer.println("</head>");
writer.println("<body>");
writer.println(" <h1>Query Results</h1>");
if (!result.isQuery()) {
writer.println(" <p><strong>Success:</strong> true</p>");
writer.println(" <p><strong>Message:</strong> SQL executed successfully</p>");
writer.println(" <p><strong>Rows Affected:</strong> " + result.getUpdateCount() + "</p>");
} else {
List<Map<String, Object>> data = result.getData();
writer.println(" <p><strong>Row Count:</strong> " + data.size() + "</p>");
if (!data.isEmpty()) {
writer.println(" <table>");
writer.println(" <thead>");
writer.println(" <tr>");
// Write header
Map<String, Object> firstRow = data.get(0);
for (String key : firstRow.keySet()) {
writer.println(" <th>" + escapeHtml(key) + "</th>");
}
writer.println(" </tr>");
writer.println(" </thead>");
writer.println(" <tbody>");
// Write data rows
for (Map<String, Object> row : data) {
writer.println(" <tr>");
for (Object value : row.values()) {
String valueStr = value != null ? value.toString() : "";
writer.println(" <td>" + escapeHtml(valueStr) + "</td>");
}
writer.println(" </tr>");
}
writer.println(" </tbody>");
writer.println(" </table>");
}
}
writer.println("</body>");
writer.println("</html>");
}
/**
* Outputs query results in Excel format.
*
* @param response HTTP response
* @param result Query result to format
* @throws IOException if writing fails
* @throws SQLException if data reading fails
*/
public void outputExcel(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"query_result.xlsx\"");
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Query Results");
if (!result.isQuery()) {
// Non-query result
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("Success");
headerRow.createCell(1).setCellValue("Message");
headerRow.createCell(2).setCellValue("RowsAffected");
Row dataRow = sheet.createRow(1);
dataRow.createCell(0).setCellValue("true");
dataRow.createCell(1).setCellValue("SQL executed successfully");
dataRow.createCell(2).setCellValue(result.getUpdateCount());
} else {
List<Map<String, Object>> data = result.getData();
if (!data.isEmpty()) {
// Create header row
Row headerRow = sheet.createRow(0);
Map<String, Object> firstRow = data.get(0);
int colIndex = 0;
for (String key : firstRow.keySet()) {
Cell cell = headerRow.createCell(colIndex++);
cell.setCellValue(key);
// Style header cells
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
cell.setCellStyle(headerStyle);
}
// Create data rows
int rowIndex = 1;
for (Map<String, Object> row : data) {
Row excelRow = sheet.createRow(rowIndex++);
colIndex = 0;
for (Object value : row.values()) {
Cell cell = excelRow.createCell(colIndex++);
if (value != null) {
if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value.toString());
}
}
}
}
// Auto-size columns
for (int i = 0; i < firstRow.size(); i++) {
sheet.autoSizeColumn(i);
}
}
}
// Write to response
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
workbook.write(baos);
baos.writeTo(response.getOutputStream());
}
}
}
/**
* Sends an error response in JSON format.
*
* @param response HTTP response
* @param statusCode HTTP status code
* @param message Error message
* @throws IOException if writing fails
*/
public void sendErrorResponse(HttpServletResponse response, int statusCode, String message) throws IOException {
response.setStatus(statusCode);
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("error", message);
errorResponse.put("statusCode", statusCode);
objectMapper.writeValue(response.getWriter(), errorResponse);
}
/**
* Escapes XML special characters.
*/
private String escapeXml(String text) {
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
/**
* Escapes HTML special characters.
*/
private String escapeHtml(String text) {
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
}
/**
* Escapes CSV special characters.
*/
private String escapeCsv(String text) {
return text.replace("\"", "\"\"");
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWABadRequestException extends RestWAException {
private static final long serialVersionUID = 4190967117943058893L;
public RestWABadRequestException() {
this("One or more properties in request is not valid or is missing.", new Object[0]);
}
public RestWABadRequestException(String message, Object... args) {
super(400, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAConflictException extends RestWAException {
private static final long serialVersionUID = 4445144621889651977L;
public RestWAConflictException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAConflictException(String message, Object... args) {
super(409, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,165 @@
package dev.alexzaw.rest4i.exception;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.time.Instant;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
public abstract class RestWAException extends WebApplicationException {
private static final long serialVersionUID = 4989164091865764152L;
protected static final String CT_UTF8 = "application/json; charset=utf-8";
public static final int SC_BAD_REQUEST = 400;
public static final int SC_CONFLICT = 409;
public static final int SC_CREATED = 201;
public static final int SC_FORBIDDEN = 403;
public static final int SC_INTERNAL_SERVER_ERROR = 500;
public static final int SC_INSUFFICIENT_STORAGE = 507;
public static final int SC_NO_CONTENT = 204;
public static final int SC_NOT_FOUND = 404;
public static final int SC_NOT_MODIFIED = 304;
public static final int SC_OK = 200;
public static final int SC_PARTIAL_CONTENT = 206;
public static final int SC_PRECONDITION_FAILED = 412;
public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
public static final int SC_REQUEST_TIMEOUT = 408;
public static final int SC_SERVICE_UNAVAILABLE = 503;
public static final int SC_TOO_MANY_REQUESTS = 429;
public static final int SC_UNAUTHORIZED = 401;
private static HashMap<Integer, String> statusMap = new HashMap<>();
static {
statusMap.put(Integer.valueOf(400), "BAD_REQUEST");
statusMap.put(Integer.valueOf(409), "CONFLICT");
statusMap.put(Integer.valueOf(201), "CREATED");
statusMap.put(Integer.valueOf(403), "FORBIDDEN");
statusMap.put(Integer.valueOf(500), "INTERNAL_SERVER_ERROR");
statusMap.put(Integer.valueOf(507), "INSUFFICIENT_STORAGE");
statusMap.put(Integer.valueOf(204), "NO_CONTENT");
statusMap.put(Integer.valueOf(404), "NOT_FOUND");
statusMap.put(Integer.valueOf(304), "NOT_MODIFIED");
statusMap.put(Integer.valueOf(200), "OK");
statusMap.put(Integer.valueOf(206), "PARTIAL_CONTENT");
statusMap.put(Integer.valueOf(412), "PRECONDITION_FAILED");
statusMap.put(Integer.valueOf(413), "ENTITY_TOO_LARGE");
statusMap.put(Integer.valueOf(408), "REQUEST_TIMEOUT");
statusMap.put(Integer.valueOf(503), "SERVICE_UNAVAILABLE");
statusMap.put(Integer.valueOf(429), "TOO_MANY_REQUESTS");
statusMap.put(Integer.valueOf(401), "UNAUTHORIZED");
}
public RestWAException(int statusCode, String contentType, String message, Object[] args) {
super(Response.status(statusCode).entity(toJSON(statusCode, (args == null || args.length == 0) ? message : String.format(message, args))).type(contentType).build());
}
public RestWAException(int statusCode, String contentType, String header, String headerValue, String message, Object[] args) {
super(Response.status(statusCode).entity(toJSON(statusCode, (args == null || args.length == 0) ? message : String.format(message, args))).header(header, headerValue).type(contentType).build());
}
public RestWAException(int statusCode, String message) {
this(statusCode, "application/json; charset=utf-8", message, null);
}
public RestWAException(int statusCode, String contentType, String message) {
this(statusCode, contentType, message, null);
}
public static String toJSON(int status, String message) {
StringBuilder sb = new StringBuilder();
Instant timestamp = Instant.now();
HttpServletRequest req = CommonUtil._serviceContext.get();
String instance = null;
if (req != null)
instance = req.getRequestURI();
if (instance == null)
instance = "";
String method = req.getMethod();
sb.append("{ ");
sb.append("\"").append("title").append("\": \"").append(toString(status)).append("\", ");
sb.append("\"").append("status").append("\": ").append(status).append(", ");
sb.append("\"").append("detail").append("\": \"").append(JSONSerializer.escapeJSON(message)).append("\", ");
sb.append("\"").append("method").append("\": \"").append(JSONSerializer.escapeJSON(method)).append("\", ");
sb.append("\"").append("instance").append("\": \"").append(JSONSerializer.escapeJSON(instance)).append("\", ");
sb.append("\"").append("timestamp").append("\": \"").append(timestamp.toString()).append("\" ");
sb.append("}");
return sb.toString();
}
public static String toString(int status) {
String stringStatus = statusMap.get(Integer.valueOf(status));
return (stringStatus == null) ? "Unable to process the request due to an internal error." : stringStatus;
}
public static class ErrorInfo {
private String message = "Unable to process the request due to an internal error.";
private int httpStatusCode = 500;
private Integer rc = null;
public ErrorInfo(int httpStatusCode, String message) {
this(null, httpStatusCode, message, new Object[0]);
}
public ErrorInfo(Integer rc, int httpStatusCode, String message, Object... args) {
this.rc = rc;
this.message = (args == null || args.length == 0) ? message : String.format(message, args);
this.httpStatusCode = httpStatusCode;
}
public int getRC() {
return this.rc.intValue();
}
public void generateException() {
String excMessage = this.message;
if (this.rc != null)
excMessage = String.valueOf(excMessage) + " [" + this.rc + "]";
switch (this.httpStatusCode) {
case 400:
throw new RestWABadRequestException(excMessage, new Object[0]);
case 409:
throw new RestWAConflictException(excMessage, new Object[0]);
case 403:
throw new RestWAForbiddenException(excMessage, new Object[0]);
case 507:
throw new RestWAInsufficientStorageException(excMessage, new Object[0]);
case 404:
throw new RestWANotFoundException(excMessage, new Object[0]);
case 412:
throw new RestWAPreconditionFailedException(excMessage, new Object[0]);
case 413:
throw new RestWAReqEntityTooLargeException(excMessage, new Object[0]);
case 408:
throw new RestWARequestTimeoutException(excMessage, new Object[0]);
case 503:
throw new RestWAServiceUnavailableException(excMessage, new Object[0]);
case 401:
throw new RestWAUnauthorizedException(excMessage, new Object[0]);
}
throw new RestWAInternalServerErrorException(excMessage, new Object[0]);
}
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAForbiddenException extends RestWAException {
private static final long serialVersionUID = -6913303980223665012L;
public RestWAForbiddenException() {
this("Not authorized to perform operation.", new Object[0]);
}
public RestWAForbiddenException(String message, Object... args) {
super(403, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAInsufficientStorageException extends RestWAException {
private static final long serialVersionUID = 4190967117943058893L;
public RestWAInsufficientStorageException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAInsufficientStorageException(String message, Object... args) {
super(400, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAInternalServerErrorException extends RestWAException {
private static final long serialVersionUID = -506355880927076778L;
public RestWAInternalServerErrorException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAInternalServerErrorException(String message, Object... args) {
super(500, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWANoContentException extends RestWAException {
private static final long serialVersionUID = 8198515127228029616L;
public RestWANoContentException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWANoContentException(String message, Object... args) {
super(204, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWANotFoundException extends RestWAException {
private static final long serialVersionUID = -8096938855779969881L;
public RestWANotFoundException() {
this("The specified resource was not found.", new Object[0]);
}
public RestWANotFoundException(String message, Object... args) {
super(404, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAPreconditionFailedException extends RestWAException {
private static final long serialVersionUID = 4445144621889651977L;
public RestWAPreconditionFailedException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAPreconditionFailedException(String message, Object... args) {
super(412, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAReqEntityTooLargeException extends RestWAException {
private static final long serialVersionUID = 8526782864122573210L;
public RestWAReqEntityTooLargeException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAReqEntityTooLargeException(String message, Object... args) {
super(413, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWARequestTimeoutException extends RestWAException {
private static final long serialVersionUID = -3140219541419534066L;
public RestWARequestTimeoutException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWARequestTimeoutException(String message, Object... args) {
super(408, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAServiceUnavailableException extends RestWAException {
private static final long serialVersionUID = 1692904038368979134L;
public RestWAServiceUnavailableException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAServiceUnavailableException(String message, Object... args) {
super(503, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAUnauthorizedException extends RestWAException {
private static final long serialVersionUID = -3140219541419534066L;
public RestWAUnauthorizedException() {
this("Unable to process the request due to an internal error.", new Object[0]);
}
public RestWAUnauthorizedException(String message, Object... args) {
super(401, "application/json; charset=utf-8", message, args);
}
}

View File

@@ -0,0 +1,14 @@
package dev.alexzaw.rest4i.exception;
public class RestWAUnauthorizedMustAuthenticateException extends RestWAException {
private static final long serialVersionUID = -4971596259925231796L;
public RestWAUnauthorizedMustAuthenticateException() {
this("User ID or password is not set or not valid.", new Object[0]);
}
public RestWAUnauthorizedMustAuthenticateException(String message, Object... args) {
super(401, "application/json; charset=utf-8", "WWW-Authenticate", "Bearer", message, args);
}
}

View File

@@ -0,0 +1,290 @@
package dev.alexzaw.rest4i.fileops;
import jcifs.CIFSContext;
import jcifs.context.SingletonContext;
import jcifs.smb.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* SMB File Manager for handling SMB/CIFS file operations.
* Provides file operations like list, read, upload, download, and zip.
*
* @author alexzaw
*/
public class SMBFileManager {
private static final ConcurrentMap<String, CIFSContext> AUTH_CONTEXTS = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public SMBFileManager() {
this.objectMapper = new ObjectMapper();
}
/**
* Creates or retrieves a CIFS authentication context for the given credentials.
*
* @param domain The domain name
* @param username The username
* @param password The password
* @return CIFSContext for authenticated operations
*/
public CIFSContext getAuthContext(String domain, String username, String password) {
String key = domain + ":" + username;
return AUTH_CONTEXTS.computeIfAbsent(key, k -> {
SingletonContext singletonContext = SingletonContext.getInstance();
return singletonContext.withCredentials(
new NtlmPasswordAuthentication(singletonContext, domain, username, password));
});
}
/**
* Lists files and directories in the specified SMB path.
*
* @param smbPath The SMB path to list
* @param auth The authentication context
* @return List of file information maps
* @throws IOException if listing fails
*/
public List<Map<String, String>> listFiles(String smbPath, CIFSContext auth) throws IOException {
List<Map<String, String>> fileList = new ArrayList<>();
String smbDir = smbPath.endsWith("/") ? smbPath : smbPath + "/";
String windowsPath = smbDir.replace("smb:", "").replace("/", "\\");
SmbFile directory = new SmbFile(smbDir, auth);
if (!directory.exists()) {
throw new FileNotFoundException("Directory does not exist: " + smbPath);
}
if (!directory.isDirectory()) {
throw new IOException("Path is not a directory: " + smbPath);
}
int fileCount = 0;
String[] urlParts = smbDir.split("/");
String smbShare = urlParts.length >= 4 ? urlParts[3] : "";
for (SmbFile file : directory.listFiles()) {
fileCount++;
String fileName = file.getName();
Map<String, String> fileInfo = new HashMap<>();
fileInfo.put("fullPath", windowsPath + fileName);
fileInfo.put("fileName", fileName);
fileInfo.put("folder", windowsPath);
fileInfo.put("share", smbShare);
fileInfo.put("srn", Integer.toString(fileCount));
fileInfo.put("isDirectory", String.valueOf(file.isDirectory()));
fileInfo.put("size", String.valueOf(file.length()));
fileInfo.put("lastModified", String.valueOf(file.getLastModified()));
fileList.add(fileInfo);
}
return fileList;
}
/**
* Reads a file from the SMB path and returns its content as InputStream.
*
* @param smbPath The SMB path to the file
* @param auth The authentication context
* @return InputStream containing file content
* @throws IOException if reading fails
*/
public SmbFileInputStream readFile(String smbPath, CIFSContext auth) throws IOException {
SmbFile smbFile = new SmbFile(smbPath, auth);
if (!smbFile.exists()) {
throw new FileNotFoundException("File does not exist: " + smbPath);
}
if (smbFile.isDirectory()) {
throw new IOException("Path is a directory, not a file: " + smbPath);
}
return new SmbFileInputStream(smbFile);
}
/**
* Uploads a file to the SMB path.
*
* @param smbPath The SMB path where to upload
* @param fileName The name of the file
* @param inputStream The input stream containing file content
* @param auth The authentication context
* @throws IOException if upload fails
*/
public void uploadFile(String smbPath, String fileName, InputStream inputStream, CIFSContext auth) throws IOException {
String smbDir = smbPath.endsWith("/") ? smbPath : smbPath + "/";
String fullPath = smbDir + fileName;
// Ensure directory exists
SmbFile directory = new SmbFile(smbDir, auth);
if (!directory.exists()) {
directory.mkdirs();
}
try (SmbFileOutputStream out = new SmbFileOutputStream(new SmbFile(fullPath, auth))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
/**
* Creates a directory at the specified SMB path.
*
* @param smbPath The SMB path for the new directory
* @param auth The authentication context
* @return true if created, false if already exists
* @throws IOException if creation fails
*/
public boolean createDirectory(String smbPath, CIFSContext auth) throws IOException {
SmbFile smbDir = new SmbFile(smbPath, auth);
if (smbDir.exists()) {
return false; // Already exists
}
smbDir.mkdirs();
return true;
}
/**
* Checks if a file or directory exists at the specified SMB path.
*
* @param smbPath The SMB path to check
* @param auth The authentication context
* @return true if exists, false otherwise
* @throws IOException if check fails
*/
public boolean exists(String smbPath, CIFSContext auth) throws IOException {
SmbFile smbFile = new SmbFile(smbPath, auth);
return smbFile.exists();
}
/**
* Creates a ZIP archive of all files in the specified directory.
*
* @param smbPath The SMB path to the directory
* @param outputStream The output stream to write the ZIP to
* @param auth The authentication context
* @throws IOException if zipping fails
*/
public void zipDirectory(String smbPath, OutputStream outputStream, CIFSContext auth) throws IOException {
String smbDir = smbPath.endsWith("/") ? smbPath : smbPath + "/";
SmbFile directory = new SmbFile(smbDir, auth);
if (!directory.exists()) {
throw new FileNotFoundException("Directory does not exist: " + smbPath);
}
if (!directory.isDirectory()) {
throw new IOException("Path is not a directory: " + smbPath);
}
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
SmbFile[] files = directory.listFiles();
for (SmbFile file : files) {
if (file.isDirectory()) {
continue; // Skip directories for now
}
String fileName = file.getName();
zos.putNextEntry(new ZipEntry(fileName));
try (InputStream fis = new SmbFileInputStream(file)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
zos.write(buffer, 0, bytesRead);
}
}
zos.closeEntry();
}
}
}
/**
* Builds a UNC path from a file path, handling drive mappings and server resolution.
*
* @param filePath The file path to convert
* @param serverMappings Properties containing server and drive mappings
* @return UNC SMB path
*/
public String buildUncPath(String filePath, Properties serverMappings) {
List<String> validDrives = Arrays.asList("F:", "G:", "L:", "K:", "S:", "P:", "M:", "R:");
String normalized = filePath
.replaceAll("[/\\\\]+", "|") // Replace any slashes/backslashes with |
.replaceAll("^\\|+", "");
List<String> filePartsList = arrayTrim(new ArrayList<>(Arrays.asList(normalized.split("\\|"))));
String[] fileParts = filePartsList.toArray(new String[0]);
String smbServer = fileParts[0];
String smbShare = "";
if (validDrives.contains(fileParts[0].substring(0, Math.min(2, fileParts[0].length())))) {
smbServer = "DEFAULT";
smbShare = serverMappings.getProperty("smb." + fileParts[0].substring(0, 1), "");
}
String server = getFQDN(smbServer, serverMappings);
String sanitizedPath = String.join("/", Arrays.copyOfRange(fileParts, 1, fileParts.length))
.replaceAll("/{2,}", "/");
String uncPath = "smb://" + String.format("%s/%s/%s", server, smbShare, sanitizedPath)
.replaceAll("/{2,}", "/");
return uncPath;
}
/**
* Gets the FQDN for a server name from the mappings.
*/
private String getFQDN(String server, Properties serverMappings) {
String inServer = server.toUpperCase();
List<String> validServers = Arrays.asList("MINICIRCUITS.COM", "MCL_NEW_YORK", "MCL_NEW_YORK.MINICIRCUITS.COM", "192.168.3.11", "DEFAULT");
if (!validServers.contains(inServer)) {
throw new RuntimeException("Unable to determine server: " + server);
}
String outServer = serverMappings.getProperty("smb.server", "localhost");
return outServer.toLowerCase();
}
/**
* Trims empty strings from a list.
*/
private List<String> arrayTrim(List<String> list) {
List<String> trimmedList = new ArrayList<>();
for (String s : list) {
if (!s.trim().isEmpty()) {
trimmedList.add(s.trim());
}
}
return trimmedList;
}
/**
* Decodes a Base64 encoded string.
*/
public String decodeBase64(String encodedString) {
return new String(Base64.getDecoder().decode(encodedString), StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,190 @@
package dev.alexzaw.rest4i.messages;
public class Messages {
public static final String REST_SERVICE_UNITIALIZED = "Service is not initialized.";
public static final String ERR_DEFAULT_MSG = "Unable to process the request due to an internal error.";
public static final String ERR_DEFAULT_MSGX = "Unable to process the request due to an internal error. %s";
public static final String ERR_BAD_PROPERTIES = "One or more properties in request is not valid or is missing.";
public static final String ERR_FORBIDDEN = "Not authorized to perform operation.";
public static final String ERR_NOT_FOUND = "The specified resource was not found.";
public static final String ERR_FUNCTION_DISABLED = "The API '%s' is currently not enabled.";
public static final String OP_NOT_SUPPORTED_PATH = "Operation on object referenced by the path '%s' is not supported.";
public static final String OP_NOT_SUPPORTED = "Operation not supported.";
public static final String NOT_NOW_ERROR = "Operation cannot be completed at this time.";
public static final String BAD_PARM_VALUE = "A parameter value is not valid.";
public static final String BAD_PARM_VALUE_LEN = "The length of a parameter value is not valid.";
public static final String BAD_PROP_VALUE = "The value for property '%s' is not valid.";
public static final String BAD_PARM_VALUE2 = "The value for parameter '%s' is not valid.";
public static final String BAD_PARM_VALUE_LEN2 = "The length of the value for parameter '%s' is not valid.";
public static final String BAD_PROP_USING_DEFAULT = "The value for property '%s' is not valid. Using default value of %s. ";
public static final String SAVE_CONFIG_FAILED = "Save of properties failed. %s";
public static final String EXCEPTION_MESSAGE = "Exception in %s";
public static final String REFLECTED_XSS_DETECTED = "Reflected cross site scripting vulnerabilities detected in input data.";
public static final String COMBOVALUES_NOT_SUPPORTED = "The combination of values specified is not supported.";
public static final String QSYS_OBJECT_NAME_MISSING = "Object name not set.";
public static final String OP_ON_PATH_FAILED = "Operation on object referenced by path '%s' failed. %s";
public static final String PATH_NAME_NOT_PF = "Path '%s' does not reference a physical file.";
public static final String PATH_NAME_NOT_FOUND = "Object referenced by the path '%s' is not found or inaccessible.";
public static final String PATH_NAME_NOT_VALID = "Path '%s' is not valid.";
public static final String PATH_NAME_REQUIRED = "Path name not set.";
public static final String SEARCH_TEXT_REQUIRED = "Path name and file name are required.";
public static final String PATH_NAME_AND_TYPE_REQUIRED = "Path name and type are required.";
public static final String UNABLE_TO_CREATE_PATH_NAME = "Unable to create the path name.";
public static final String PATH_NAME_AND_ENCODING_REQUIRED = "Path name and encoding configuration are required.";
public static final String PATH_NAME_AND_CONTENT_REQUIRED = "Path name and content are required.";
public static final String UNABLE_TO_UPLOAD_CONTENT_FOR_FILE = "Unable to upload the content for the file. File not found or inaccessible.";
public static final String UNABLE_TO_DELETE_PATH = "Unable to delete the path. File not found or permission is required.";
public static final String UNABLE_TO_RENAME_FILE = "Unable to rename the file.";
public static final String UNABLE_TO_COPY_FILE = "Unable to copy the file.";
public static final String ENCODING_IS_INVALID = "The specified encoding '%s' is not valid.";
public static final String ENCODING_SETTING_NOT_SUPPORTED = "Encoding setting is not supported.";
public static final String DELETE_FAILED = "Unable to delete object specified by path '%s'.";
public static final String BAD_FILE_SIZE = "File '%s' has size %d which exceeds maximum allowed file size of %d.";
public static final String TARGET_ALREADY_EXISTS = "Target path '%s' already exists or path is not valid.";
public static final String ETAG_CONFLICT = "Update failed. File referenced by path '%s' does not match client version.";
public static final String PATH_NOT_DIR = "Object referenced by the path '%s' is not a directory.";
public static final String UNABLE_CHECKSUM = "Unable to checksum file referenced by path '%s' with the error '%s'.";
public static final String NET_ACCEPT_ERROR_SSL = "Resource must be accessed with a secure connection.";
public static final String NET_CONNECT_ERROR_SSL = "Unable to connect to server over a secure communication channel. %s";
public static final String NET_CONNECT_ERROR = "Unable to connect to server. %s";
public static final String NET_UNKNOWN_HOST = "The host '%s' is not known.";
public static final String NET_CONNECTION_DROPPED = "The connection to server dropped unexpectedly. Retry operation.";
public static final String AUTH_CRED_INVALID = "User ID or password is not set or not valid.";
public static final String AUTH_HOST_INVALID = "Host name not set or server is not an IBM i server.";
public static final String AUTH_TOKEN_INVALID = "Authorization token not valid or may have expired.";
public static final String AUTH_TOKEN_MISSING = "Authorization token not found.";
public static final String AUTH_TOKEN_ORIGINATION_ERROR = "Requestor is not originator of the authorization token.";
public static final String AUTH_TOKEN_EXPIRED = "Authorization token has expired.";
public static final String AUTH_USER_NOTADMIN = "User not an administrator.";
public static final String AUTH_NOT_LOCALHOST = "Host connection is not set to localhost.";
public static final String CL_COMMAND_MISSING = "Invalid command. At least one CL command must be specified.";
public static final String CL_COMMAND_FAILED = "CL command failed. Command is [%s]. Error message(s): %s";
public static final String CL_COMMAND_BAD_NAME = "CL command not specified or not valid.";
public static final String CL_COMMAND_BAD_LIBRARY = "Library not specified or not valid.";
public static final String SQL_STATEMENT_MISSING = "Invalid SQL statement. At least one SQL statement must be specified.";
public static final String SQL_STATEMENT_FAIL = "Session initialization failed. The failed SQL statement is [%s]. Error message(s): %s";
public static final String ERROR_MAX_SESSIONS = "Maximum number of sessions has been reached.";
public static final String ERROR_MAX_SESSIONS_USER = "Maximum number of sessions per user has been reached.";
public static final String HOSTNAME_RESOLUTION_ERROR = "Unable to get canonical host name.";
public static final String SESSION_IN_USE = "Request timed out while waiting for the session to become available.";
public static final String REQUEST_INTERRUPTED = "Request interrupted while waiting for session to become available.";
public static final String CERT_STORE_NOT_FOUND = "Certificate store not found or inaccessible.";
public static final String CERT_KEYSTORE_NOT_VALID = "Certificate store not supported or is not a valid certificate store.";
public static final String CERT_DATA_NOT_VALID = "Certificate data not valid or not in the correct format. %s";
public static final String CERT_NOT_SUPPORTED = "Certificate not supported. %s";
public static final String GSKMSG_UNKNOWN = "Unknown error occurred while using internal DCM SPI.";
public static final String GSKMSG_NOSTASHFILE = "Stash file does not exist.";
public static final String GSKMSG_CERT_NOPRIVATEKEY = "One or more certificates does not have a private key";
public static final String GSKMSG_DBALREADYEXISTS = "Certificate store exists.";
public static final String GSKMSG_DBPASSWORD = "Certificate store password not valid. If this is an attempt to change the password, the new password must not match existing password.";
public static final String GSKMSG_DBTYPE = "Certificate store type not valid.";
public static final String GSKMSG_DBDUPKEY = "Duplicate alias in certificate store.";
public static final String GSKMSG_DBCREATE = "Certificate store failed to create.";
public static final String GSKMSG_CERT_NOTFOUND_ERROR = "Certificate not found.";
public static final String GSKMSG_FILEOPEN_ERROR = "Error occurred when attempting to open file.";
public static final String GSKMSG_CERT_DATA_ERROR = "Certificate data is not valid or is not in the correct format.";
public static final String GSKMSG_CERT_VALIDATION_FAILURE = "Certificate validation failed. %s";
public static final String GSKMSG_APPDEF_NOTFOUND = "Application definition not found.";
public static final String GSKMSG_PASSWORDNOT_VALID = "Password not valid.";
public static final String GSKMSG_CA_INUSE = "Requested operation failed because the certificate is being used.";
public static final String GSK_KEYSTORE_NAME_ERROR = "Keystore name is not valid.";
public static final String GSKMSG_PKCS12_PWD_ERROR = "PKCS12 certificate password not valid.";
public static final String GSKMSG_ALGO_ERROR = "Algorithm is not support or is not valid.";
public static final String GSK_CCA_DEFAULTMESSAGE = "Common Cryptographic Architecture Interface error. %s";
public static final String GSKMSG_INDEX = "Stash file error. %s";
public static final String GSKMSG_CERT_EXPORT_ERROR = "The export of certificate in the requested format is not supported.";
public static final String GSKMSG_CERT_IMPORT_ERROR = "The import of certificate in the specified format is not supported.";
}

View File

@@ -0,0 +1,305 @@
package dev.alexzaw.rest4i.model;
import java.time.Instant;
import java.util.Map;
/**
* Generic API response wrapper that provides a standardized response structure
* for all REST4i integration endpoints.
*
* This wrapper ensures consistent response format across all services while
* maintaining compatibility with existing IBM i RSE API patterns.
*
* @param <T> The type of data being returned
* @author alexzaw
*/
public class ApiResponse<T> {
private boolean success;
private T data;
private ErrorInfo error;
private PaginationInfo pagination;
private String message;
private String timestamp;
private Map<String, Object> metadata;
/**
* Default constructor for successful responses without data.
*/
public ApiResponse() {
this.success = true;
this.timestamp = Instant.now().toString();
}
/**
* Constructor for successful responses with data.
*
* @param data The response data
*/
public ApiResponse(T data) {
this();
this.data = data;
}
/**
* Constructor for successful responses with data and message.
*
* @param data The response data
* @param message A success message
*/
public ApiResponse(T data, String message) {
this(data);
this.message = message;
}
/**
* Constructor for error responses.
*
* @param error The error information
*/
public ApiResponse(ErrorInfo error) {
this.success = false;
this.error = error;
this.timestamp = Instant.now().toString();
}
/**
* Creates a successful response with data.
*
* @param <T> The type of data
* @param data The response data
* @return ApiResponse with success=true and the provided data
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(data);
}
/**
* Creates a successful response with data and message.
*
* @param <T> The type of data
* @param data The response data
* @param message A success message
* @return ApiResponse with success=true, data, and message
*/
public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(data, message);
}
/**
* Creates a successful response with just a message (no data).
*
* @param message A success message
* @return ApiResponse with success=true and the provided message
*/
public static ApiResponse<Void> success(String message) {
ApiResponse<Void> response = new ApiResponse<>();
response.setMessage(message);
return response;
}
/**
* Creates a successful paginated response.
*
* @param <T> The type of data
* @param data The response data
* @param pagination Pagination information
* @return ApiResponse with success=true, data, and pagination info
*/
public static <T> ApiResponse<T> success(T data, PaginationInfo pagination) {
ApiResponse<T> response = new ApiResponse<>(data);
response.setPagination(pagination);
return response;
}
/**
* Creates an error response.
*
* @param error The error information
* @return ApiResponse with success=false and error details
*/
public static <T> ApiResponse<T> error(ErrorInfo error) {
return new ApiResponse<>(error);
}
/**
* Creates an error response with simple error message.
*
* @param errorMessage The error message
* @return ApiResponse with success=false and error message
*/
public static <T> ApiResponse<T> error(String errorMessage) {
return new ApiResponse<>(new ErrorInfo("GENERAL_ERROR", errorMessage));
}
/**
* Creates an error response with error code and message.
*
* @param errorCode The error code
* @param errorMessage The error message
* @return ApiResponse with success=false and error details
*/
public static <T> ApiResponse<T> error(String errorCode, String errorMessage) {
return new ApiResponse<>(new ErrorInfo(errorCode, errorMessage));
}
/**
* Creates an error response from an exception.
*
* @param exception The exception
* @return ApiResponse with success=false and exception details
*/
public static <T> ApiResponse<T> error(Exception exception) {
return new ApiResponse<>(ErrorInfo.fromException(exception));
}
// Getters and Setters
/**
* Gets the success status.
*
* @return true if the operation was successful, false otherwise
*/
public boolean isSuccess() {
return success;
}
/**
* Sets the success status.
*
* @param success The success status
*/
public void setSuccess(boolean success) {
this.success = success;
}
/**
* Gets the response data.
*
* @return The response data
*/
public T getData() {
return data;
}
/**
* Sets the response data.
*
* @param data The response data
*/
public void setData(T data) {
this.data = data;
}
/**
* Gets the error information.
*
* @return The error information, null if no error
*/
public ErrorInfo getError() {
return error;
}
/**
* Sets the error information.
*
* @param error The error information
*/
public void setError(ErrorInfo error) {
this.error = error;
if (error != null) {
this.success = false;
}
}
/**
* Gets the pagination information.
*
* @return The pagination information, null if not paginated
*/
public PaginationInfo getPagination() {
return pagination;
}
/**
* Sets the pagination information.
*
* @param pagination The pagination information
*/
public void setPagination(PaginationInfo pagination) {
this.pagination = pagination;
}
/**
* Gets the response message.
*
* @return The response message
*/
public String getMessage() {
return message;
}
/**
* Sets the response message.
*
* @param message The response message
*/
public void setMessage(String message) {
this.message = message;
}
/**
* Gets the response timestamp.
*
* @return The ISO-8601 formatted timestamp
*/
public String getTimestamp() {
return timestamp;
}
/**
* Sets the response timestamp.
*
* @param timestamp The ISO-8601 formatted timestamp
*/
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
/**
* Gets additional metadata.
*
* @return The metadata map
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* Sets additional metadata.
*
* @param metadata The metadata map
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
/**
* Adds a metadata entry.
*
* @param key The metadata key
* @param value The metadata value
*/
public void addMetadata(String key, Object value) {
if (this.metadata == null) {
this.metadata = new java.util.HashMap<>();
}
this.metadata.put(key, value);
}
@Override
public String toString() {
return String.format("ApiResponse{success=%s, message='%s', hasData=%s, hasError=%s, timestamp='%s'}",
success, message, data != null, error != null, timestamp);
}
}

View File

@@ -0,0 +1,401 @@
package dev.alexzaw.rest4i.model;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
* Error information model for consistent error handling across all REST4i integration services.
*
* Provides structured error information including error codes, messages, details,
* and additional context to help with debugging and error resolution.
*
* @author alexzaw
*/
public class ErrorInfo {
private String code;
private String message;
private String details;
private String timestamp;
private String path;
private String method;
private int httpStatus;
private List<ValidationError> validationErrors;
private Map<String, Object> context;
private String stackTrace;
/**
* Default constructor.
*/
public ErrorInfo() {
this.timestamp = Instant.now().toString();
this.validationErrors = new ArrayList<>();
this.context = new HashMap<>();
}
/**
* Constructor with error code and message.
*
* @param code The error code
* @param message The error message
*/
public ErrorInfo(String code, String message) {
this();
this.code = code;
this.message = message;
}
/**
* Constructor with error code, message, and details.
*
* @param code The error code
* @param message The error message
* @param details Additional error details
*/
public ErrorInfo(String code, String message, String details) {
this(code, message);
this.details = details;
}
/**
* Constructor with error code, message, and HTTP status.
*
* @param code The error code
* @param message The error message
* @param httpStatus The HTTP status code
*/
public ErrorInfo(String code, String message, int httpStatus) {
this(code, message);
this.httpStatus = httpStatus;
}
/**
* Creates ErrorInfo from an Exception.
*
* @param exception The exception
* @return ErrorInfo instance
*/
public static ErrorInfo fromException(Exception exception) {
ErrorInfo errorInfo = new ErrorInfo();
// Set error code based on exception type
errorInfo.setCode(getErrorCodeFromException(exception));
errorInfo.setMessage(exception.getMessage() != null ? exception.getMessage() : "An error occurred");
// Set HTTP status based on exception type
errorInfo.setHttpStatus(getHttpStatusFromException(exception));
// Add stack trace for debugging (in development mode)
if (shouldIncludeStackTrace()) {
errorInfo.setStackTrace(getStackTraceString(exception));
}
// Add exception class as context
errorInfo.addContext("exceptionType", exception.getClass().getSimpleName());
return errorInfo;
}
/**
* Creates ErrorInfo for validation errors.
*
* @param message The main validation error message
* @param validationErrors List of specific validation errors
* @return ErrorInfo instance
*/
public static ErrorInfo validation(String message, List<ValidationError> validationErrors) {
ErrorInfo errorInfo = new ErrorInfo("VALIDATION_ERROR", message, 400);
errorInfo.setValidationErrors(validationErrors);
return errorInfo;
}
/**
* Creates ErrorInfo for authentication errors.
*
* @param message The authentication error message
* @return ErrorInfo instance
*/
public static ErrorInfo authentication(String message) {
return new ErrorInfo("AUTHENTICATION_ERROR", message, 401);
}
/**
* Creates ErrorInfo for authorization errors.
*
* @param message The authorization error message
* @return ErrorInfo instance
*/
public static ErrorInfo authorization(String message) {
return new ErrorInfo("AUTHORIZATION_ERROR", message, 403);
}
/**
* Creates ErrorInfo for resource not found errors.
*
* @param message The not found error message
* @return ErrorInfo instance
*/
public static ErrorInfo notFound(String message) {
return new ErrorInfo("RESOURCE_NOT_FOUND", message, 404);
}
/**
* Creates ErrorInfo for conflict errors.
*
* @param message The conflict error message
* @return ErrorInfo instance
*/
public static ErrorInfo conflict(String message) {
return new ErrorInfo("RESOURCE_CONFLICT", message, 409);
}
/**
* Creates ErrorInfo for internal server errors.
*
* @param message The server error message
* @return ErrorInfo instance
*/
public static ErrorInfo serverError(String message) {
return new ErrorInfo("INTERNAL_SERVER_ERROR", message, 500);
}
/**
* Creates ErrorInfo for database errors.
*
* @param message The database error message
* @param sqlState The SQL state if available
* @return ErrorInfo instance
*/
public static ErrorInfo database(String message, String sqlState) {
ErrorInfo errorInfo = new ErrorInfo("DATABASE_ERROR", message, 500);
if (sqlState != null) {
errorInfo.addContext("sqlState", sqlState);
}
return errorInfo;
}
/**
* Creates ErrorInfo for timeout errors.
*
* @param message The timeout error message
* @return ErrorInfo instance
*/
public static ErrorInfo timeout(String message) {
return new ErrorInfo("REQUEST_TIMEOUT", message, 408);
}
/**
* Gets the error code from exception type.
*/
private static String getErrorCodeFromException(Exception exception) {
String className = exception.getClass().getSimpleName();
if (className.contains("SQL")) {
return "DATABASE_ERROR";
} else if (className.contains("IO") || className.contains("FileNotFound")) {
return "IO_ERROR";
} else if (className.contains("Security") || className.contains("Auth")) {
return "SECURITY_ERROR";
} else if (className.contains("Timeout")) {
return "REQUEST_TIMEOUT";
} else if (className.contains("IllegalArgument") || className.contains("Invalid")) {
return "VALIDATION_ERROR";
} else {
return "GENERAL_ERROR";
}
}
/**
* Gets HTTP status code from exception type.
*/
private static int getHttpStatusFromException(Exception exception) {
String className = exception.getClass().getSimpleName();
if (className.contains("IllegalArgument") || className.contains("Invalid")) {
return 400; // Bad Request
} else if (className.contains("Security") || className.contains("Auth")) {
return 401; // Unauthorized
} else if (className.contains("FileNotFound") || className.contains("NotFound")) {
return 404; // Not Found
} else if (className.contains("Timeout")) {
return 408; // Request Timeout
} else {
return 500; // Internal Server Error
}
}
/**
* Determines if stack trace should be included.
*/
private static boolean shouldIncludeStackTrace() {
// Include stack trace in development mode or if specifically enabled
String includeStackTrace = System.getProperty("rest4i.error.includeStackTrace", "false");
return "true".equalsIgnoreCase(includeStackTrace);
}
/**
* Converts exception stack trace to string.
*/
private static String getStackTraceString(Exception exception) {
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
exception.printStackTrace(pw);
return sw.toString();
}
// Getters and Setters
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public int getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(int httpStatus) {
this.httpStatus = httpStatus;
}
public List<ValidationError> getValidationErrors() {
return validationErrors;
}
public void setValidationErrors(List<ValidationError> validationErrors) {
this.validationErrors = validationErrors != null ? validationErrors : new ArrayList<>();
}
public void addValidationError(String field, String message) {
this.validationErrors.add(new ValidationError(field, message));
}
public void addValidationError(ValidationError validationError) {
this.validationErrors.add(validationError);
}
public Map<String, Object> getContext() {
return context;
}
public void setContext(Map<String, Object> context) {
this.context = context != null ? context : new HashMap<>();
}
public void addContext(String key, Object value) {
this.context.put(key, value);
}
public String getStackTrace() {
return stackTrace;
}
public void setStackTrace(String stackTrace) {
this.stackTrace = stackTrace;
}
/**
* Validation error details.
*/
public static class ValidationError {
private String field;
private String message;
private Object rejectedValue;
public ValidationError() {}
public ValidationError(String field, String message) {
this.field = field;
this.message = message;
}
public ValidationError(String field, String message, Object rejectedValue) {
this.field = field;
this.message = message;
this.rejectedValue = rejectedValue;
}
// Getters and Setters
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getRejectedValue() {
return rejectedValue;
}
public void setRejectedValue(Object rejectedValue) {
this.rejectedValue = rejectedValue;
}
@Override
public String toString() {
return String.format("ValidationError{field='%s', message='%s', rejectedValue=%s}",
field, message, rejectedValue);
}
}
@Override
public String toString() {
return String.format("ErrorInfo{code='%s', message='%s', httpStatus=%d, timestamp='%s'}",
code, message, httpStatus, timestamp);
}
}

View File

@@ -0,0 +1,469 @@
package dev.alexzaw.rest4i.model;
import java.util.HashMap;
import java.util.Map;
/**
* Pagination information model for handling large datasets in REST4i integration services.
*
* Provides comprehensive pagination metadata including page numbers, sizes, totals,
* and navigation links to support efficient data retrieval and navigation.
*
* @author alexzaw
*/
public class PaginationInfo {
private int page;
private int size;
private int totalPages;
private long totalElements;
private boolean first;
private boolean last;
private boolean hasNext;
private boolean hasPrevious;
private int numberOfElements;
private boolean empty;
private SortInfo sort;
private Map<String, String> links;
/**
* Default constructor.
*/
public PaginationInfo() {
this.links = new HashMap<>();
}
/**
* Constructor with basic pagination parameters.
*
* @param page Current page number (0-based)
* @param size Number of elements per page
* @param totalElements Total number of elements across all pages
*/
public PaginationInfo(int page, int size, long totalElements) {
this();
this.page = page;
this.size = size;
this.totalElements = totalElements;
calculateDerivedFields();
}
/**
* Constructor with all pagination parameters.
*
* @param page Current page number (0-based)
* @param size Number of elements per page
* @param totalElements Total number of elements across all pages
* @param numberOfElements Number of elements in current page
*/
public PaginationInfo(int page, int size, long totalElements, int numberOfElements) {
this(page, size, totalElements);
this.numberOfElements = numberOfElements;
this.empty = numberOfElements == 0;
}
/**
* Creates pagination info from request parameters.
*
* @param page Current page number (0-based)
* @param size Number of elements per page
* @param totalElements Total number of elements
* @param numberOfElements Number of elements in current page
* @return PaginationInfo instance
*/
public static PaginationInfo of(int page, int size, long totalElements, int numberOfElements) {
return new PaginationInfo(page, size, totalElements, numberOfElements);
}
/**
* Creates pagination info from request parameters with sorting.
*
* @param page Current page number (0-based)
* @param size Number of elements per page
* @param totalElements Total number of elements
* @param numberOfElements Number of elements in current page
* @param sort Sorting information
* @return PaginationInfo instance
*/
public static PaginationInfo of(int page, int size, long totalElements, int numberOfElements, SortInfo sort) {
PaginationInfo pagination = new PaginationInfo(page, size, totalElements, numberOfElements);
pagination.setSort(sort);
return pagination;
}
/**
* Creates empty pagination info (no results).
*
* @param page Current page number
* @param size Number of elements per page
* @return PaginationInfo instance representing empty results
*/
public static PaginationInfo empty(int page, int size) {
PaginationInfo pagination = new PaginationInfo(page, size, 0);
pagination.setEmpty(true);
return pagination;
}
/**
* Calculates derived fields based on basic pagination parameters.
*/
private void calculateDerivedFields() {
this.totalPages = size > 0 ? (int) Math.ceil((double) totalElements / size) : 0;
this.first = page == 0;
this.last = page >= totalPages - 1 || totalPages == 0;
this.hasNext = !last && totalPages > 0;
this.hasPrevious = !first;
this.empty = totalElements == 0;
}
/**
* Generates navigation links for the pagination.
*
* @param baseUrl The base URL for generating links
* @param additionalParams Additional query parameters to include
*/
public void generateLinks(String baseUrl, Map<String, String> additionalParams) {
StringBuilder baseUrlBuilder = new StringBuilder(baseUrl);
// Add additional parameters to base URL
if (additionalParams != null && !additionalParams.isEmpty()) {
boolean firstParam = !baseUrl.contains("?");
for (Map.Entry<String, String> param : additionalParams.entrySet()) {
baseUrlBuilder.append(firstParam ? "?" : "&");
baseUrlBuilder.append(param.getKey()).append("=").append(param.getValue());
firstParam = false;
}
}
String urlBase = baseUrlBuilder.toString();
String separator = urlBase.contains("?") ? "&" : "?";
// Self link
links.put("self", String.format("%s%spage=%d&size=%d", urlBase, separator, page, size));
// First page link
if (totalPages > 0) {
links.put("first", String.format("%s%spage=0&size=%d", urlBase, separator, size));
}
// Last page link
if (totalPages > 0) {
links.put("last", String.format("%s%spage=%d&size=%d", urlBase, separator, totalPages - 1, size));
}
// Previous page link
if (hasPrevious) {
links.put("prev", String.format("%s%spage=%d&size=%d", urlBase, separator, page - 1, size));
}
// Next page link
if (hasNext) {
links.put("next", String.format("%s%spage=%d&size=%d", urlBase, separator, page + 1, size));
}
}
/**
* Adds a custom link.
*
* @param rel The link relation
* @param href The link URL
*/
public void addLink(String rel, String href) {
this.links.put(rel, href);
}
// Getters and Setters
/**
* Gets the current page number (0-based).
*
* @return The current page number
*/
public int getPage() {
return page;
}
/**
* Sets the current page number.
*
* @param page The current page number (0-based)
*/
public void setPage(int page) {
this.page = page;
calculateDerivedFields();
}
/**
* Gets the number of elements per page.
*
* @return The page size
*/
public int getSize() {
return size;
}
/**
* Sets the number of elements per page.
*
* @param size The page size
*/
public void setSize(int size) {
this.size = size;
calculateDerivedFields();
}
/**
* Gets the total number of pages.
*
* @return The total number of pages
*/
public int getTotalPages() {
return totalPages;
}
/**
* Gets the total number of elements across all pages.
*
* @return The total number of elements
*/
public long getTotalElements() {
return totalElements;
}
/**
* Sets the total number of elements.
*
* @param totalElements The total number of elements
*/
public void setTotalElements(long totalElements) {
this.totalElements = totalElements;
calculateDerivedFields();
}
/**
* Checks if this is the first page.
*
* @return true if this is the first page
*/
public boolean isFirst() {
return first;
}
/**
* Checks if this is the last page.
*
* @return true if this is the last page
*/
public boolean isLast() {
return last;
}
/**
* Checks if there is a next page.
*
* @return true if there is a next page
*/
public boolean isHasNext() {
return hasNext;
}
/**
* Checks if there is a previous page.
*
* @return true if there is a previous page
*/
public boolean isHasPrevious() {
return hasPrevious;
}
/**
* Gets the number of elements in the current page.
*
* @return The number of elements in current page
*/
public int getNumberOfElements() {
return numberOfElements;
}
/**
* Sets the number of elements in the current page.
*
* @param numberOfElements The number of elements in current page
*/
public void setNumberOfElements(int numberOfElements) {
this.numberOfElements = numberOfElements;
this.empty = numberOfElements == 0;
}
/**
* Checks if the current page is empty.
*
* @return true if the current page has no elements
*/
public boolean isEmpty() {
return empty;
}
/**
* Sets whether the current page is empty.
*
* @param empty true if the current page has no elements
*/
public void setEmpty(boolean empty) {
this.empty = empty;
}
/**
* Gets the sorting information.
*
* @return The sorting information
*/
public SortInfo getSort() {
return sort;
}
/**
* Sets the sorting information.
*
* @param sort The sorting information
*/
public void setSort(SortInfo sort) {
this.sort = sort;
}
/**
* Gets the navigation links.
*
* @return Map of navigation links
*/
public Map<String, String> getLinks() {
return links;
}
/**
* Sets the navigation links.
*
* @param links Map of navigation links
*/
public void setLinks(Map<String, String> links) {
this.links = links != null ? links : new HashMap<>();
}
/**
* Sorting information for paginated results.
*/
public static class SortInfo {
private boolean sorted;
private boolean unsorted;
private boolean empty;
private String property;
private String direction;
/**
* Default constructor for unsorted results.
*/
public SortInfo() {
this.sorted = false;
this.unsorted = true;
this.empty = true;
}
/**
* Constructor for sorted results.
*
* @param property The property to sort by
* @param direction The sort direction (ASC or DESC)
*/
public SortInfo(String property, String direction) {
this.property = property;
this.direction = direction != null ? direction.toUpperCase() : "ASC";
this.sorted = true;
this.unsorted = false;
this.empty = false;
}
/**
* Creates ascending sort info.
*
* @param property The property to sort by
* @return SortInfo for ascending sort
*/
public static SortInfo asc(String property) {
return new SortInfo(property, "ASC");
}
/**
* Creates descending sort info.
*
* @param property The property to sort by
* @return SortInfo for descending sort
*/
public static SortInfo desc(String property) {
return new SortInfo(property, "DESC");
}
/**
* Creates unsorted info.
*
* @return SortInfo for unsorted results
*/
public static SortInfo unsorted() {
return new SortInfo();
}
// Getters and Setters
public boolean isSorted() {
return sorted;
}
public void setSorted(boolean sorted) {
this.sorted = sorted;
}
public boolean isUnsorted() {
return unsorted;
}
public void setUnsorted(boolean unsorted) {
this.unsorted = unsorted;
}
public boolean isEmpty() {
return empty;
}
public void setEmpty(boolean empty) {
this.empty = empty;
}
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
public String getDirection() {
return direction;
}
public void setDirection(String direction) {
this.direction = direction;
}
@Override
public String toString() {
if (unsorted) {
return "UNSORTED";
}
return String.format("%s: %s", property, direction);
}
}
@Override
public String toString() {
return String.format("PaginationInfo{page=%d, size=%d, totalPages=%d, totalElements=%d, first=%s, last=%s}",
page, size, totalPages, totalElements, first, last);
}
}

View File

@@ -0,0 +1,350 @@
package dev.alexzaw.rest4i.pdf;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.util.Matrix;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* PDF Manager for handling PDF operations including conversion, merge, rotation, and unprotection.
*
* @author alexzaw
*/
public class PDFManager {
private static final Logger logger = Logger.getLogger(PDFManager.class.getName());
/**
* PDF Information container
*/
public static class PDFInfo {
private final PDDocument document;
private final int rotation;
private final boolean isLandscape;
public PDFInfo(PDDocument doc, int rotation, boolean isLandscape) {
this.document = doc;
this.rotation = rotation;
this.isLandscape = isLandscape;
}
public PDDocument getDocument() { return document; }
public int getRotation() { return rotation; }
public boolean isLandscape() { return isLandscape; }
}
/**
* Converts HTML content to PDF.
*
* @param htmlContent The HTML content to convert
* @param pageSize The page size (A4, A3, etc.)
* @param orientation The page orientation (portrait/landscape)
* @return ByteArrayOutputStream containing the PDF
* @throws IOException if conversion fails
*/
public ByteArrayOutputStream convertHtmlToPdf(String htmlContent, String pageSize, String orientation) throws IOException {
try {
Document doc = Jsoup.parse(htmlContent);
doc.outputSettings()
.syntax(Document.OutputSettings.Syntax.xml)
.escapeMode(org.jsoup.nodes.Entities.EscapeMode.xhtml);
// Convert to XHTML
String xhtml = doc.html();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfRendererBuilder builder = new PdfRendererBuilder();
float[] pageDimensions = getPageSize(pageSize, orientation);
builder.useDefaultPageSize(pageDimensions[0], pageDimensions[1], PdfRendererBuilder.PageSizeUnits.MM);
builder.withHtmlContent(xhtml, null);
builder.toStream(baos);
builder.run();
return baos;
} catch (Exception e) {
logger.log(Level.SEVERE, "Error converting HTML to PDF", e);
throw new IOException("Error converting HTML to PDF: " + e.getMessage(), e);
}
}
/**
* Merges multiple PDF documents into one.
*
* @param pdfInputStreams List of PDF input streams to merge
* @return ByteArrayOutputStream containing the merged PDF
* @throws IOException if merge fails
*/
public ByteArrayOutputStream mergePdfs(List<InputStream> pdfInputStreams) throws IOException {
File tempDir = null;
List<Integer> pageCounts = new ArrayList<>();
try {
tempDir = Files.createTempDirectory("pdf-merge-").toFile();
PDFMergerUtility pdfMerger = new PDFMergerUtility();
for (int i = 0; i < pdfInputStreams.size(); i++) {
InputStream pdfStream = pdfInputStreams.get(i);
// Create temp file for each input PDF
File tempInputFile = new File(tempDir, "input-" + i + ".pdf");
Files.copy(pdfStream, tempInputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// Analyze and process PDF
PDFInfo pdfInfo = analyzePDF(tempInputFile);
if (pdfInfo != null) {
PDDocument doc = pdfInfo.getDocument();
// Add page count to the list
pageCounts.add(doc.getNumberOfPages());
// Auto-rotate to landscape if needed
if (!pdfInfo.isLandscape()) {
PDPage firstPage = doc.getPage(0);
firstPage.setRotation((firstPage.getRotation() + 90) % 360);
// Save rotated document to new temp file
File rotatedFile = new File(tempDir, "rotated-" + i + ".pdf");
doc.save(rotatedFile);
pdfMerger.addSource(rotatedFile);
} else {
pdfMerger.addSource(tempInputFile);
}
doc.close();
} else {
// If analysis fails, use original file and count its pages
try (PDDocument doc = PDDocument.load(tempInputFile)) {
pageCounts.add(doc.getNumberOfPages());
}
pdfMerger.addSource(tempInputFile);
}
}
// Set up merged PDF output
File mergedFile = new File(tempDir, "merged.pdf");
pdfMerger.setDestinationFileName(mergedFile.getAbsolutePath());
pdfMerger.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
// Read merged file into byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Files.copy(mergedFile.toPath(), baos);
return baos;
} finally {
// Clean up temp files
if (tempDir != null) {
deleteDirectory(tempDir);
}
}
}
/**
* Rotates pages in a PDF document.
*
* @param pdfInputStream The PDF input stream
* @param rotation The rotation angle (90, 180, 270, 360/0)
* @return ByteArrayOutputStream containing the rotated PDF
* @throws IOException if rotation fails
*/
public ByteArrayOutputStream rotatePdf(InputStream pdfInputStream, int rotation) throws IOException {
try (PDDocument doc = PDDocument.load(pdfInputStream)) {
// Apply rotation to all pages
for (PDPage page : doc.getPages()) {
page.setRotation(rotation);
}
// Return rotated PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
return baos;
}
}
/**
* Removes protection from a PDF by converting it to images and back to PDF.
*
* @param pdfInputStream The protected PDF input stream
* @return ByteArrayOutputStream containing the unprotected PDF
* @throws IOException if unprotection fails
*/
public ByteArrayOutputStream unprotectPdf(InputStream pdfInputStream) throws IOException {
try (PDDocument document = PDDocument.load(pdfInputStream)) {
PDDocument newDocument = convertToUnprotected(document);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
return baos;
}
}
/**
* Analyzes a PDF file and returns information about its properties.
*
* @param pdfFile The PDF file to analyze
* @return PDFInfo containing document properties
*/
private PDFInfo analyzePDF(File pdfFile) {
try {
PDDocument document = PDDocument.load(pdfFile);
PDPage firstPage = document.getPage(0);
int rotation = firstPage.getRotation();
// Get page dimensions
PDRectangle mediaBox = firstPage.getMediaBox();
float width = mediaBox.getWidth();
float height = mediaBox.getHeight();
// Consider rotation when determining landscape
boolean isRotated = (rotation == 90 || rotation == 270);
if (isRotated) {
float temp = width;
width = height;
height = temp;
}
boolean isLandscape = width > height;
return new PDFInfo(document, rotation, isLandscape);
} catch (IOException e) {
logger.log(Level.WARNING, "Error analyzing PDF file", e);
return null;
}
}
/**
* Converts a protected PDF to unprotected by rendering pages as images.
*
* @param document The protected PDF document
* @return New unprotected PDF document
* @throws IOException if conversion fails
*/
private PDDocument convertToUnprotected(PDDocument document) throws IOException {
PDDocument newDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
// Pre-calculate total pages for better memory management
int totalPages = document.getNumberOfPages();
for (int page = 0; page < totalPages; page++) {
// Get original page dimensions
PDPage originalPage = document.getPage(page);
PDRectangle originalMediaBox = originalPage.getMediaBox();
BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300);
// Create new page with original dimensions
PDPage newPage = new PDPage(new PDRectangle(
originalMediaBox.getWidth(),
originalMediaBox.getHeight()
));
newDocument.addPage(newPage);
try (PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage)) {
PDImageXObject pdImage = LosslessFactory.createFromImage(newDocument, image);
// Scale image to fit original dimensions
contentStream.transform(new Matrix(
originalMediaBox.getWidth() / image.getWidth(),
0, 0,
originalMediaBox.getHeight() / image.getHeight(),
0, 0
));
contentStream.drawImage(pdImage, 0, 0);
}
// Help GC by clearing the image
image.flush();
}
return newDocument;
}
/**
* Gets page dimensions based on size and orientation.
*
* @param size The page size (A4, A3, etc.)
* @param orientation The orientation (portrait/landscape)
* @return Array with [width, height] in millimeters
*/
private float[] getPageSize(String size, String orientation) {
float width = 210; // Default to A4 width
float height = 297; // Default to A4 height
if (size != null) {
switch (size.toUpperCase()) {
case "A1":
width = 594;
height = 841;
break;
case "A2":
width = 420;
height = 594;
break;
case "A3":
width = 297;
height = 420;
break;
case "A5":
width = 148;
height = 210;
break;
case "A6":
width = 105;
height = 148;
break;
case "A7":
width = 74;
height = 105;
break;
// A4 is already set as default
}
}
return "landscape".equalsIgnoreCase(orientation) ?
new float[] { height, width } : new float[] { width, height };
}
/**
* Recursively deletes a directory and all its contents.
*
* @param directory The directory to delete
*/
private void deleteDirectory(File directory) {
if (directory.exists()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
}
directory.delete();
}
}

View File

@@ -0,0 +1,466 @@
package dev.alexzaw.rest4i.pool;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400ConnectionPool;
import com.ibm.as400.access.AS400JDBCConnection;
import com.ibm.as400.access.AS400JDBCDriver;
import com.ibm.as400.access.SecureAS400;
import dev.alexzaw.rest4i.session.Session;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.config.EnhancedConfigurationManager;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AS400 Connection Pool Service that manages connection pools for IBM i systems.
*
* This service provides connection pooling using IBM's AS400ConnectionPool
* with configurable pool parameters from properties. It integrates with the
* existing Session class to maintain compatibility with RSE session authentication.
*
* Features:
* - Connection pooling with configurable parameters
* - Integration with existing Session authentication
* - Hot-reloadable configuration via EnhancedConfigurationManager
* - Thread-safe operations with concurrent access support
* - Automatic pool cleanup and resource management
*
* @author alexzaw
*/
public class AS400ConnectionPoolService {
private static final Logger logger = Logger.getLogger(AS400ConnectionPoolService.class.getName());
// Singleton instance
private static volatile AS400ConnectionPoolService instance;
private static final Object INSTANCE_LOCK = new Object();
// Connection pools keyed by host+user combination
private final Map<String, AS400ConnectionPool> connectionPools = new ConcurrentHashMap<>();
private final Map<String, AS400JDBCDriver> jdbcDrivers = new ConcurrentHashMap<>();
// Configuration manager
private final EnhancedConfigurationManager configManager;
// Default pool configuration
private static final int DEFAULT_MAX_CONNECTIONS = 20;
private static final int DEFAULT_MAX_INACTIVITY_TIME = 300000; // 5 minutes in milliseconds
private static final int DEFAULT_MAX_LIFETIME = 1800000; // 30 minutes in milliseconds
private static final int DEFAULT_MAX_USE_COUNT = 1000;
private static final boolean DEFAULT_PRETEST_CONNECTIONS = true;
private static final boolean DEFAULT_RUN_MAINTENANCE = true;
/**
* Private constructor for singleton pattern.
*/
private AS400ConnectionPoolService() {
this.configManager = EnhancedConfigurationManager.getInstance();
}
/**
* Gets the singleton instance of AS400ConnectionPoolService.
*
* @return The singleton instance
*/
public static AS400ConnectionPoolService getInstance() {
if (instance == null) {
synchronized (INSTANCE_LOCK) {
if (instance == null) {
instance = new AS400ConnectionPoolService();
}
}
}
return instance;
}
/**
* Gets a connection from the pool for the specified session.
*
* @param session The authenticated session
* @return An AS400 connection from the pool
* @throws Exception If unable to get a connection
*/
public AS400 getConnection(Session session) throws Exception {
if (session == null) {
throw new IllegalArgumentException("Session cannot be null");
}
String poolKey = createPoolKey(session.getHost(), session.getUserid());
AS400ConnectionPool pool = getOrCreateConnectionPool(session);
logger.fine("Getting AS400 connection from pool: " + poolKey);
return pool.getConnection(session.getHost(), session.getUserid());
}
/**
* Gets a JDBC connection from the pool for the specified session.
*
* @param session The authenticated session
* @return A JDBC connection from the pool
* @throws SQLException If unable to get a JDBC connection
* @throws Exception If unable to get the AS400 connection
*/
public Connection getJDBCConnection(Session session) throws Exception {
AS400 as400 = getConnection(session);
String poolKey = createPoolKey(session.getHost(), session.getUserid());
AS400JDBCDriver driver = getOrCreateJDBCDriver(poolKey);
// Get JDBC properties from session settings
Properties jdbcProperties = getJDBCProperties(session);
logger.fine("Getting JDBC connection from pool: " + poolKey);
return driver.connect(as400, jdbcProperties, null, false);
}
/**
* Returns a connection to the pool.
*
* @param connection The AS400 connection to return
*/
public void returnConnection(AS400 connection) {
if (connection == null) {
return;
}
try {
String poolKey = createPoolKey(connection.getSystemName(), connection.getUserId());
AS400ConnectionPool pool = connectionPools.get(poolKey);
if (pool != null) {
logger.fine("Returning AS400 connection to pool: " + poolKey);
pool.returnConnectionToPool(connection);
} else {
logger.warning("No pool found for connection: " + poolKey);
connection.resetAllServices();
}
} catch (Exception e) {
logger.log(Level.WARNING, "Error returning connection to pool", e);
try {
connection.resetAllServices();
} catch (Exception ex) {
logger.log(Level.WARNING, "Error resetting connection services", ex);
}
}
}
/**
* Returns a JDBC connection to the pool by closing it properly.
*
* @param connection The JDBC connection to return
*/
public void returnJDBCConnection(Connection connection) {
if (connection != null) {
try {
logger.fine("Closing JDBC connection");
connection.close();
} catch (SQLException e) {
logger.log(Level.WARNING, "Error closing JDBC connection", e);
}
}
}
/**
* Gets or creates a connection pool for the specified session.
*
* @param session The authenticated session
* @return The connection pool
* @throws Exception If unable to create the pool
*/
private AS400ConnectionPool getOrCreateConnectionPool(Session session) throws Exception {
String poolKey = createPoolKey(session.getHost(), session.getUserid());
return connectionPools.computeIfAbsent(poolKey, key -> {
try {
logger.info("Creating new AS400 connection pool: " + key);
return createConnectionPool(session.getHost(), session.getUserid());
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to create connection pool: " + key, e);
throw new RuntimeException(e);
}
});
}
/**
* Creates a new connection pool with configured parameters.
*
* @param host The IBM i host
* @param userId The user ID
* @return The configured connection pool
* @throws Exception If unable to create the pool
*/
private AS400ConnectionPool createConnectionPool(String host, String userId) throws Exception {
AS400ConnectionPool pool = new AS400ConnectionPool();
// Configure pool parameters from configuration
int maxConnections = configManager.getInt("connection.pool.maxConnections", DEFAULT_MAX_CONNECTIONS);
int maxInactivityTime = configManager.getInt("connection.pool.maxInactivityTime", DEFAULT_MAX_INACTIVITY_TIME);
int maxLifetime = configManager.getInt("connection.pool.maxLifetime", DEFAULT_MAX_LIFETIME);
int maxUseCount = configManager.getInt("connection.pool.maxUseCount", DEFAULT_MAX_USE_COUNT);
boolean pretestConnections = configManager.getBoolean("connection.pool.pretestConnections", DEFAULT_PRETEST_CONNECTIONS);
boolean runMaintenance = configManager.getBoolean("connection.pool.runMaintenance", DEFAULT_RUN_MAINTENANCE);
pool.setMaxConnections(maxConnections);
pool.setMaxInactivity(maxInactivityTime);
pool.setMaxLifetime(maxLifetime);
pool.setMaxUseCount(maxUseCount);
pool.setPretestConnections(pretestConnections);
pool.setRunMaintenance(runMaintenance);
// Configure cleanup interval if specified
int cleanupInterval = configManager.getInt("connection.pool.cleanupInterval", -1);
if (cleanupInterval > 0) {
pool.setCleanupInterval(cleanupInterval);
}
logger.info(String.format("Connection pool created for %s@%s with maxConnections=%d, maxInactivityTime=%d",
userId, host, maxConnections, maxInactivityTime));
return pool;
}
/**
* Gets or creates a JDBC driver for the specified pool.
*
* @param poolKey The pool key
* @return The JDBC driver
*/
private AS400JDBCDriver getOrCreateJDBCDriver(String poolKey) {
return jdbcDrivers.computeIfAbsent(poolKey, key -> {
logger.fine("Creating new AS400 JDBC driver: " + key);
return new AS400JDBCDriver();
});
}
/**
* Creates a pool key from host and user ID.
*
* @param host The host
* @param userId The user ID
* @return The pool key
*/
private String createPoolKey(String host, String userId) {
return String.format("%s@%s", userId != null ? userId.toUpperCase() : "NULL",
host != null ? host.toLowerCase() : "localhost");
}
/**
* Gets JDBC properties for the session, combining defaults with session-specific settings.
*
* @param session The session
* @return JDBC properties
*/
private Properties getJDBCProperties(Session session) {
Properties props = new Properties();
// Start with default JDBC properties from Session class
for (Map.Entry<String, String> entry : Session.DEFAULT_JDBCPROPERTIES.entrySet()) {
props.setProperty(entry.getKey(), entry.getValue());
}
// Add configuration-based overrides
Map<String, Object> jdbcConfig = configManager.getPropertiesWithPrefix("jdbc");
for (Map.Entry<String, Object> entry : jdbcConfig.entrySet()) {
props.setProperty(entry.getKey(), entry.getValue().toString());
}
// Add session-specific properties if available
try {
// Use reflection to get settings since it's private
// In a production environment, this should be done through proper Session API methods
java.lang.reflect.Field settingsField = Session.class.getDeclaredField("settings_");
settingsField.setAccessible(true);
Object settings = settingsField.get(session);
if (settings != null) {
java.lang.reflect.Field sqlPropsField = settings.getClass().getDeclaredField("sqlProperties");
sqlPropsField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, String> sqlProps = (Map<String, String>) sqlPropsField.get(settings);
if (sqlProps != null) {
for (Map.Entry<String, String> entry : sqlProps.entrySet()) {
props.setProperty(entry.getKey(), entry.getValue());
}
}
}
} catch (Exception e) {
logger.fine("Could not access session SQL properties: " + e.getMessage());
}
return props;
}
/**
* Gets pool statistics for monitoring and debugging.
*
* @param host The host to get statistics for (null for all pools)
* @param userId The user ID to get statistics for (null for all pools)
* @return Pool statistics as a map
*/
public Map<String, Object> getPoolStatistics(String host, String userId) {
Map<String, Object> statistics = new ConcurrentHashMap<>();
if (host != null && userId != null) {
// Get statistics for specific pool
String poolKey = createPoolKey(host, userId);
AS400ConnectionPool pool = connectionPools.get(poolKey);
if (pool != null) {
statistics.put(poolKey, getPoolStatistics(pool, host, userId));
}
} else {
// Get statistics for all pools - extract host/userId from poolKey
for (Map.Entry<String, AS400ConnectionPool> entry : connectionPools.entrySet()) {
String poolKey = entry.getKey();
// Parse poolKey to extract host and userId (format: "USER@host")
String[] parts = poolKey.split("@");
if (parts.length == 2) {
String poolUserId = parts[0];
String poolHost = parts[1];
statistics.put(poolKey, getPoolStatistics(entry.getValue(), poolHost, poolUserId));
} else {
// Fallback for malformed keys - create basic statistics
Map<String, Object> basicStats = new ConcurrentHashMap<>();
AS400ConnectionPool pool = entry.getValue();
basicStats.put("maxConnections", pool.getMaxConnections());
basicStats.put("maxInactivity", pool.getMaxInactivity());
basicStats.put("poolKey", poolKey);
statistics.put(poolKey, basicStats);
}
}
}
return statistics;
}
/**
* Gets statistics for a specific pool.
*
* @param pool The connection pool
* @param host The host for pool-specific statistics
* @param userId The user ID for pool-specific statistics
* @return Pool statistics
*/
private Map<String, Object> getPoolStatistics(AS400ConnectionPool pool, String host, String userId) {
Map<String, Object> stats = new ConcurrentHashMap<>();
// Use available methods that require host and userId parameters
stats.put("activeConnections", pool.getActiveConnectionCount(host, userId));
stats.put("availableConnections", pool.getAvailableConnectionCount(host, userId));
stats.put("maxConnections", pool.getMaxConnections());
stats.put("maxInactivity", pool.getMaxInactivity());
stats.put("maxLifetime", pool.getMaxLifetime());
stats.put("maxUseCount", pool.getMaxUseCount());
stats.put("maxUseTime", pool.getMaxUseTime());
stats.put("cleanupInterval", pool.getCleanupInterval());
stats.put("pretestConnections", pool.isPretestConnections());
stats.put("runMaintenance", pool.isRunMaintenance());
// Get system and user information
String[] systemNames = pool.getSystemNames();
if (systemNames != null) {
stats.put("systemNames", java.util.Arrays.asList(systemNames));
}
String[] users = pool.getUsers(host);
if (users != null) {
stats.put("users", java.util.Arrays.asList(users));
}
String[] connectedUsers = pool.getConnectedUsers(host);
if (connectedUsers != null) {
stats.put("connectedUsers", java.util.Arrays.asList(connectedUsers));
}
return stats;
}
/**
* Cleans up a specific connection pool.
*
* @param host The host
* @param userId The user ID
*/
public void cleanupPool(String host, String userId) {
String poolKey = createPoolKey(host, userId);
AS400ConnectionPool pool = connectionPools.remove(poolKey);
if (pool != null) {
logger.info("Cleaning up connection pool: " + poolKey);
try {
pool.close();
} catch (Exception e) {
logger.log(Level.WARNING, "Error closing connection pool: " + poolKey, e);
}
}
AS400JDBCDriver driver = jdbcDrivers.remove(poolKey);
if (driver != null) {
logger.fine("Removed JDBC driver: " + poolKey);
}
}
/**
* Shuts down all connection pools and releases resources.
*/
public void shutdown() {
logger.info("Shutting down AS400 Connection Pool Service");
// Close all connection pools
for (Map.Entry<String, AS400ConnectionPool> entry : connectionPools.entrySet()) {
try {
logger.info("Closing connection pool: " + entry.getKey());
entry.getValue().close();
} catch (Exception e) {
logger.log(Level.WARNING, "Error closing connection pool: " + entry.getKey(), e);
}
}
connectionPools.clear();
jdbcDrivers.clear();
logger.info("AS400 Connection Pool Service shut down completed");
}
/**
* Forces maintenance cleanup of idle connections across all pools.
* Note: Since cleanupConnections() is not public in AS400ConnectionPool,
* we trigger maintenance instead which performs similar cleanup operations.
*/
public void forceCleanup() {
logger.info("Forcing maintenance cleanup of idle connections");
for (Map.Entry<String, AS400ConnectionPool> entry : connectionPools.entrySet()) {
try {
// Use reflection to call the package-private cleanupConnections method
// This is a workaround since the method is not public
java.lang.reflect.Method cleanupMethod = AS400ConnectionPool.class.getDeclaredMethod("cleanupConnections");
cleanupMethod.setAccessible(true);
cleanupMethod.invoke(entry.getValue());
logger.fine("Cleaned up connections for pool: " + entry.getKey());
} catch (Exception e) {
logger.log(Level.WARNING, "Error during cleanup for pool: " + entry.getKey() + ". This is expected if security manager restricts reflection.", e);
// Fallback: try to trigger maintenance by setting run maintenance temporarily
try {
AS400ConnectionPool pool = entry.getValue();
boolean originalMaintenance = pool.isRunMaintenance();
if (!originalMaintenance) {
pool.setRunMaintenance(true);
// Give some time for maintenance to run
Thread.sleep(100);
pool.setRunMaintenance(originalMaintenance);
}
} catch (Exception fallbackError) {
logger.log(Level.FINE, "Fallback maintenance trigger also failed for pool: " + entry.getKey(), fallbackError);
}
}
}
}
}

View File

@@ -0,0 +1,527 @@
package dev.alexzaw.rest4i.session;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400JDBCConnection;
import com.ibm.as400.access.AS400JDBCDriver;
import com.ibm.as400.access.AS400Message;
import com.ibm.as400.access.CharConverter;
import com.ibm.as400.access.CommandCall;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.SecureAS400;
import com.ibm.as400.access.ServiceProgramCall;
import dev.alexzaw.rest4i.api.CLCommandAPIImpl;
import dev.alexzaw.rest4i.api.SQLAPIImpl;
import dev.alexzaw.rest4i.api.SessionAPIImpl;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.exception.RestWARequestTimeoutException;
import dev.alexzaw.rest4i.exception.RestWAUnauthorizedException;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import dev.alexzaw.rest4i.util.JSONSerializer;
import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Session {
public static final Map<String, String> DEFAULT_JDBCPROPERTIES;
static {
DEFAULT_JDBCPROPERTIES = (Map<String, String>)Stream.<String[]>of(new String[][] {
{ "access", "all" }, { "auto commit", "true" }, { "autocommit exception", "false" }, { "bidi implicit reordering", "true" }, { "bidi numeric ordering", "false" }, { "bidi string type", "5" }, { "big decimal", "true" }, { "block criteria", "2" }, { "block size", "32" }, { "character truncation", "true" },
{ "concurrent access resolution", "2" }, { "cursor hold", "true" }, { "cursor sensitivity", "asensitive" }, { "data compression", "true" }, { "data truncation", "true" }, { "database name", "" }, { "date format", "iso" }, { "date separator", "" }, { "decfloat rounding mode", "half even" }, { "decimal separator", "" },
{ "driver", "toolbox" }, { "errors", "basic" }, { "extended dynamic", "false" }, { "extended metadata", "false" }, { "full open", "false" }, { "hold input locators", "true" }, { "hold statements", "false" }, { "ignore warnings", "01003,0100C,01567" }, { "lazy close", "false" }, { "libraries", "*LIBL" },
{ "lob threshold", "32768" }, { "maximum blocked input rows", "32000" }, { "maximum precision", "31" }, { "maximum scale", "31" }, { "metadata source", "1" }, { "minimum divide scale", "0" }, { "naming", "system" }, { "numeric range error", "true" }, { "package", "" }, { "package add", "true" },
{ "package cache", "false" }, { "package ccsid", "13488" }, { "package criteria", "default" }, { "package error", "warning" }, { "package library", "QGPL" }, { "portNumber", "0" }, { "prefetch", "true" }, { "proxy server", "" }, { "qaqqinilib", "" }, { "query optimize goal", "0" },
{ "query replace truncated parameter", "" }, { "query storage limit", "-1" }, { "query timeout mechanism", "qqrytimlmt" }, { "remarks", "system" }, { "secondary URL", "" }, { "server trace", "0" }, { "sort", "hex" }, { "sort language", "" }, { "sort table", "" }, { "sort weight", "shared" },
{ "time format", "iso" }, { "time separator", ":" }, { "trace", "false" }, { "transaction isolation", "read uncommitted" }, { "translate binary", "false" }, { "translate boolean", "true" }, { "translate hex", "character" }, { "true autocommit", "false" }, { "use block update", "false" }, { "variable field compression", "all" },
{ "XA loosely coupled support", "0" } }).collect(Collectors.toMap(data -> data[0], data -> data[1]));
}
protected static AS400JDBCDriver jdbcDriver_ = null;
private AS400JDBCConnection dbconn_ = null;
private String domain_ = "rseapi";
private long creation_ = 0L;
private long lastused_ = 0L;
private long useCount_ = 0L;
private String userid_ = null;
private String host_ = null;
private AS400 as400_ = null;
private String token_ = null;
private boolean isExpired_ = false;
private SessionAPIImpl.RSEAPI_SessionSettings settings_ = null;
private ReentrantLock lock_ = new ReentrantLock();
private boolean singleUse_ = false;
public Session(String host, String userid, char[] password) throws Exception {
this.as400_ = getAS400Connection(host, userid, password);
initializeAS400(false, this.settings_);
this.host_ = host;
this.userid_ = userid;
this.creation_ = Instant.now().getEpochSecond();
this.lastused_ = this.creation_;
this.token_ = String.valueOf(UUID.randomUUID().toString()) +
"-" + Long.toHexString(this.creation_) +
"-" + CommonUtil.stringToHex(CommonUtil.getRemoteAddr());
this.settings_ = new SessionAPIImpl.RSEAPI_SessionSettings();
this.settings_.continueOnError = Boolean.valueOf(false);
this.settings_.sqlTreatWarningsAsErrors = Boolean.valueOf(false);
}
public SessionAPIImpl.SetSessionSettingsResults setSettings(SessionAPIImpl.RSEAPI_SessionSettings newSettings) throws Exception {
SessionAPIImpl.SetSessionSettingsResults sr = null;
if (newSettings == null)
return sr;
try {
sanitize(newSettings);
if (newSettings.resetSettings.booleanValue())
initializeAS400(true, null);
sr = applySessionSettings(newSettings);
merge(newSettings, sr);
} catch (Exception e) {
try {
initializeAS400(true, this.settings_);
} catch (Exception IGNORE) {
e.printStackTrace();
}
throw e;
}
return sr;
}
private void sanitize(SessionAPIImpl.RSEAPI_SessionSettings newSettings) {
if (newSettings.resetSettings == null)
newSettings.resetSettings = Boolean.valueOf(false);
if (newSettings.libraryList != null) {
newSettings.libraryList.removeIf(x -> !(x != null && CommonUtil.hasContent(x)));
List<String> listWithoutDuplicates = (List<String>)newSettings.libraryList.stream()
.distinct()
.collect(Collectors.toList());
newSettings.libraryList = listWithoutDuplicates;
}
if (newSettings.clCommands != null)
newSettings.clCommands.removeIf(x -> !(x != null && CommonUtil.hasContent(x)));
if (newSettings.sqlProperties != null) {
newSettings.sqlProperties.remove("password");
newSettings.sqlProperties.remove("prompt");
newSettings.sqlProperties.remove("user");
}
if (!newSettings.resetSettings.booleanValue() && this.settings_.sqlProperties != null && !this.settings_.sqlProperties.isEmpty()) {
HashMap<String, String> temp = new HashMap<>();
temp.putAll(this.settings_.sqlProperties);
if (newSettings.sqlProperties != null)
temp.putAll(newSettings.sqlProperties);
newSettings.sqlProperties = temp;
}
if (newSettings.sqlProperties != null && newSettings.sqlProperties.isEmpty())
newSettings.sqlProperties = null;
if (!CommonUtil.hasContent(newSettings.sqlDefaultSchema))
newSettings.sqlDefaultSchema = null;
if (newSettings.sqlTreatWarningsAsErrors == null)
newSettings.sqlTreatWarningsAsErrors = Boolean.valueOf(false);
if (newSettings.continueOnError == null)
newSettings.continueOnError = Boolean.valueOf(false);
}
private void merge(SessionAPIImpl.RSEAPI_SessionSettings newSettings, SessionAPIImpl.SetSessionSettingsResults sr) {
if (newSettings.resetSettings.booleanValue()) {
this.settings_.libraryList = newSettings.libraryList;
this.settings_.clCommands = newSettings.clCommands;
this.settings_.envVariables = newSettings.envVariables;
if (sr == null || !sr.sqlInitializationErrors())
this.settings_.sqlStatements = newSettings.sqlStatements;
} else {
if (this.settings_.libraryList == null) {
this.settings_.libraryList = newSettings.libraryList;
} else {
List<String> newList = (List<String>)newSettings.libraryList.stream()
.filter(n -> !this.settings_.libraryList.contains(n))
.collect(Collectors.toList());
this.settings_.libraryList.addAll(newList);
}
if (this.settings_.clCommands == null) {
this.settings_.clCommands = newSettings.clCommands;
} else {
this.settings_.clCommands.addAll(newSettings.clCommands);
}
if (this.settings_.envVariables == null) {
this.settings_.envVariables = newSettings.envVariables;
} else {
this.settings_.envVariables.putAll(newSettings.envVariables);
}
if (sr == null || !sr.sqlInitializationErrors())
if (this.settings_.sqlStatements == null) {
this.settings_.sqlStatements = newSettings.sqlStatements;
} else {
this.settings_.sqlStatements.addAll(newSettings.sqlStatements);
}
}
this.settings_.continueOnError = newSettings.continueOnError;
this.settings_.sqlTreatWarningsAsErrors = newSettings.sqlTreatWarningsAsErrors;
if (sr == null || !sr.sqlInitializationErrors()) {
this.settings_.sqlDefaultSchema = newSettings.sqlDefaultSchema;
this.settings_.sqlProperties = newSettings.sqlProperties;
}
}
public long getCreation() {
return this.creation_;
}
public void setCreation(long creation) {
this.creation_ = creation;
}
public void setSingleUse(boolean b) {
this.singleUse_ = b;
}
public boolean getSingleUse() {
return this.singleUse_;
}
public long getLastUsed() {
return this.lastused_;
}
public synchronized void setLastUsed() {
if (isExpired())
throw new RestWAUnauthorizedException("Authorization token has expired.", new Object[0]);
this.lastused_ = Instant.now().getEpochSecond();
this.useCount_++;
}
public long getUseCount() {
return this.useCount_;
}
public String getDomain() {
return this.domain_;
}
public synchronized long getExpiration() {
return this.lastused_ + GlobalProperties.getSessionMaxInactivity();
}
public synchronized boolean isExpired() {
if (this.isExpired_)
return true;
long now = Instant.now().getEpochSecond();
if (now > getExpiration()) {
markExpired();
} else if (GlobalProperties.getSessionMaxLifetime() > 0L && now - getCreation() > GlobalProperties.getSessionMaxLifetime()) {
markExpired();
} else if (GlobalProperties.getSessionMaxUseCount() > 0L && (this.useCount_ >= GlobalProperties.getSessionMaxUseCount() || this.useCount_ < 0L)) {
markExpired();
}
return this.isExpired_;
}
public synchronized void markExpired() {
this.isExpired_ = true;
}
public String getUserid() {
return this.userid_;
}
public boolean isLocalhost() {
return !(CommonUtil.hasContent(this.host_) && !this.host_.equalsIgnoreCase("localhost"));
}
public String getHost() {
return this.host_;
}
public AS400 getAS400() throws Exception {
if (this.as400_ == null || !this.as400_.isConnectionAlive(2))
initializeAS400(true, this.settings_);
return this.as400_;
}
public AS400JDBCConnection getJDBC() throws Exception {
initializeJDBC(this.settings_, false);
return this.dbconn_;
}
public String getToken() {
return this.token_;
}
public void toJSON(JSONSerializer json) {
json.startObject("sessionInfo");
json.add("userID", getUserid());
json.add("host", getHost());
json.add("expiration", Instant.ofEpochSecond(getExpiration()).toString());
json.add("creation", Instant.ofEpochSecond(getCreation()).toString());
json.add("lastUsed", Instant.ofEpochSecond(getLastUsed()).toString());
json.add("domain", getDomain());
json.add("expired", Boolean.valueOf(isExpired()));
json.endObject();
if (this.settings_ == null) {
json.add("sessionSettings", (String)null);
} else {
json.startObject("sessionSettings");
json.add("libraryList", this.settings_.libraryList);
json.add("clCommands", this.settings_.clCommands);
json.add("envVariables", this.settings_.envVariables);
json.add("sqlDefaultSchema", this.settings_.sqlDefaultSchema);
json.add("sqlTreatWarningsAsErrors", this.settings_.sqlTreatWarningsAsErrors, false);
json.add("sqlProperties", getCombinedJDBCProperties(this.settings_.sqlProperties));
json.add("sqlStatements", this.settings_.sqlStatements);
json.endObject();
}
}
public void reclaimResources() {
if (this.dbconn_ != null) {
try {
this.dbconn_.close();
} catch (Exception exception) {}
this.dbconn_ = null;
}
if (this.as400_ != null) {
try {
this.as400_.resetAllServices();
} catch (Exception exception) {}
this.as400_ = null;
}
}
private static AS400 getAS400Connection(String host, String userid, char[] password) throws Exception {
SecureAS400 secureAS400 = null;
AS400 as400 = null;
try {
if ((GlobalProperties.isIBMi() && (
host.equalsIgnoreCase("localhost") || host.equals("127.0.0.1"))) ||
!GlobalProperties.useSecureConnections()) {
as400 = new AS400(host, userid, password);
} else {
secureAS400 = new SecureAS400(host, userid, password);
as400 = secureAS400;
}
as400.validateSignon();
} catch (Exception e) {
try {
if (as400 != null)
as400.resetAllServices();
} catch (Exception exception) {}
throw e;
}
return as400;
}
private void initializeAS400(boolean disconnect, SessionAPIImpl.RSEAPI_SessionSettings sessionSettings) throws Exception {
try {
if (disconnect) {
if (this.dbconn_ != null) {
try {
this.dbconn_.close();
} catch (Exception exception) {}
this.dbconn_ = null;
}
this.as400_.disconnectAllServices();
}
this.as400_.setGuiAvailable(false);
CommandCall cmd = new CommandCall(this.as400_);
cmd.setThreadSafe(false);
cmd.run("QSYS/CHGJOB INQMSGRPY(*DFT)");
int jobCCSID = cmd.getServerJob().getCodedCharacterSetID();
AsyncLogger.traceDebug("initializeAS400", "ccsid='%d'", new Object[] { Integer.valueOf(jobCCSID) });
if (this.as400_.getCcsid() != jobCCSID) {
this.as400_.disconnectAllServices();
this.as400_.setCcsid(jobCCSID);
cmd.run("QSYS/CHGJOB INQMSGRPY(*DFT)");
}
applySessionSettings(sessionSettings);
} catch (Exception e) {
try {
this.as400_.disconnectAllServices();
} catch (Exception exception) {}
throw e;
}
}
private void initializeJDBC(SessionAPIImpl.RSEAPI_SessionSettings sessionSettings, boolean resetConnection) throws Exception {
if (jdbcDriver_ == null)
JDBCDriverInit();
if (resetConnection || !this.as400_.isConnected(4) || !this.as400_.isConnectionAlive(4))
if (this.dbconn_ != null) {
try {
this.dbconn_.close();
} catch (Exception exception) {}
this.dbconn_ = null;
}
if (this.dbconn_ == null) {
Map<String, String> temp = getCombinedJDBCProperties(null);
if (sessionSettings != null && sessionSettings.sqlProperties != null)
temp.putAll(sessionSettings.sqlProperties);
Properties jdbcProperties = CommonUtil.mapToProperties(temp);
this.dbconn_ = (AS400JDBCConnection)jdbcDriver_.connect(this.as400_, jdbcProperties, sessionSettings.sqlDefaultSchema, false);
}
}
private static synchronized void JDBCDriverInit() throws Exception {
if (jdbcDriver_ == null)
jdbcDriver_ = new AS400JDBCDriver();
}
private SessionAPIImpl.SetSessionSettingsResults applySessionSettings(SessionAPIImpl.RSEAPI_SessionSettings sessionSettings) throws Exception {
if (sessionSettings == null)
return null;
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> esLib = setLibl(this.as400_, sessionSettings.libraryList, sessionSettings.continueOnError.booleanValue());
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> esEV = setEnvVariables(this.as400_, sessionSettings.envVariables, sessionSettings.continueOnError.booleanValue());
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> esCL = CLCommandAPIImpl.runClCommands(this.as400_, sessionSettings.clCommands, sessionSettings.continueOnError.booleanValue());
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> esSQ = null;
List<String> jdbcInitErrors = null;
if (sessionSettings.sqlDefaultSchema != null || sessionSettings.sqlProperties != null) {
boolean resetConnection = false;
if ((sessionSettings.sqlDefaultSchema != null && this.settings_.sqlDefaultSchema == null) || (
sessionSettings.sqlDefaultSchema == null && this.settings_.sqlDefaultSchema != null) || (
sessionSettings.sqlDefaultSchema != null &&
this.settings_.sqlDefaultSchema != null &&
!sessionSettings.sqlDefaultSchema.equals(this.settings_.sqlDefaultSchema)))
resetConnection = true;
if ((sessionSettings.sqlProperties != null && this.settings_.sqlProperties == null) || (
sessionSettings.sqlProperties == null && this.settings_.sqlProperties != null) ||
!sessionSettings.sqlProperties.equals(this.settings_.sqlProperties))
resetConnection = true;
try {
initializeJDBC(sessionSettings, resetConnection);
} catch (SQLException e) {
jdbcInitErrors = new ArrayList<>();
jdbcInitErrors.add(e.getMessage());
}
if (jdbcInitErrors == null)
esSQ = SQLAPIImpl.runSQLStatements(this.dbconn_, sessionSettings.sqlStatements, sessionSettings.sqlTreatWarningsAsErrors.booleanValue(), sessionSettings.continueOnError.booleanValue());
}
return new SessionAPIImpl.SetSessionSettingsResults(esCL, esSQ, esLib, esEV, jdbcInitErrors);
}
public boolean inUse() {
return this.lock_.isLocked();
}
public void lock() {
if (this.lock_.isHeldByCurrentThread())
return;
try {
if (GlobalProperties.getSessionMaxWaitTime() == -1L) {
this.lock_.lock();
} else if (!this.lock_.tryLock(GlobalProperties.getSessionMaxWaitTime(), TimeUnit.SECONDS)) {
throw new RestWARequestTimeoutException("Request timed out while waiting for the session to become available.", new Object[0]);
}
} catch (InterruptedException e) {
throw new RestWAInternalServerErrorException("Request interrupted while waiting for session to become available.", new Object[0]);
}
}
public void unlock() {
if (this.lock_.isHeldByCurrentThread())
this.lock_.unlock();
}
public boolean getSQLTreatWarningsAsErrors() {
return (this.settings_ != null && this.settings_.sqlTreatWarningsAsErrors != null && this.settings_.sqlTreatWarningsAsErrors.booleanValue());
}
private Map<String, String> getCombinedJDBCProperties(Map<String, String> userSepcifiedProperties) {
Map<String, String> sqlProperties = new HashMap<>();
sqlProperties.putAll(DEFAULT_JDBCPROPERTIES);
if (userSepcifiedProperties != null && !userSepcifiedProperties.isEmpty())
sqlProperties.putAll(userSepcifiedProperties);
return sqlProperties;
}
public static List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> setLibl(AS400 sys, List<String> libList, boolean continueOnError) throws Exception {
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> es = null;
StringBuilder errorMessages = null;
if (libList == null || libList.isEmpty())
return es;
CommandCall cmd = new CommandCall(sys);
StringBuilder sbuf = new StringBuilder();
for (String lib : libList) {
sbuf.setLength(0);
if (!CommonUtil.hasContent(lib))
continue;
sbuf.append("QSYS/ADDLIBLE ").append(lib.trim()).append(" *LAST");
if (!cmd.run(sbuf.toString())) {
AS400Message[] messageList = cmd.getMessageList();
for (int i = 0; i < messageList.length; i++) {
if (messageList[i].getType() == 15)
if (!messageList[i].getID().equalsIgnoreCase("CPF2103")) {
if (errorMessages == null)
errorMessages = new StringBuilder();
errorMessages.append(messageList[i].getID()).append(": ").append(messageList[i].getText());
}
}
if (errorMessages != null && errorMessages.length() > 0) {
if (continueOnError) {
if (es == null)
es = new ArrayList<>();
es.add(new SessionAPIImpl.SetSessionSettingsResults.ErrorSource(lib, errorMessages.toString()));
errorMessages.setLength(0);
continue;
}
throw new RestWABadRequestException("CL command failed. Command is [%s]. Error message(s): %s", new Object[] { sbuf.toString(), errorMessages.toString() });
}
}
}
return es;
}
public static List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> setEnvVariables(AS400 as400, Map<String, String> envVariables, boolean continueOnError) throws Exception {
List<SessionAPIImpl.SetSessionSettingsResults.ErrorSource> es = null;
if (envVariables == null || envVariables.isEmpty())
return es;
ServiceProgramCall spc = CommonUtil.getServiceProgramCall(as400, "/QSYS.LIB/QP0ZCPA.SRVPGM", "putenv", 1, false, true);
CharConverter converter = new CharConverter(as400.getCcsid(), as400);
ProgramParameter[] pgmParmList = spc.getParameterList();
Set<String> keys = envVariables.keySet();
for (String key : keys) {
if (!CommonUtil.hasContent(key))
continue;
String value = envVariables.get(key);
if (value == null)
value = "";
pgmParmList[0].setInputData(converter.stringToByteArray(String.valueOf(key) + "=" + value + "\000"));
spc.run();
if (spc.getIntegerReturnValue() != 0) {
String errorMessage = "putenv() failed with errno " + spc.getErrno();
if (continueOnError) {
if (es == null)
es = new ArrayList<>();
es.add(new SessionAPIImpl.SetSessionSettingsResults.ErrorSource(key, errorMessage));
continue;
}
throw new RestWAInternalServerErrorException("Unable to process the request due to an internal error. %s", new Object[] { errorMessage });
}
}
return es;
}
}

View File

@@ -0,0 +1,127 @@
package dev.alexzaw.rest4i.session;
import dev.alexzaw.rest4i.exception.RestWAServiceUnavailableException;
import dev.alexzaw.rest4i.util.AsyncLogger;
import dev.alexzaw.rest4i.util.CommonUtil;
import dev.alexzaw.rest4i.util.GlobalProperties;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class SessionRepository {
private static ConcurrentHashMap<String, Session> token2SessionTable_ = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, AtomicLong> sessionsStatsTable_ = new ConcurrentHashMap<>();
private static boolean repositoryDestroyed = false;
static {
startCleanupThread();
}
public static synchronized Session createSession(String host, String userid, char[] password) throws Exception {
AsyncLogger.traceDebug("createSession", "in-parms: host='%s', userid='%s'", new Object[] { host, userid });
Session session = new Session(host, userid, password);
long maxSessions = GlobalProperties.getMaxSessions();
long maxSessionsPerUser = GlobalProperties.getMaxSessionsPerUser();
AtomicLong count = null;
try {
if (maxSessions != -1L && getTotalSessions() >= maxSessions)
throw new RestWAServiceUnavailableException("Maximum number of sessions has been reached.", new Object[0]);
String countKey = userid.toUpperCase();
synchronized (sessionsStatsTable_) {
count = sessionsStatsTable_.get(countKey);
if (count == null) {
count = new AtomicLong();
sessionsStatsTable_.put(countKey, count);
}
if (maxSessionsPerUser != -1L && count.get() >= maxSessionsPerUser)
throw new RestWAServiceUnavailableException("Maximum number of sessions per user has been reached.", new Object[0]);
count.incrementAndGet();
}
} catch (Exception e) {
session.reclaimResources();
throw e;
}
token2SessionTable_.put(session.getToken(), session);
return session;
}
public static synchronized void deleteSession(String sessionToken) {
AsyncLogger.traceDebug("deleteSession", "token", new Object[0]);
Session session = token2SessionTable_.remove(sessionToken);
if (session != null) {
session.reclaimResources();
String countKey = session.getUserid().toUpperCase();
AtomicLong count = sessionsStatsTable_.get(countKey);
if (count != null && count.longValue() > 0L)
count.decrementAndGet();
}
}
public static synchronized void deleteSession(Session session) {
deleteSession(session.getToken());
}
public static Session findSession(String sessionToken) {
return repositoryDestroyed ? null : token2SessionTable_.get(sessionToken);
}
public static long getTotalSessions() {
return (repositoryDestroyed ? 0L : token2SessionTable_.size());
}
public static Map<String, AtomicLong> getSessionsStatisticsData() {
return sessionsStatsTable_;
}
public static synchronized void destroy() {
repositoryDestroyed = true;
AsyncLogger.traceDebug("destroySessionRepository", "", new Object[0]);
sessionsStatsTable_.clear();
for (Session s : token2SessionTable_.values()) {
try {
s.reclaimResources();
} catch (Exception exception) {}
}
token2SessionTable_.clear();
}
public static synchronized void clearSessions(String user) {
if (user == null)
user = "*ALL";
if (!CommonUtil.hasContent(user))
return;
boolean isAll = user.equalsIgnoreCase("*ALL");
for (Session s : token2SessionTable_.values()) {
if (isAll || s.getUserid().equalsIgnoreCase(user))
s.markExpired();
}
}
private static boolean startCleanupThread() {
class SessionCleanup implements Runnable {
public void run() {
AsyncLogger.traceDebug("startCleanupThread", "Session cleanup thread starting.", new Object[0]);
while (!SessionRepository.repositoryDestroyed) {
long millisecsToSleep = GlobalProperties.getSessionCleanupInterval() * 1000L;
try {
Thread.sleep(millisecsToSleep);
AsyncLogger.traceDebug("startCleanupThread", "Cleanup in process.", new Object[0]);
SessionRepository.token2SessionTable_.forEach((key, value) -> {
if (value.isExpired() && !value.inUse())
SessionRepository.deleteSession(key);
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
SessionCleanup sessionCleanup = new SessionCleanup();
Thread cleanupThread = new Thread(sessionCleanup);
cleanupThread.start();
return true;
}
}

View File

@@ -0,0 +1,134 @@
package dev.alexzaw.rest4i.util;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
public class AsyncLogger {
protected static final Logger LOGGER = Logger.getLogger("IBMiRSEAPI");
private static ArrayBlockingQueue<TraceMessage> _blockingStdMsgQueue = new ArrayBlockingQueue<>(1000);
private static volatile boolean _stdLoggerActive = true;
static {
startStdLoggerHandlerThread();
}
private static class AUDIT extends Level {
private static final long serialVersionUID = -7472873829993078361L;
public AUDIT() {
super("AUDIT", 850);
}
}
private static Level AUDIT = new AUDIT();
private static class TraceMessage {
String _classSignature;
String _message;
Throwable _exception;
Object[] _args;
Level _level;
TraceMessage(Level level, String signature, String msg, Object[] args, Throwable ex) {
this._level = level;
this._classSignature = signature;
this._message = msg;
this._exception = ex;
this._args = args;
}
public void print() {
StringBuilder sb = new StringBuilder();
if (this._message == null)
this._message = "";
if (CommonUtil.hasContent(this._classSignature))
sb.append(this._classSignature).append(": ");
try {
sb.append(String.format(this._message, this._args));
} catch (Exception e) {
AsyncLogger.LOGGER.log(Level.WARNING, e.getMessage());
}
if (this._exception != null) {
AsyncLogger.LOGGER.log(this._level, sb.toString(), this._exception);
} else {
AsyncLogger.LOGGER.log(this._level, sb.toString());
}
}
}
private static boolean startStdLoggerHandlerThread() {
class MessageQueue implements Runnable {
public void run() {
AsyncLogger.LOGGER.info("Standard logger starting.");
while (AsyncLogger._stdLoggerActive) {
AsyncLogger.TraceMessage logData = null;
try {
logData = AsyncLogger._blockingStdMsgQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (logData != null)
logData.print();
}
AsyncLogger.LOGGER.info("Standard logger exiting.");
}
};
MessageQueue msgQueue = new MessageQueue();
Thread msgThread = new Thread(msgQueue);
msgThread.start();
return true;
}
public static void stopStdLoggerHandler() {
_stdLoggerActive = false;
LOGGER.info("Deactivating standard logger.");
}
public static void traceAudit(String classSignature, String message, Object... args) {
queueLogMessage(AUDIT, classSignature, message, args, null);
}
public static void traceInfo(String classSignature, String message, Object... args) {
queueLogMessage(Level.INFO, classSignature, message, args, null);
}
public static void traceWarning(String classSignature, String message, Object... args) {
queueLogMessage(Level.WARNING, classSignature, message, args, null);
}
public static void traceDebug(String classSignature, String message, Object... args) {
queueLogMessage(Level.FINEST, classSignature, message, args, null);
}
public static void traceDebug(String classSignature, Throwable exception, String message, Object... args) {
queueLogMessage(Level.FINEST, classSignature, message, args, exception);
}
public static void traceSevere(String classSignature, String message, Object... args) {
queueLogMessage(Level.SEVERE, classSignature, message, args, null);
}
public static void traceException(String classSignature, Throwable exception, String message, Object... args) {
if (message == null)
message = exception.getMessage();
queueLogMessage(Level.SEVERE, classSignature, message, args, exception);
}
private static void queueLogMessage(Level level, String classSignature, String message, Object[] args, Throwable exception) {
TraceMessage tm = new TraceMessage(level, classSignature, message, args, exception);
try {
if (!_blockingStdMsgQueue.offer(tm))
tm.print();
} catch (Exception e) {
LOGGER.severe("Exception in queueLogMessage: " + e.getMessage());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
package dev.alexzaw.rest4i.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class ChecksumUtil {
public static String getMessageDigest(String content) throws Exception {
byte[] byteArray = content.getBytes();
InputStream byteStream = new ByteArrayInputStream(byteArray);
return getMessageDigest(byteStream);
}
public static String getMessageDigest(InputStream byteStream) throws Exception {
String digest = null;
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw e;
}
DigestInputStream ds = new DigestInputStream(byteStream, md);
BufferedInputStream bs = new BufferedInputStream(ds);
byte[] buffer = new byte[4096];
int bytesRead = -1;
do {
try {
bytesRead = bs.read(buffer);
} catch (IOException e) {
throw e;
}
} while (bytesRead >= 0);
try {
bs.close();
} catch (IOException e) {
throw e;
}
byte[] digestArray = md.digest();
digest = toHexString(digestArray);
return digest;
}
private static String toHexString(byte[] bytes) {
StringBuilder builder = new StringBuilder(2 * bytes.length);
byte b;
int i;
byte[] arrayOfByte;
for (i = (arrayOfByte = bytes).length, b = 0; b < i; ) {
byte b1 = arrayOfByte[b];
String s = Integer.toHexString(0xFF & b1);
if (b1 >= 0 && b1 < 16)
builder.append('0');
builder.append(s);
b++;
}
return builder.toString();
}
}

View File

@@ -0,0 +1,142 @@
package dev.alexzaw.rest4i.util;
public interface CommonConstants {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String WWWAUTHENTICATE_HEADER = "WWW-Authenticate";
public static final String path = "path";
public static final String operand = "operand";
public static final String command = "command";
public static final String Scan = "Scan";
public static final String hostEncoding = "hostEncoding";
public static final String binary = "binary";
public static final String Convert = "Convert";
public static final String Etag = "ETag";
public static final String recordRangeTag = "record-range";
public static final String content = "content";
public static final String IfMatch = "If-Match";
public static final String newName = "newName";
public static final String destination = "destination";
public static final String filename = "filename";
public static final String text = "text";
public static final String invocation = "invocation";
public static final String mbr = "mbr";
public static final String records = "records";
public static final String status = "status";
public static final String filter = "filter";
public static final String prefix = "prefix";
public static final String owner = "owner";
public static final String jobName = "jobName";
public static final String Basic = "Basic";
public static final String Bearer = "Bearer";
public static final String port = "port";
public static final String userid = "userID";
public static final String expiration = "expiration";
public static final String creation = "creation";
public static final String lastused = "lastUsed";
public static final String domain = "domain";
public static final String utf8 = "UTF-8";
public static final String PrettyOutput = "prettyOutput";
public static final String NumOfResults = "number-of-results";
public static final String objectLibrary = "objectLibrary";
public static final String objectType = "objectType";
public static final String objectSubtype = "objectSubtype";
public static final String objectName = "objectName";
public static final String attrList = "attrList";
public static final String TIMESTAMP = "timestamp";
public static final String TITLE = "title";
public static final String STATUS = "status";
public static final String DETAIL = "detail";
public static final String TYPE = "type";
public static final String ERROR = "error";
public static final String INSTANCE = "instance";
public static final String DETAILS = "details";
public static final String libList = "liblist";
public static final String encoding = "encoding";
public static final String default_encoding = "default.encoding";
public static final String RSEAPI_VERSION_PATH = "/api/v1";
public static final String SESSIONCOOKIENAME = "apimlAuthenticationToken";
public static final String expired = "expired";
public static final String host = "host";
public static final String ENVVARS = "envvars";
public static final String verbose = "verbose";
public static final String ignoreerrors = "ignoreerrors";
public static final String memberName = "memberName";
public static final String memberType = "memberType";
public static final String MAXJOBLOGRECORDS = "maxjoblogrecords";
public static final String JOBLOGFILTER = "joblogfilter";
public static final String HOST = "host";
public static final String METHOD = "method";
public static final String library = "library";
public static final String commandname = "commandname";
public static final String ignorecase = "ignorecase";
public static final String user = "user";
}

View File

@@ -0,0 +1,456 @@
package dev.alexzaw.rest4i.util;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.ErrorCodeParameter;
import com.ibm.as400.access.IFSFile;
import com.ibm.as400.access.IFSFileInputStream;
import com.ibm.as400.access.IFSFileOutputStream;
import com.ibm.as400.access.IFSFileReader;
import com.ibm.as400.access.IFSTextFileOutputStream;
import com.ibm.as400.access.ProgramCall;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.QSYSObjectPathName;
import com.ibm.as400.access.ServiceProgramCall;
import com.ibm.as400.access.User;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.Reader;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
public class CommonUtil {
public static final String TEMPDIR = "/QIBM/UserData/OS/AdminInst/admin5/wlp/usr/servers/admin5/QiasTemp";
public static final ThreadLocal<HttpServletRequest> _serviceContext = new ThreadLocal<>();
public static String bytesToString(byte[] rawBytes) {
StringBuilder sb = new StringBuilder();
if (rawBytes != null)
for (int i = 0; i < rawBytes.length; i++)
sb.append(Integer.toString((rawBytes[i] & 0xFF) + 256, 16).substring(1));
return sb.toString();
}
public static String stringToHex(String s) {
StringBuilder sb = new StringBuilder();
char[] ch = s.toCharArray();
for (int i = 0; i < ch.length; i++)
sb.append(Integer.toHexString(ch[i]));
return sb.toString();
}
public static String hexToString(String hex) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hex.length() - 1; i += 2) {
String tempInHex = hex.substring(i, i + 2);
sb.append((char)Integer.parseInt(tempInHex, 16));
}
return sb.toString();
}
public static boolean isValid(String property, long value, long min, long max, boolean genException) {
if (value < min || (max != -1L && value > max)) {
if (genException)
throw new RestWABadRequestException("The value for property '%s' is not valid.", new Object[] { property });
return false;
}
return true;
}
public static long getNumericProperty(String propName, String s, long min, long max, long defaultValue) {
long returnValue = defaultValue;
boolean usingDefault = false;
if (hasContent(s))
try {
returnValue = Long.parseLong(s);
} catch (Exception e) {
returnValue = defaultValue;
usingDefault = true;
}
if ((returnValue != defaultValue && returnValue < min) || (
max != -1L && returnValue > max) ||
returnValue > Long.MAX_VALUE) {
returnValue = defaultValue;
usingDefault = true;
}
if (usingDefault && propName != null)
AsyncLogger.traceWarning("", "The value for property '%s' is not valid. Using default value of %s. ", new Object[] { propName, Long.toString(defaultValue) });
return returnValue;
}
public static int getLength(String s) {
return (s == null) ? 0 : s.length();
}
public static String toBase64(byte[] data) {
if (data == null || data.length == 0)
return "";
return DatatypeConverter.printBase64Binary(data);
}
public static byte[] fromBase64(String data) {
if (data == null)
return new byte[0];
return DatatypeConverter.parseBase64Binary(data);
}
public static String toDate(Date sqlDate) {
return sqlDate.toString();
}
public static String toTime(Time sqlTime) {
ZoneOffset zoneOffSet = ZoneId.systemDefault().getRules().getOffset(Instant.now());
return String.valueOf(sqlTime.toString()) + zoneOffSet.toString();
}
public static String toTimestamp(Timestamp sqlTimestamp) {
LocalDateTime localDateTime = sqlTimestamp.toLocalDateTime();
ZoneOffset zoneOffSet = ZoneId.systemDefault().getRules().getOffset(localDateTime);
OffsetDateTime offsetDateTime = localDateTime.atOffset(zoneOffSet);
String offsetDateTime_string = offsetDateTime.format(DateTimeFormatter.ISO_DATE_TIME);
String zoneOffSet_string = zoneOffSet.toString();
if (zoneOffSet_string.length() > 6) {
int indx = offsetDateTime_string.lastIndexOf(":");
offsetDateTime_string = offsetDateTime_string.substring(0, indx);
}
return offsetDateTime_string;
}
public static String getRemoteUser() {
String remoteUser = null;
HttpServletRequest req = _serviceContext.get();
if (req != null)
remoteUser = req.getRemoteUser();
return remoteUser;
}
public static String getRemoteAddr() {
String remoteAddr = null;
HttpServletRequest req = _serviceContext.get();
if (req != null)
remoteAddr = req.getRemoteAddr();
return remoteAddr;
}
public static int getServerPort() {
int port = -1;
HttpServletRequest req = _serviceContext.get();
if (req != null)
port = req.getServerPort();
return port;
}
public static boolean hasContent(String string) {
return (string != null && string.trim().length() > 0);
}
public static boolean hasContent(char[] ca) {
return (ca != null && ca.length > 0);
}
public static boolean hasContent(List<String> strArray) {
if (strArray == null)
return false;
for (String s : strArray) {
if (hasContent(s))
return true;
}
return false;
}
public static String getFilenameFromPath(String path) {
int lastSlash = path.lastIndexOf('/');
return path.substring(lastSlash + 1);
}
public static String trimLeft(String s) {
if (s == null || s.length() == 0)
return "";
int startIndex = 0;
int len = s.length();
for (startIndex = 0; startIndex < len; startIndex++) {
char c = s.charAt(startIndex);
if (c != ' ')
break;
}
return s.substring(startIndex);
}
public static String trimBoth(String s) {
String ls = trimLeft(s);
return trimRight(ls);
}
public static String trimRight(String s) {
if (s == null || s.length() == 0)
return "";
int endIndex = 0;
int len = s.length();
for (endIndex = len - 1; endIndex > -1; endIndex--) {
char c = s.charAt(endIndex);
if (c != ' ')
break;
}
return s.substring(0, endIndex + 1);
}
public static String normalizePath(String path) {
if (!hasContent(path))
throw new RestWABadRequestException("Path name not set.", new Object[0]);
if (detectReflectedXSS(path))
throw new RestWABadRequestException("Reflected cross site scripting vulnerabilities detected in input data.", new Object[0]);
StringBuilder sb = new StringBuilder();
if (!path.startsWith("/"))
sb.append("/");
sb.append(path);
while (sb.length() > 1 && sb.charAt(1) == '/')
sb.deleteCharAt(1);
while (sb.length() > 0 && sb.charAt(sb.length() - 1) == '/')
sb.setLength(sb.length() - 1);
if (sb.length() == 0)
sb.append('/');
return sb.toString();
}
public static Properties mapToProperties(Map<String, String> map) {
Properties p = new Properties();
if (map != null) {
Set<Map.Entry<String, String>> set = map.entrySet();
for (Map.Entry<String, String> entry : set) {
if (hasContent(entry.getKey()) && hasContent(entry.getValue()))
p.put(((String)entry.getKey()).trim(), ((String)entry.getValue()).trim());
}
}
return p;
}
public static boolean detectReflectedXSS(String input) {
if (!hasContent(input))
return false;
String lc = input.toLowerCase();
if ((lc.contains("<script>") || lc.contains("<script ")) && (
lc.contains("</script>") || lc.contains("</script ")))
return true;
return false;
}
public static boolean isPathProblematic(QSYSObjectPathName p) {
return !(p.getLibraryName().indexOf('/') == -1 &&
p.getLibraryName().indexOf('/') == -1 &&
p.getObjectName().indexOf('/') == -1 &&
p.getMemberName().indexOf('/') == -1);
}
public static boolean hasAllObjectAuthority(AS400 system) throws Exception {
User usr = new User(system, system.getUserId());
if (usr.hasSpecialAuthority(User.SPECIAL_AUTHORITY_ALL_OBJECT))
return true;
return false;
}
public static ServiceProgramCall getServiceProgramCall(AS400 as400, String srvpgmPath, String procedure, int numOfParams, boolean containsErrorCodeParam, boolean intReturnValue) throws Exception {
ProgramParameter[] parameterList = new ProgramParameter[numOfParams];
ProgramParameter pgmParameter = null;
if (numOfParams > 0 && containsErrorCodeParam) {
ErrorCodeParameter errorCodeParameter = new ErrorCodeParameter();
errorCodeParameter.setParameterType(2);
parameterList[numOfParams - 1] = (ProgramParameter)errorCodeParameter;
numOfParams--;
}
for (int i = 0; i < numOfParams; i++) {
pgmParameter = new ProgramParameter();
pgmParameter.setParameterType(2);
parameterList[i] = pgmParameter;
}
ServiceProgramCall spc = new ServiceProgramCall(as400);
spc.setProgram(srvpgmPath, parameterList);
spc.setProcedureName(procedure);
spc.setThreadSafe(true);
if (intReturnValue)
spc.setReturnValueFormat(1);
return spc;
}
public static ProgramCall getProgramCall(AS400 as400, String pgmPath, int numOfParams, boolean containsErrorCodeParam) throws Exception {
ProgramParameter[] parameterList = new ProgramParameter[numOfParams];
ProgramParameter pgmParameter = null;
if (numOfParams > 0 && containsErrorCodeParam) {
ErrorCodeParameter errorCodeParameter = new ErrorCodeParameter();
errorCodeParameter.setParameterType(2);
parameterList[numOfParams - 1] = (ProgramParameter)errorCodeParameter;
numOfParams--;
}
for (int i = 0; i < numOfParams; i++) {
pgmParameter = new ProgramParameter();
pgmParameter.setParameterType(2);
parameterList[i] = pgmParameter;
}
ProgramCall pc = new ProgramCall(as400);
pc.setProgram(pgmPath, parameterList);
pc.setThreadSafe(true);
return pc;
}
public static Comparator<String> StringComparator = new Comparator<String>() {
public int compare(String o1, String o2) {
if (o1.equals(o2))
return 0;
String o1Lower = o1.toLowerCase();
String o2Lower = o2.toLowerCase();
if (o1Lower.equals(o2Lower))
return o1Lower.equals(o1) ? -1 : 1;
return o1Lower.compareTo(o2Lower);
}
};
public static Pattern getGenericNamePattern(String s) {
Pattern p = null;
try {
if (s != null) {
String regExpr = "";
if (s.endsWith("*")) {
s = s.substring(0, s.length() - 1);
regExpr = "(.*)";
}
if (!s.equals("*") && !s.isEmpty())
p = Pattern.compile(String.valueOf(Pattern.quote(s)) + regExpr, 2);
}
} catch (Exception exception) {}
return p;
}
public static String generateTempFilePath(AS400 sys, String fn_prefix, String fn_suffix, boolean createFile) throws Exception {
String suffix = hasContent(fn_suffix) ? ("." + fn_suffix) : "";
String filePath = "/QIBM/UserData/OS/AdminInst/admin5/wlp/usr/servers/admin5/QiasTemp" + File.separator + fn_prefix + "_" + Thread.currentThread().getId() + suffix;
IFSFile ifsfile = new IFSFile(sys, filePath);
deleteFile(ifsfile);
if (createFile) {
if (!ifsfile.createNewFile()) {
AsyncLogger.traceSevere("generateTempFilePath", "Unable to create file '%s'.", new Object[] { ifsfile.getPath() });
throw new RestWAInternalServerErrorException();
}
ifsfile.setCCSID(819);
}
return filePath;
}
public static byte[] readFromFile(AS400 sys, String filePath, boolean deleteAfterRead) throws Exception {
IFSFileInputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
try {
is = new IFSFileInputStream(sys, filePath, -4);
int dataRead = 0;
while ((dataRead = is.read(buffer)) > 0)
baos.write(buffer, 0, dataRead);
} finally {
try {
if (is != null)
is.close();
} catch (Exception exception) {}
if (deleteAfterRead)
deleteFile(sys, filePath);
}
return baos.toByteArray();
}
public static byte[] readFromFileText(AS400 sys, String filePath, boolean deleteAfterRead) throws Exception {
BufferedReader is = null;
StringBuilder sb = new StringBuilder();
try {
IFSFile ifsfile = new IFSFile(sys, filePath);
is = new BufferedReader((Reader)new IFSFileReader(ifsfile, ifsfile.getCCSID(), -4));
String l;
while ((l = is.readLine()) != null)
sb.append(l).append("\n");
} finally {
try {
if (is != null)
is.close();
} catch (Exception exception) {}
if (deleteAfterRead)
deleteFile(sys, filePath);
}
return sb.toString().getBytes();
}
public static void writeToFile(AS400 sys, String filePath, byte[] data) throws Exception {
IFSFileOutputStream os = new IFSFileOutputStream(sys, filePath, 819);
try {
os.write(data);
os.flush();
} finally {
try {
if (os != null)
os.close();
} catch (Exception exception) {}
}
}
public static void writeToFileText(AS400 sys, String filePath, String data) throws Exception {
IFSTextFileOutputStream os = new IFSTextFileOutputStream(sys, filePath, 819);
try {
os.write(data);
os.flush();
} finally {
try {
if (os != null)
os.close();
} catch (Exception exception) {}
}
}
public static void deleteFile(IFSFile ifsfile) throws Exception {
if (ifsfile.exists() && !ifsfile.delete()) {
AsyncLogger.traceSevere("deleteFile", "Unable to delete file '%s'.", new Object[] { ifsfile.getPath() });
throw new RestWAInternalServerErrorException();
}
}
public static void deleteFile(AS400 sys, String filePath) throws Exception {
deleteFile(new IFSFile(sys, filePath));
}
public static String generateDistinquishedName(String... args) throws Exception {
int size = (args != null) ? args.length : 0;
int attrcount = size / 2;
if (attrcount == 0)
return null;
List<Rdn> rdnList = new ArrayList<>();
int j = 0;
for (int i = 0; i < attrcount; i++) {
if (hasContent(args[j]) && hasContent(args[j + 1])) {
Rdn rdn = new Rdn(args[j], args[j + 1]);
rdnList.add(0, rdn);
}
j += 2;
}
if (!rdnList.isEmpty()) {
LdapName ldapname = new LdapName(rdnList);
return ldapname.toString();
}
return null;
}
}

View File

@@ -0,0 +1,403 @@
package dev.alexzaw.rest4i.util;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class GlobalProperties {
private static final boolean IS_IBMI = System.getProperty("os.name").equalsIgnoreCase("OS/400");
private static final String RSEAPI_USERATA = "/QIBM/UserData/OS/RSEAPI";
private static final String RSEAPI_PROPERTY_PATH = "/QIBM/UserData/OS/RSEAPI/rseapi.properties";
public static final String RSEAPI_VERSION = "1.0.7";
private static final String RSEAPI_BASEPATH = "rseapi";
private static final String RSEAPI_ADMINUSERS = "com.ibm.rseapi.adminusers";
private static final String RSEAPI_INCLUDEUSERS = "com.ibm.rseapi.includeusers";
private static final String RSEAPI_EXCLUDEUSERS = "com.ibm.rseapi.excludeusers";
private static final String RSEAPI_SESSION_MAXUSECOUNT = "com.ibm.rseapi.maxsessionusecount";
private static final String RSEAPI_SESSION_MAXLIFETIME = "com.ibm.rseapi.maxsessionlifetime";
private static final String RSEAPI_SESSION_MAXINACTIVITY = "com.ibm.rseapi.maxsessioninactivity";
private static final String RSEAPI_SESSION_MAXWAITTIME = "com.ibm.rseapi.maxsessionwaittime";
private static final String RSEAPI_SESSION_MAXPERUSER = "com.ibm.rseapi.maxsessionsperuser";
private static final String RSEAPI_SESSION_MAX = "com.ibm.rseapi.maxsessions";
private static final String RSEAPI_SESSION_CLEANUP_INTERVAL = "com.ibm.rseapi.sessioncleanupinterval";
private static final String RSEAPI_MAX_FILE_SIZE = "com.ibm.rseapi.maxfilesize";
public static final long NO_MAX = -1L;
public static final int DEFAULT_SESSION_CLEANUP_INTERVAL = 300;
public static final long DEFAULT_SESSION_MAXUSECOUNT = -1L;
public static final long DEFAULT_SESSION_MAXLIFETIME = -1L;
public static final long DEFAULT_SESSION_MAXINACTIVITY = 7200L;
public static final long DEFAULT_SESSION_MAXWAITTIME = 300L;
public static final long DEFAULT_SESSION_MAXPERUSER = 20L;
public static final long DEFAULT_SESSION_MAX = 100L;
public static final long DEFAULT_MAX_FILESIZE = 3072000L;
public static final long MIN_FILESIZE = 0L;
public static final long MAX_FILESIZE = 15360000L;
public static final long MIN_SESSION_MAXUSECOUNT = 1L;
public static final long MAX_SESSION_MAXUSECOUNT = -1L;
public static final long MIN_SESSION_MAXLIFETIME = 30L;
public static final long MAX_SESSION_MAXLIFETIME = -1L;
public static final long MIN_SESSION_MAXINACTIVITY = 30L;
public static final long MAX_SESSION_MAXINACTIVITY = 7200L;
public static final long MIN_SESSION_CLEANUP_INTERVAL = 30L;
public static final long MAX_SESSION_CLEANUP_INTERVAL = 900L;
public static final long MIN_SESSION_PERUSER = 1L;
public static final long MAX_SESSION_PERUSER = -1L;
public static final long MIN_SESSION_MAX = 0L;
public static final long MAX_SESSION_MAX = -1L;
public static final long MIN_SESSION_WAITTIME = 0L;
public static final long MAX_SESSION_WAITTIME = -1L;
private static Set<String> _adminUsersSet = new HashSet<>();
private static List<String> _adminUsers = new ArrayList<>();
private static Set<String> _includeUsersSet = new HashSet<>();
private static List<String> _includeUsers = new ArrayList<>();
private static Set<String> _excludeUsersSet = new HashSet<>();
private static List<String> _excludeUsers = new ArrayList<>();
private static long _sessionMaxLifetime = -1L;
private static long _sessionMaxUseCount = -1L;
private static long _sessionMaxInactivity = 7200L;
private static long _sessionMaxWaitTime = 300L;
private static long _sessionCleanupInterval = 300L;
private static long _maxSessionsPerUser = 20L;
private static long _maxSessions = 100L;
private static long _maxFileSize = 3072000L;
private static boolean _useSecureConnections = true;
private static String _hostName = "localhost";
private static String _os_name = System.getProperty("os.name");
private static String _os_version = System.getProperty("os.version");
private static String _java_version = System.getProperty("java.version");
static {
try {
_hostName = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException e) {
AsyncLogger.traceWarning("GlobalProperties", "Unable to get canonical host name. " + e.getMessage(), new Object[0]);
try {
_hostName = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException IGNORE) {
_hostName = null;
}
}
updatePropertiesFromJVMProperties();
updatePropertiesFromFile();
StringBuilder sb = new StringBuilder();
sb.append("[").append("com.ibm.rseapi.adminusers").append("=").append(_adminUsers).append("] ");
sb.append("[").append("com.ibm.rseapi.includeusers").append("=").append(_includeUsers).append("] ");
sb.append("[").append("com.ibm.rseapi.excludeusers").append("=").append(_excludeUsers).append("] ");
sb.append("[").append("com.ibm.rseapi.maxfilesize").append("=").append(_maxFileSize).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessions").append("=").append(_maxSessions).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessionsperuser").append("=").append(_maxSessionsPerUser).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessioninactivity").append("=").append(_sessionMaxInactivity).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessionlifetime").append("=").append(_sessionMaxLifetime).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessionusecount").append("=").append(_sessionMaxUseCount).append("] ");
sb.append("[").append("com.ibm.rseapi.maxsessionwaittime").append("=").append(_sessionMaxWaitTime).append("] ");
sb.append("[").append("com.ibm.rseapi.sessioncleanupinterval").append("=").append(_sessionCleanupInterval).append("] ");
AsyncLogger.traceDebug("GlobalProperties", sb.toString(), new Object[0]);
}
public static String getRSEAPIVersion() {
return "1.0.7";
}
public static String getRSEAPIBasePath() {
return "rseapi";
}
public static long getSessionCleanupInterval() {
return _sessionCleanupInterval;
}
public static void setSessionCleanupInterval(long l) {
_sessionCleanupInterval = l;
}
public static long getSessionMaxInactivity() {
return _sessionMaxInactivity;
}
public static void setSessionMaxInactivity(long l) {
_sessionMaxInactivity = l;
}
public static long getSessionMaxLifetime() {
return _sessionMaxLifetime;
}
public static void setSessionMaxLifetime(long l) {
_sessionMaxLifetime = l;
}
public static void setSessionMaxUseCount(long l) {
_sessionMaxUseCount = l;
}
public static long getSessionMaxUseCount() {
return _sessionMaxUseCount;
}
public static long getMaxSessionsPerUser() {
return _maxSessionsPerUser;
}
public static void setMaxSessionsPerUser(long l) {
_maxSessionsPerUser = l;
}
public static long getMaxSessions() {
return _maxSessions;
}
public static void setMaxSessions(long l) {
_maxSessions = l;
}
public static long getMaxFileSize() {
return _maxFileSize;
}
public static void setMaxFileSize(long l) {
_maxFileSize = l;
}
public static long getSessionMaxWaitTime() {
return _sessionMaxWaitTime;
}
public static void setSessionMaxWaitTime(long l) {
_sessionMaxWaitTime = l;
}
public static boolean useSecureConnections() {
return _useSecureConnections;
}
public static boolean isIBMi() {
return IS_IBMI;
}
public static String getHostName() {
return _hostName;
}
public static String getOSName() {
return _os_name;
}
public static String getOSVersion() {
return _os_version;
}
public static String getJavaVersion() {
return _java_version;
}
public static List<String> getAdminUsers() {
return _adminUsers;
}
private static void setAdminUserids(String adminUsers) {
List<String> userList = null;
if (!CommonUtil.hasContent(adminUsers)) {
userList = new ArrayList<>();
} else {
userList = Arrays.asList(adminUsers.split(","));
}
setAdminUserids(userList);
}
public static void setAdminUserids(List<String> adminUsers) {
if (adminUsers == null)
return;
_adminUsersSet.clear();
_adminUsers = null;
for (String u : adminUsers) {
if (CommonUtil.hasContent(u))
_adminUsersSet.add(u.trim().toUpperCase());
}
_adminUsers = new ArrayList<>(_adminUsersSet);
Collections.sort(_adminUsers);
}
public static boolean isAdminUserid(String u) {
return _adminUsersSet.contains(u.toUpperCase());
}
public static List<String> getIncludeUsers() {
return _includeUsers;
}
private static void setIncludeUserids(String includeUsers) {
List<String> userList = null;
if (!CommonUtil.hasContent(includeUsers)) {
userList = new ArrayList<>();
} else {
userList = Arrays.asList(includeUsers.split(","));
}
setIncludeUserids(userList);
}
public static void setIncludeUserids(List<String> includeUsers) {
if (includeUsers == null)
return;
_includeUsersSet.clear();
_includeUsers = null;
for (String u : includeUsers) {
if (CommonUtil.hasContent(u))
_includeUsersSet.add(u.trim().toUpperCase());
}
_includeUsers = new ArrayList<>(_includeUsersSet);
Collections.sort(_includeUsers);
}
public static boolean isIncludeUserid(String u) {
return !(!_includeUsersSet.isEmpty() && !_includeUsersSet.contains(u.toUpperCase()));
}
public static List<String> getExcludeUsers() {
return _excludeUsers;
}
private static void setExcludeUserids(String excludeUsers) {
List<String> userList = null;
if (!CommonUtil.hasContent(excludeUsers)) {
userList = new ArrayList<>();
} else {
userList = Arrays.asList(excludeUsers.split(","));
}
setExcludeUserids(userList);
}
public static void setExcludeUserids(List<String> excludeUsers) {
if (excludeUsers == null)
return;
_excludeUsersSet.clear();
_excludeUsers = null;
for (String u : excludeUsers) {
if (CommonUtil.hasContent(u))
_excludeUsersSet.add(u.trim().toUpperCase());
}
_excludeUsers = new ArrayList<>(_excludeUsersSet);
Collections.sort(_excludeUsers);
}
public static boolean isExcludeUserid(String u) {
return _excludeUsersSet.contains(u.toUpperCase());
}
public static void save() {
if (!IS_IBMI)
throw new RestWABadRequestException("Save of properties failed. %s", new Object[] { "" });
File pf = new File("/QIBM/UserData/OS/RSEAPI/rseapi.properties");
try {
Exception exception2, exception1 = null;
} catch (Exception e) {
AsyncLogger.traceException("save", e, null, new Object[0]);
throw new RestWAInternalServerErrorException("Save of properties failed. %s", new Object[] { e.getMessage() });
}
}
private static void updatePropertiesFromJVMProperties() {
String value = System.getProperty("com.ibm.rseapi.adminusers");
setAdminUserids(value);
value = System.getProperty("com.ibm.rseapi.includeusers");
setIncludeUserids(value);
value = System.getProperty("com.ibm.rseapi.excludeusers");
setExcludeUserids(value);
value = System.getProperty("com.ibm.rseapi.sessioncleanupinterval");
_sessionCleanupInterval = CommonUtil.getNumericProperty("com.ibm.rseapi.sessioncleanupinterval", value, 30L, 7200L, _sessionCleanupInterval);
value = System.getProperty("com.ibm.rseapi.maxsessioninactivity");
_sessionMaxInactivity = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessioninactivity", value, 30L, 7200L, _sessionMaxInactivity);
value = System.getProperty("com.ibm.rseapi.maxsessionlifetime");
_sessionMaxLifetime = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessionlifetime", value, 30L, -1L, _sessionMaxLifetime);
value = System.getProperty("com.ibm.rseapi.maxsessionusecount");
_sessionMaxUseCount = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessionusecount", value, 1L, -1L, _sessionMaxUseCount);
value = System.getProperty("com.ibm.rseapi.maxsessionsperuser");
_maxSessionsPerUser = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessionsperuser", value, 1L, -1L, _maxSessionsPerUser);
value = System.getProperty("com.ibm.rseapi.maxsessions");
_maxSessions = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessions", value, 0L, -1L, _maxSessions);
value = System.getProperty("com.ibm.rseapi.maxfilesize");
_maxFileSize = CommonUtil.getNumericProperty("com.ibm.rseapi.maxfilesize", value, 0L, 15360000L, _maxFileSize);
value = System.getProperty("com.ibm.rseapi.maxsessionwaittime");
_sessionMaxWaitTime = CommonUtil.getNumericProperty("com.ibm.rseapi.maxsessionwaittime", value, 0L, -1L, _sessionMaxWaitTime);
}
private static void updatePropertiesFromFile() {
if (!IS_IBMI)
return;
File pf = new File("/QIBM/UserData/OS/RSEAPI/rseapi.properties");
if (!pf.exists() || !pf.canRead())
return;
try {
Exception exception2, exception1 = null;
} catch (Exception e) {
AsyncLogger.traceException("GlobalProperties", e, null, new Object[0]);
}
}
}

View File

@@ -0,0 +1,175 @@
package dev.alexzaw.rest4i.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class JSONSerializer {
private static final String[] REPLACEMENT_CHARS = new String[128];
static {
for (int i = 0; i <= 31; i++) {
REPLACEMENT_CHARS[i] = String.format("\\u%04x", new Object[] { Integer.valueOf(i) });
}
REPLACEMENT_CHARS[34] = "\\\"";
REPLACEMENT_CHARS[92] = "\\\\";
REPLACEMENT_CHARS[9] = "\\t";
REPLACEMENT_CHARS[8] = "\\b";
REPLACEMENT_CHARS[10] = "\\n";
REPLACEMENT_CHARS[13] = "\\r";
REPLACEMENT_CHARS[12] = "\\f";
}
private StringBuilder _sb = null;
public JSONSerializer() {
this(100);
}
public JSONSerializer(int initialSize) {
this._sb = new StringBuilder(initialSize);
}
public JSONSerializer startObject() {
this._sb.append("{");
return this;
}
public JSONSerializer startObject(String identifier) {
this._sb.append("\"").append(escapeJSON(identifier)).append("\": {");
return this;
}
public JSONSerializer endObject() {
if (this._sb.charAt(this._sb.length() - 1) == ',')
this._sb.setLength(this._sb.length() - 1);
this._sb.append("},");
return this;
}
public JSONSerializer startArray() {
this._sb.append("[");
return this;
}
public JSONSerializer startArray(String identifier) {
this._sb.append("\"").append(escapeJSON(identifier)).append("\": [");
return this;
}
public JSONSerializer endArray() {
if (this._sb.charAt(this._sb.length() - 1) == ',')
this._sb.setLength(this._sb.length() - 1);
this._sb.append("],");
return this;
}
public JSONSerializer add(String name, String value) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
if (value != null) {
this._sb.append("\"").append(escapeJSON(value)).append("\",");
} else {
this._sb.append("null,");
}
return this;
}
public JSONSerializer add(String name, Long value) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
if (value != null) {
this._sb.append(value.longValue()).append(",");
} else {
this._sb.append("null,");
}
return this;
}
public JSONSerializer add(String name, Integer value) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
if (value != null) {
this._sb.append(value.intValue()).append(",");
} else {
this._sb.append("null,");
}
return this;
}
public JSONSerializer add(String name, Boolean value) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
if (value != null) {
this._sb.append(value.toString()).append(",");
} else {
this._sb.append("null,");
}
return this;
}
public JSONSerializer add(String name, Boolean value, boolean defaultValueIfNull) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
value = Boolean.valueOf((value != null) ? value.booleanValue() : defaultValueIfNull);
this._sb.append(value.toString()).append(",");
return this;
}
public JSONSerializer add(String name, List<String> values) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
this._sb.append("[");
if (values != null && !values.isEmpty()) {
for (String v : values)
this._sb.append("\"").append(escapeJSON(v)).append("\",");
this._sb.setLength(this._sb.length() - 1);
}
this._sb.append("],");
return this;
}
public JSONSerializer add(String name, Map<String, String> map) {
this._sb.append("\"").append(escapeJSON(name)).append("\": ");
this._sb.append("{");
if (map != null && !map.isEmpty()) {
List<String> mapKeys = new ArrayList<>(map.keySet());
Collections.sort(mapKeys);
for (String k : mapKeys) {
this._sb.append("\"").append(escapeJSON(k)).append("\": ");
this._sb.append("\"").append(escapeJSON(map.get(k))).append("\",");
}
this._sb.setLength(this._sb.length() - 1);
}
this._sb.append("},");
return this;
}
public String toString() {
if (this._sb.charAt(this._sb.length() - 1) == ',')
this._sb.setLength(this._sb.length() - 1);
return this._sb.toString();
}
public static String escapeJSON(String raw) {
if (raw == null || raw.length() == 0)
return "";
return escapeJSON(new StringBuilder(raw.length()), raw).toString();
}
private static StringBuilder escapeJSON(StringBuilder sb, String raw) {
if (raw == null || raw.length() == 0)
return sb;
int len = raw.length();
String replacement = "";
int beginning = 0;
for (int i = 0; i < len; i++) {
char charAt = raw.charAt(i);
if ((charAt >= '€' || (replacement = REPLACEMENT_CHARS[charAt]) != null) && charAt < '€') {
if (i > beginning)
sb.append(raw.substring(beginning, i));
sb.append(replacement);
beginning = i + 1;
}
}
if (beginning < len)
sb.append(raw.substring(beginning));
return sb;
}
}

View File

@@ -0,0 +1,443 @@
package dev.alexzaw.rest4i.util;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;
/**
* JSON utility class for parsing and manipulating JSON data without external dependencies.
*
* This implementation provides basic JSON parsing capabilities using only standard
* Java libraries, suitable for use in environments where external JSON libraries
* may not be available.
*
* @author alexzaw
*/
public class JsonUtils {
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?");
/**
* Parses JSON from an InputStream into a Map.
*
* @param inputStream The input stream containing JSON data
* @return A Map representation of the JSON data
* @throws IOException If reading from the stream fails
* @throws IllegalArgumentException If the JSON is malformed
*/
public static Map<String, Object> parseJsonToMap(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return parseJsonToMap(sb.toString());
}
}
/**
* Parses a JSON string into a Map.
*
* @param jsonString The JSON string to parse
* @return A Map representation of the JSON data
* @throws IllegalArgumentException If the JSON is malformed
*/
public static Map<String, Object> parseJsonToMap(String jsonString) {
if (jsonString == null || jsonString.trim().isEmpty()) {
return new HashMap<>();
}
JsonParser parser = new JsonParser(jsonString.trim());
Object result = parser.parseValue();
if (result instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> mapResult = (Map<String, Object>) result;
return mapResult;
} else {
throw new IllegalArgumentException("JSON does not represent an object");
}
}
/**
* Converts a Map to a JSON string.
*
* @param map The map to convert
* @return JSON string representation
*/
public static String mapToJsonString(Map<String, Object> map) {
if (map == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) {
sb.append(",");
}
sb.append("\"").append(escapeJsonString(entry.getKey())).append("\":");
sb.append(valueToJsonString(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
/**
* Converts any value to its JSON string representation.
*
* @param value The value to convert
* @return JSON string representation of the value
*/
public static String valueToJsonString(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + escapeJsonString((String) value) + "\"";
} else if (value instanceof Number) {
return value.toString();
} else if (value instanceof Boolean) {
return value.toString();
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
return mapToJsonString(map);
} else if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
return listToJsonString(list);
} else {
return "\"" + escapeJsonString(value.toString()) + "\"";
}
}
/**
* Converts a List to a JSON string.
*
* @param list The list to convert
* @return JSON string representation
*/
public static String listToJsonString(List<Object> list) {
if (list == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
boolean first = true;
for (Object item : list) {
if (!first) {
sb.append(",");
}
sb.append(valueToJsonString(item));
first = false;
}
sb.append("]");
return sb.toString();
}
/**
* Escapes special characters in a JSON string.
*
* @param str The string to escape
* @return The escaped string
*/
private static String escapeJsonString(String str) {
if (str == null) {
return "";
}
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/**
* Simple JSON parser implementation.
*/
private static class JsonParser {
private final String json;
private int index;
public JsonParser(String json) {
this.json = json;
this.index = 0;
}
public Object parseValue() {
skipWhitespace();
if (index >= json.length()) {
throw new IllegalArgumentException("Unexpected end of JSON");
}
char ch = json.charAt(index);
switch (ch) {
case '{':
return parseObject();
case '[':
return parseArray();
case '"':
return parseString();
case 't':
case 'f':
return parseBoolean();
case 'n':
return parseNull();
default:
if (ch == '-' || Character.isDigit(ch)) {
return parseNumber();
}
throw new IllegalArgumentException("Unexpected character: " + ch);
}
}
private Map<String, Object> parseObject() {
Map<String, Object> map = new HashMap<>();
expect('{');
skipWhitespace();
if (peek() == '}') {
index++; // consume '}'
return map;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
expect(':');
skipWhitespace();
Object value = parseValue();
map.put(key, value);
skipWhitespace();
char ch = peek();
if (ch == '}') {
index++; // consume '}'
break;
} else if (ch == ',') {
index++; // consume ','
} else {
throw new IllegalArgumentException("Expected ',' or '}', got: " + ch);
}
}
return map;
}
private List<Object> parseArray() {
List<Object> list = new ArrayList<>();
expect('[');
skipWhitespace();
if (peek() == ']') {
index++; // consume ']'
return list;
}
while (true) {
skipWhitespace();
Object value = parseValue();
list.add(value);
skipWhitespace();
char ch = peek();
if (ch == ']') {
index++; // consume ']'
break;
} else if (ch == ',') {
index++; // consume ','
} else {
throw new IllegalArgumentException("Expected ',' or ']', got: " + ch);
}
}
return list;
}
private String parseString() {
expect('"');
StringBuilder sb = new StringBuilder();
while (index < json.length()) {
char ch = json.charAt(index);
if (ch == '"') {
index++; // consume closing quote
return sb.toString();
} else if (ch == '\\') {
index++; // consume backslash
if (index >= json.length()) {
throw new IllegalArgumentException("Unexpected end of JSON in string escape");
}
char escaped = json.charAt(index);
switch (escaped) {
case '"':
sb.append('"');
break;
case '\\':
sb.append('\\');
break;
case '/':
sb.append('/');
break;
case 'b':
sb.append('\b');
break;
case 'f':
sb.append('\f');
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
default:
throw new IllegalArgumentException("Invalid escape sequence: \\" + escaped);
}
index++;
} else {
sb.append(ch);
index++;
}
}
throw new IllegalArgumentException("Unterminated string");
}
private Number parseNumber() {
int start = index;
if (peek() == '-') {
index++;
}
if (!Character.isDigit(peek())) {
throw new IllegalArgumentException("Invalid number format");
}
// Parse integer part
if (peek() == '0') {
index++;
} else {
while (index < json.length() && Character.isDigit(peek())) {
index++;
}
}
boolean isDouble = false;
// Parse decimal part
if (index < json.length() && peek() == '.') {
isDouble = true;
index++;
if (index >= json.length() || !Character.isDigit(peek())) {
throw new IllegalArgumentException("Invalid number format");
}
while (index < json.length() && Character.isDigit(peek())) {
index++;
}
}
// Parse exponent part
if (index < json.length() && (peek() == 'e' || peek() == 'E')) {
isDouble = true;
index++;
if (index < json.length() && (peek() == '+' || peek() == '-')) {
index++;
}
if (index >= json.length() || !Character.isDigit(peek())) {
throw new IllegalArgumentException("Invalid number format");
}
while (index < json.length() && Character.isDigit(peek())) {
index++;
}
}
String numberStr = json.substring(start, index);
try {
if (isDouble) {
return Double.parseDouble(numberStr);
} else {
long longValue = Long.parseLong(numberStr);
if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) {
return (int) longValue;
} else {
return longValue;
}
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid number format: " + numberStr);
}
}
private Boolean parseBoolean() {
if (json.substring(index).startsWith("true")) {
index += 4;
return Boolean.TRUE;
} else if (json.substring(index).startsWith("false")) {
index += 5;
return Boolean.FALSE;
} else {
throw new IllegalArgumentException("Invalid boolean value");
}
}
private Object parseNull() {
if (json.substring(index).startsWith("null")) {
index += 4;
return null;
} else {
throw new IllegalArgumentException("Invalid null value");
}
}
private void expect(char expected) {
if (index >= json.length() || json.charAt(index) != expected) {
throw new IllegalArgumentException("Expected '" + expected + "'");
}
index++;
}
private char peek() {
if (index >= json.length()) {
throw new IllegalArgumentException("Unexpected end of JSON");
}
return json.charAt(index);
}
private void skipWhitespace() {
while (index < json.length() && Character.isWhitespace(json.charAt(index))) {
index++;
}
}
}
}

View File

@@ -0,0 +1,405 @@
package dev.alexzaw.rest4i.util;
import dev.alexzaw.rest4i.model.ApiResponse;
import dev.alexzaw.rest4i.model.ErrorInfo;
import dev.alexzaw.rest4i.model.PaginationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Map;
/**
* Response helper utility for creating standardized REST responses using the ApiResponse wrapper.
*
* This utility provides convenient methods for creating consistent HTTP responses
* across all REST4i integration services while maintaining compatibility with
* existing IBM i RSE API patterns.
*
* @author alexzaw
*/
public class ResponseHelper {
/**
* Creates a successful response with data and HTTP 200 status.
*
* @param <T> The type of data
* @param data The response data
* @return JAX-RS Response with HTTP 200 and JSON content
*/
public static <T> Response ok(T data) {
return Response.ok(ApiResponse.success(data))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a successful response with data and message.
*
* @param <T> The type of data
* @param data The response data
* @param message Success message
* @return JAX-RS Response with HTTP 200 and JSON content
*/
public static <T> Response ok(T data, String message) {
return Response.ok(ApiResponse.success(data, message))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a successful response with just a message (no data).
*
* @param message Success message
* @return JAX-RS Response with HTTP 200 and JSON content
*/
public static Response ok(String message) {
return Response.ok(ApiResponse.success(message))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a successful paginated response.
*
* @param <T> The type of data
* @param data The response data
* @param pagination Pagination information
* @return JAX-RS Response with HTTP 200 and JSON content
*/
public static <T> Response ok(T data, PaginationInfo pagination) {
return Response.ok(ApiResponse.success(data, pagination))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a successful response with custom metadata.
*
* @param <T> The type of data
* @param data The response data
* @param metadata Custom metadata to include
* @return JAX-RS Response with HTTP 200 and JSON content
*/
public static <T> Response ok(T data, Map<String, Object> metadata) {
ApiResponse<T> response = ApiResponse.success(data);
response.setMetadata(metadata);
return Response.ok(response)
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a created response (HTTP 201) with location header.
*
* @param <T> The type of data
* @param data The created resource data
* @param location The location of the created resource
* @return JAX-RS Response with HTTP 201 and JSON content
*/
public static <T> Response created(T data, URI location) {
return Response.created(location)
.entity(ApiResponse.success(data, "Resource created successfully"))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a created response (HTTP 201) with location header and message.
*
* @param <T> The type of data
* @param data The created resource data
* @param location The location of the created resource
* @param message Custom creation message
* @return JAX-RS Response with HTTP 201 and JSON content
*/
public static <T> Response created(T data, URI location, String message) {
return Response.created(location)
.entity(ApiResponse.success(data, message))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates an accepted response (HTTP 202) for async operations.
*
* @param message Processing message
* @return JAX-RS Response with HTTP 202 and JSON content
*/
public static Response accepted(String message) {
return Response.accepted(ApiResponse.success(message))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a no content response (HTTP 204).
*
* @return JAX-RS Response with HTTP 204
*/
public static Response noContent() {
return Response.noContent().build();
}
/**
* Creates a bad request response (HTTP 400).
*
* @param message Error message
* @return JAX-RS Response with HTTP 400 and JSON content
*/
public static Response badRequest(String message) {
ErrorInfo error = new ErrorInfo("BAD_REQUEST", message, 400);
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a bad request response with validation errors.
*
* @param message Main error message
* @param validationErrors Specific validation errors
* @return JAX-RS Response with HTTP 400 and JSON content
*/
public static Response badRequest(String message, java.util.List<ErrorInfo.ValidationError> validationErrors) {
ErrorInfo error = ErrorInfo.validation(message, validationErrors);
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates an unauthorized response (HTTP 401).
*
* @param message Error message
* @return JAX-RS Response with HTTP 401 and JSON content
*/
public static Response unauthorized(String message) {
ErrorInfo error = ErrorInfo.authentication(message);
return Response.status(Response.Status.UNAUTHORIZED)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a forbidden response (HTTP 403).
*
* @param message Error message
* @return JAX-RS Response with HTTP 403 and JSON content
*/
public static Response forbidden(String message) {
ErrorInfo error = ErrorInfo.authorization(message);
return Response.status(Response.Status.FORBIDDEN)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a not found response (HTTP 404).
*
* @param message Error message
* @return JAX-RS Response with HTTP 404 and JSON content
*/
public static Response notFound(String message) {
ErrorInfo error = ErrorInfo.notFound(message);
return Response.status(Response.Status.NOT_FOUND)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a conflict response (HTTP 409).
*
* @param message Error message
* @return JAX-RS Response with HTTP 409 and JSON content
*/
public static Response conflict(String message) {
ErrorInfo error = ErrorInfo.conflict(message);
return Response.status(Response.Status.CONFLICT)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a request timeout response (HTTP 408).
*
* @param message Error message
* @return JAX-RS Response with HTTP 408 and JSON content
*/
public static Response requestTimeout(String message) {
ErrorInfo error = ErrorInfo.timeout(message);
return Response.status(408) // Request Timeout
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates an internal server error response (HTTP 500).
*
* @param message Error message
* @return JAX-RS Response with HTTP 500 and JSON content
*/
public static Response serverError(String message) {
ErrorInfo error = ErrorInfo.serverError(message);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a response from an exception.
*
* @param exception The exception
* @return JAX-RS Response with appropriate HTTP status and JSON content
*/
public static Response fromException(Exception exception) {
ErrorInfo error = ErrorInfo.fromException(exception);
Response.Status status = getStatusFromError(error);
return Response.status(status)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a response from an exception with additional context.
*
* @param exception The exception
* @param request The HTTP request for context
* @return JAX-RS Response with appropriate HTTP status and JSON content
*/
public static Response fromException(Exception exception, HttpServletRequest request) {
ErrorInfo error = ErrorInfo.fromException(exception);
// Add request context
if (request != null) {
error.setPath(request.getRequestURI());
error.setMethod(request.getMethod());
}
Response.Status status = getStatusFromError(error);
return Response.status(status)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a custom error response.
*
* @param httpStatus HTTP status code
* @param errorCode Error code
* @param message Error message
* @return JAX-RS Response with specified status and JSON content
*/
public static Response error(int httpStatus, String errorCode, String message) {
ErrorInfo error = new ErrorInfo(errorCode, message, httpStatus);
return Response.status(httpStatus)
.entity(ApiResponse.error(error))
.type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
.build();
}
/**
* Creates a response with custom content type.
*
* @param <T> The type of data
* @param data The response data
* @param contentType The content type
* @return JAX-RS Response with specified content type
*/
public static <T> Response custom(T data, String contentType) {
if (data instanceof ApiResponse) {
return Response.ok(data).type(contentType).build();
} else {
return Response.ok(ApiResponse.success(data)).type(contentType).build();
}
}
/**
* Creates a response with custom status and content type.
*
* @param <T> The type of data
* @param status HTTP status
* @param data The response data
* @param contentType The content type
* @return JAX-RS Response with specified status and content type
*/
public static <T> Response custom(Response.Status status, T data, String contentType) {
if (data instanceof ApiResponse) {
return Response.status(status).entity(data).type(contentType).build();
} else {
return Response.status(status).entity(ApiResponse.success(data)).type(contentType).build();
}
}
/**
* Creates a response for file download.
*
* @param data The file data
* @param fileName The file name
* @param contentType The content type
* @return JAX-RS Response configured for file download
*/
public static Response download(Object data, String fileName, String contentType) {
return Response.ok(data)
.type(contentType)
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.header("Content-Transfer-Encoding", "binary")
.build();
}
/**
* Gets the appropriate HTTP status from an ErrorInfo object.
*
* @param error The error information
* @return The corresponding HTTP status
*/
private static Response.Status getStatusFromError(ErrorInfo error) {
int httpStatus = error.getHttpStatus();
switch (httpStatus) {
case 400: return Response.Status.BAD_REQUEST;
case 401: return Response.Status.UNAUTHORIZED;
case 403: return Response.Status.FORBIDDEN;
case 404: return Response.Status.NOT_FOUND;
case 409: return Response.Status.CONFLICT;
case 500: return Response.Status.INTERNAL_SERVER_ERROR;
default: return Response.Status.INTERNAL_SERVER_ERROR;
}
}
/**
* Adds CORS headers to a response builder.
*
* @param responseBuilder The response builder
* @return The response builder with CORS headers
*/
public static Response.ResponseBuilder withCors(Response.ResponseBuilder responseBuilder) {
return responseBuilder
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Token")
.header("Access-Control-Max-Age", "86400");
}
/**
* Creates an OPTIONS response for CORS preflight requests.
*
* @return JAX-RS Response for CORS preflight
*/
public static Response corsOptions() {
return withCors(Response.ok()).build();
}
}

View File

@@ -0,0 +1,475 @@
package dev.alexzaw.rest4i.util;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Bin4;
import com.ibm.as400.access.AS400Bin8;
import com.ibm.as400.access.AS400UnsignedBin4;
import com.ibm.as400.access.CharConverter;
import com.ibm.as400.access.IFSFile;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.QSYSObjectPathName;
import com.ibm.as400.access.ServiceProgramCall;
import dev.alexzaw.rest4i.api.CLCommandAPIImpl;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWANotFoundException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class SystemObject {
protected AS400 _system = null;
protected IFSFile _ifsFile = null;
protected String _ifsPath = null;
protected String _ifsPathOriginal = null;
protected QSYSObjectPathName _qsysPath = null;
protected boolean _iASPQSYSObject = false;
protected boolean _isQSYSPFMember = false;
protected long _size = -1L;
protected String _description = "";
protected int _ccsid = -1;
protected boolean _getAttrCalled = false;
protected UNIXErrno _unixErrno = null;
public SystemObject(AS400 sys, String path) {
path = CommonUtil.normalizePath(path);
initObject(sys, new IFSFile(sys, path), path);
}
public SystemObject(IFSFile ifsFile) {
initObject(ifsFile.getSystem(), ifsFile, ifsFile.getPath());
}
protected void initObject(AS400 sys, IFSFile ifsFile, String path) {
this._ifsFile = ifsFile;
this._ifsPathOriginal = path;
this._ifsPath = ifsFile.getCanonicalPath();
this._system = sys;
if (isQSYSObject(sys, path)) {
String tempIFSPath = this._ifsPath;
if (tempIFSPath.equalsIgnoreCase("/QSYS.LIB"))
tempIFSPath = String.valueOf(tempIFSPath) + "/QSYS.LIB";
this._qsysPath = getQSYSObjectPathName(tempIFSPath);
this._isQSYSPFMember = isQSYSPFMBR(this._qsysPath);
}
try {
int errno = doQp0lGetAttr(this._system, this, false);
if (errno == 0)
this._getAttrCalled = true;
} catch (Exception exception) {}
}
public static boolean isQSYSObject(AS400 system, String path) {
boolean isQSYSObject = false;
path = CommonUtil.normalizePath(path);
String tempPath = path.toUpperCase();
if (tempPath.startsWith("/QSYS.LIB/") || tempPath.equals("/QSYS.LIB")) {
isQSYSObject = true;
} else {
String mountPoint = path;
int endOfFirstPart = tempPath.indexOf("/QSYS.LIB", 1);
if (endOfFirstPart == -1) {
isQSYSObject = false;
} else {
mountPoint = path.substring(0, endOfFirstPart);
IFSFile tempIFSFile = new IFSFile(system, mountPoint);
try {
if (tempIFSFile.getASP() > 32)
isQSYSObject = true;
} catch (Exception e) {
AsyncLogger.traceDebug("initObject", e, "getASP failed. path='%s'", new Object[] { path });
}
}
}
return isQSYSObject;
}
public int getCCSID() throws Exception {
if (this._getAttrCalled)
return this._ccsid;
return this._ifsFile.getCCSID();
}
public long getSize() throws Exception {
if (this._getAttrCalled)
return this._size;
return this._ifsFile.length();
}
public String getPath() {
return this._ifsPath;
}
public QSYSObjectPathName getQSYSPath() {
return this._qsysPath;
}
public IFSFile getIFSFile() {
return this._ifsFile;
}
public boolean delete() {
try {
return this._ifsFile.delete();
} catch (IOException IGNORE) {
AsyncLogger.traceDebug("delete", IGNORE, "path='%s'", new Object[] { this._ifsPath });
return false;
}
}
public boolean exists() throws Exception {
if (this._getAttrCalled)
return true;
return this._ifsFile.exists();
}
public static QSYSObjectPathName getQSYSObjectPathName(String path) {
QSYSObjectPathName qsysPath = null;
if (CommonUtil.hasContent(path))
try {
qsysPath = new QSYSObjectPathName(path);
} catch (Exception IGNORE) {
qsysPath = null;
}
return qsysPath;
}
public boolean isFile() throws Exception {
return this._ifsFile.isFile();
}
public boolean isDirectory() throws Exception {
return this._ifsFile.isDirectory();
}
public boolean isQSYSObject() {
return (this._qsysPath != null);
}
public boolean isQSYSPF() throws Exception {
if (!isQSYSFile())
return false;
String st = getSubtype();
return (st != null && st.equals("PF"));
}
public boolean isQSYSFile() {
return (isQSYSObject() && this._qsysPath.getObjectType().equalsIgnoreCase("FILE"));
}
public boolean isQSYSLIB() {
return (isQSYSObject() && this._qsysPath.getObjectType().equalsIgnoreCase("LIB"));
}
public static boolean isQSYSPFMBR(QSYSObjectPathName qsysPath) {
return (qsysPath != null && qsysPath.getObjectType().equalsIgnoreCase("MBR"));
}
public boolean isQSYSPFMBR() {
return this._isQSYSPFMember;
}
public static QSYSObjectPathName getQSYSPhysicalFilePath(QSYSObjectPathName qsysPath) {
if (!isQSYSPFMBR(qsysPath))
return null;
StringBuilder sb = new StringBuilder();
if (CommonUtil.hasContent(qsysPath.getAspName()))
sb.append("/").append(qsysPath.getAspName());
sb.append("/QSYS.LIB");
if (!qsysPath.getLibraryName().equalsIgnoreCase("QSYS"))
sb.append("/").append(qsysPath.getLibraryName()).append(".LIB");
sb.append("/").append(qsysPath.getObjectName()).append(".FILE");
return new QSYSObjectPathName(sb.toString());
}
public String getSubtype() throws Exception {
return this._ifsFile.getSubtype();
}
public String getOwnerName() throws Exception {
return this._ifsFile.getOwnerName();
}
public long getLastModified() throws Exception {
return this._ifsFile.lastModified() / 1000L;
}
public String getDescription() throws Exception {
if (this._getAttrCalled) {
int errno = doQp0lGetAttr(this._system, this, true);
if (errno == 0)
return this._description;
}
return null;
}
public SystemObjectInfo getInfo(boolean fullDetails) throws Exception {
try {
if (fullDetails)
return new SystemObjectInfo(getCCSID(), getOwnerName(),
getLastModified(), this._ifsPath, getSize(),
isDirectory(), getSubtype(), -1, -1, getDescription());
return new SystemObjectInfo(this._ifsPath, isDirectory(), getSubtype(), getDescription());
} catch (Exception e) {
AsyncLogger.traceDebug("getSystemObjectInfo", e, "path='%s'", new Object[] { this._ifsPath });
return new SystemObjectInfo(this._ifsPath, fullDetails);
}
}
public static class SystemObjectInfo {
public boolean inAccessible = false;
public boolean fullDetails = false;
public int ccsid;
public String owner;
public String description;
public long lastModified;
public String path;
public long size;
public boolean isDir;
public String subType;
public int recordLength;
public int numberOfRecords;
public SystemObjectInfo(int ccsid, String owner, long lastModified, String path, long size, boolean isDir, String subType, int numberOfRecords, int recordLength, String description) {
this.fullDetails = true;
this.ccsid = ccsid;
this.owner = owner;
this.lastModified = lastModified;
this.path = path;
this.size = size;
this.isDir = isDir;
this.subType = subType;
this.numberOfRecords = numberOfRecords;
this.recordLength = recordLength;
this.description = description;
}
public SystemObjectInfo(String path, boolean isDir, String subType, String description) {
this.path = path;
this.isDir = isDir;
this.subType = subType;
this.description = description;
}
public SystemObjectInfo(String path, boolean fullDetails) {
this.fullDetails = fullDetails;
this.inAccessible = true;
this.path = path;
}
public void toJSON(JSONSerializer json) {
json.startObject();
json.add("path", this.path);
json.add("description", this.inAccessible ? null : this.description);
json.add("isDir", this.inAccessible ? null : Boolean.valueOf(this.isDir));
json.add("subType", this.inAccessible ? null : this.subType);
if (this.fullDetails) {
json.add("owner", this.inAccessible ? null : this.owner);
json.add("ccsid", this.inAccessible ? null : Integer.valueOf(this.ccsid));
json.add("lastModified", this.inAccessible ? null : Long.valueOf(this.lastModified));
json.add("size", this.inAccessible ? null : Long.valueOf(this.size));
json.add("recordLength", this.inAccessible ? null : Integer.valueOf(this.recordLength));
json.add("numberOfRecords", this.inAccessible ? null : Integer.valueOf(this.numberOfRecords));
}
json.endObject();
}
}
public List<SystemObject> listDir(boolean includeHidden, String pattern, String subtypeFilter) throws Exception {
if (!exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { this._ifsPath });
if (!isDirectory())
throw new RestWABadRequestException("Object referenced by the path '%s' is not a directory.", new Object[] { this._ifsPath });
List<SystemObject> list = new ArrayList<>();
List<SystemObject> nondirlist = new ArrayList<>();
int patternType = includeHidden ? 1 : 0;
this._ifsFile.setPatternMatching(patternType);
IFSFile[] ifsfiles = null;
if (pattern != null) {
ifsfiles = this._ifsFile.listFiles(null, pattern);
} else {
ifsfiles = this._ifsFile.listFiles();
}
SystemObject sysObj = null;
byte b;
int i;
IFSFile[] arrayOfIFSFile1;
for (i = (arrayOfIFSFile1 = ifsfiles).length, b = 0; b < i; ) {
IFSFile f = arrayOfIFSFile1[b];
sysObj = new SystemObject(f);
if (subtypeFilter == null || subtypeFilter.equalsIgnoreCase("*ALL") || (
sysObj.getSubtype() != null && sysObj.getSubtype().equals(subtypeFilter)))
if (sysObj.isDirectory()) {
list.add(sysObj);
} else {
nondirlist.add(sysObj);
}
b++;
}
list = sortList(list);
list.addAll(sortList(nondirlist));
return list;
}
public void mkdir() throws Exception {
String command = String.format("QSYS/MKDIR DIR('%s') DTAAUT(*EXCLUDE) OBJAUT(*NONE) CRTOBJAUD(*SYSVAL)CRTOBJSCAN(*PARENT) RSTDRNMUNL(*NO)", new Object[] { this._ifsPath });
CLCommandAPIImpl.runClCommand(this._system, command);
}
private static Comparator<SystemObject> _sysObjectComparator = new Comparator<SystemObject>() {
public int compare(SystemObject o1, SystemObject o2) {
String o1Lower = o1.getName().toLowerCase();
String o2Lower = o2.getName().toLowerCase();
if (o1Lower.equals(o2Lower))
return o1Lower.equals(o1.getName()) ? -1 : 1;
return o1Lower.compareTo(o2Lower);
}
};
private static final byte ebcdicDelim = 97;
private static final byte nullDelim = 0;
public static List<SystemObject> sortList(List<SystemObject> list) {
if (list.isEmpty())
return list;
Collections.sort(list, _sysObjectComparator);
return list;
}
public String getName() {
return this._ifsFile.getName();
}
private static final byte[] hexZeros = new byte[50];
private static final AS400Bin4 intConverter = new AS400Bin4();
private static final AS400UnsignedBin4 uintConverter = new AS400UnsignedBin4();
private static final AS400Bin8 longConverter = new AS400Bin8();
public static final int QP0L_ATTR_DATA_SIZE_64 = 14;
public static final int QP0L_ATTR_CCSID = 27;
public static final int QP0L_ATTR_TEXT = 48;
private static int doQp0lGetAttr(AS400 sys, SystemObject sysObject, boolean getDescription) throws Exception {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ServiceProgramCall spc = CommonUtil.getServiceProgramCall(sys, "/QSYS.LIB/QP0LLIB2.SRVPGM", "Qp0lGetAttr", 7, false, true);
CharConverter charConverter = new CharConverter(sys.getCcsid(), sys);
ProgramParameter[] pgmParmList = spc.getParameterList();
byte[] pathBytes = null;
byte delim = 0;
if (sysObject.isQSYSObject() && CommonUtil.isPathProblematic(sysObject.getQSYSPath())) {
QSYSObjectPathName qsysPath = sysObject.getQSYSPath();
String asp = qsysPath.getAspName();
String lib = qsysPath.getLibraryName();
String obj = qsysPath.getObjectName();
String mbr = qsysPath.getMemberName();
if (!asp.isEmpty()) {
bo.write(delim);
if (asp.charAt(0) == '/') {
bo.write(charConverter.stringToByteArray(asp.substring(1)));
} else {
bo.write(charConverter.stringToByteArray(asp));
}
}
if (!lib.isEmpty()) {
bo.write(delim);
bo.write(charConverter.stringToByteArray(String.valueOf(lib) + ".LIB"));
}
if (!obj.isEmpty()) {
bo.write(delim);
bo.write(charConverter.stringToByteArray(String.valueOf(obj) + "." + qsysPath.getObjectType()));
}
if (!mbr.isEmpty()) {
bo.write(delim);
bo.write(charConverter.stringToByteArray(String.valueOf(mbr) + ".MBR"));
}
pathBytes = bo.toByteArray();
} else {
delim = 97;
pathBytes = charConverter.stringToByteArray(sysObject.getPath());
}
bo.reset();
bo.write(hexZeros, 0, 16);
bo.write(intConverter.toBytes(pathBytes.length));
bo.write(delim);
bo.write(new byte[11]);
bo.write(pathBytes);
pgmParmList[0].setInputData(bo.toByteArray());
int numAttrs = getDescription ? 3 : 2;
bo.reset();
bo.write(intConverter.toBytes(numAttrs));
bo.write(intConverter.toBytes(14));
bo.write(intConverter.toBytes(27));
if (getDescription)
bo.write(intConverter.toBytes(48));
pgmParmList[1].setInputData(bo.toByteArray());
pgmParmList[2].setOutputDataLength(200);
pgmParmList[3].setParameterType(1);
pgmParmList[3].setInputData(intConverter.toBytes(200));
pgmParmList[4].setOutputDataLength(4);
pgmParmList[5].setOutputDataLength(4);
pgmParmList[6].setParameterType(1);
pgmParmList[6].setInputData(intConverter.toBytes(1));
spc.run();
int errno = 0;
if (spc.getIntegerReturnValue() != 0)
errno = spc.getErrno();
sysObject._size = -1L;
sysObject._description = "";
sysObject._ccsid = -1;
if (errno != 0) {
AsyncLogger.traceDebug("doQp0lGetAttr", "errno='%d'", new Object[] { Integer.valueOf(errno) });
return errno;
}
byte[] outBuf = pgmParmList[2].getOutputData();
int offset = 0;
long nextOffset = -1L;
while (nextOffset != 0L) {
nextOffset = uintConverter.toLong(outBuf, offset + 0);
long attrID = uintConverter.toLong(outBuf, offset + 4);
long attrDataSize = uintConverter.toLong(outBuf, offset + 8);
if (attrDataSize > 0L)
if (attrID == 14L) {
sysObject._size = longConverter.toLong(outBuf, offset + 16);
} else if (attrID == 27L) {
sysObject._ccsid = intConverter.toInt(outBuf, offset + 16);
} else if (attrID == 48L) {
sysObject._description = charConverter.byteArrayToString(outBuf, offset + 16, (int)attrDataSize);
sysObject._description = sysObject._description.replace('\0', ' ');
sysObject._description = CommonUtil.trimRight(sysObject._description);
}
offset = (int)nextOffset;
}
return 0;
}
}

View File

@@ -0,0 +1,13 @@
package dev.alexzaw.rest4i.util;
import com.ibm.as400.access.AS400;
public final class SystemObjectFactory {
public static SystemObject getSystemObject(AS400 system, String path) {
path = CommonUtil.normalizePath(path);
if (SystemObject.isQSYSObject(system, path))
return new SystemObjectQSYS(system, path);
return new SystemObject(system, path);
}
}

View File

@@ -0,0 +1,403 @@
package dev.alexzaw.rest4i.util;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Exception;
import com.ibm.as400.access.AS400FileRecordDescription;
import com.ibm.as400.access.ExtendedIOException;
import com.ibm.as400.access.FieldDescription;
import com.ibm.as400.access.IFSFile;
import com.ibm.as400.access.MemberDescription;
import com.ibm.as400.access.MemberList;
import com.ibm.as400.access.ObjectDescription;
import com.ibm.as400.access.ObjectDoesNotExistException;
import com.ibm.as400.access.ObjectList;
import com.ibm.as400.access.QSYSObjectPathName;
import com.ibm.as400.access.RecordFormat;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWANotFoundException;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
public class SystemObjectQSYS extends SystemObject {
private MemberDescription _mbrDesc = null;
private ObjectDescription _objDesc = null;
public SystemObjectQSYS(AS400 sys, String path) {
super(sys, path);
}
public SystemObjectQSYS(AS400 system, MemberDescription mbrDesc) {
super(system, mbrDesc.getPath());
this._mbrDesc = mbrDesc;
}
public SystemObjectQSYS(AS400 system, ObjectDescription objDesc) {
super(system, objDesc.getPath());
this._objDesc = objDesc;
}
public int getCCSID() throws Exception {
int ccsid = -1;
if (this._getAttrCalled)
ccsid = this._ccsid;
return ccsid;
}
public long getSize() throws Exception {
long size = -1L;
try {
if (this._getAttrCalled)
size = this._size;
if (size == -1L)
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
int dataSpaceSize = ((Integer)this._mbrDesc.getValue(14)).intValue();
int dataSpaceMultiplier = ((Integer)this._mbrDesc.getValue(24)).intValue();
size = (dataSpaceMultiplier * dataSpaceSize);
} else {
ensureObjectDescriptionSet(this._qsysPath);
size = ((Long)this._objDesc.getValue(701)).longValue();
}
} catch (Exception e) {
AsyncLogger.traceDebug("getSize", e, "path='%s'", new Object[] { this._ifsPath });
}
return size;
}
public boolean delete() {
return super.delete();
}
public boolean exists() throws Exception {
if (this._ifsPath.equalsIgnoreCase("/QSYS.LIB"))
return true;
if (this._ifsPath.equalsIgnoreCase("/QSYS.LIB/QSYS.LIB"))
return false;
if (this._getAttrCalled)
return true;
if (this._qsysPath == null)
return false;
try {
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
this._mbrDesc.getValue(37);
return true;
}
ensureObjectDescriptionSet(this._qsysPath);
return this._objDesc.exists();
} catch (ObjectDoesNotExistException e) {
return false;
} catch (ExtendedIOException e) {
int rc = e.getReturnCode();
AsyncLogger.traceDebug("exists() - qsys", "%s [path=%s] [rc=%d]", new Object[] { e.getMessage(), this._ifsPath, Integer.valueOf(rc) });
if (rc == 2 || rc == 3)
return false;
} catch (AS400Exception e) {
AsyncLogger.traceDebug("getSystemObjectInfo", (Throwable)e, "path='%s'", new Object[] { this._ifsPath });
String errorMsg = e.getMessage();
if (errorMsg != null)
if (errorMsg.indexOf("CPD3C31") != -1 ||
errorMsg.indexOf("CPF954F") != -1 ||
errorMsg.indexOf("CPF9551") != -1 ||
errorMsg.indexOf("CPF959E") != -1 ||
errorMsg.indexOf("CPF98A1") != -1 ||
errorMsg.indexOf("CPF980F") != -1 ||
errorMsg.indexOf("CPF9801") != -1 ||
errorMsg.indexOf("CPF9810") != -1 ||
errorMsg.indexOf("CPF9811") != -1 ||
errorMsg.indexOf("CPF9812") != -1 ||
errorMsg.indexOf("CPF9814") != -1 ||
errorMsg.indexOf("CPF9815") != -1)
return false;
}
return true;
}
public boolean isFile() throws Exception {
return (exists() && isQSYSPFMBR());
}
public boolean isDirectory() throws Exception {
return (exists() && (isQSYSPF() || isQSYSLIB()));
}
public int getRecordLength() throws Exception {
int recordLength = -1;
if (!isQSYSFile() && !isQSYSPFMBR())
return -1;
if (isQSYSFile()) {
String st = getSubtype();
if (st == null || (
!st.equalsIgnoreCase("PF") && !st.equalsIgnoreCase("PF-SRC") && !st.equalsIgnoreCase("PF-DTA")))
return -1;
}
AS400FileRecordDescription af = new AS400FileRecordDescription(this._system, this._ifsPath);
RecordFormat[] rf = af.retrieveRecordFormat();
FieldDescription[] fd = rf[0].getFieldDescriptions();
recordLength = 0;
for (int i = 0; i < fd.length; i++)
recordLength += fd[i].getLength();
return recordLength;
}
private int getNumberOfRecords() {
int numberOfRecords = -1;
try {
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
numberOfRecords = ((Integer)this._mbrDesc.getValue(13)).intValue();
}
} catch (Exception e) {
AsyncLogger.traceDebug("getNumberOfRecords", e, "path='%s'", new Object[] { this._ifsPath });
numberOfRecords = -1;
}
return numberOfRecords;
}
public String getSubtype() throws Exception {
String subtype = null;
try {
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
subtype = (String)this._mbrDesc.getValue(5);
} else {
ensureObjectDescriptionSet(this._qsysPath);
subtype = this._objDesc.getValueAsString(202);
}
} catch (Exception e) {
AsyncLogger.traceDebug("getSubtype", e, "path='%s'", new Object[] { this._ifsPath });
subtype = null;
}
return subtype;
}
public String getOwnerName() throws Exception {
String owner = null;
try {
ensureObjectDescriptionSet(this._qsysPath);
owner = this._objDesc.getValueAsString(302);
} catch (Exception e) {
AsyncLogger.traceDebug("getSubtype", e, "path='%s'", new Object[] { this._ifsPath });
owner = null;
}
return owner;
}
public long getLastModified() throws Exception {
long lastModified = 0L;
Date date = null;
try {
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
date = (Date)this._mbrDesc.getValue(17);
if (date == null)
date = (Date)this._mbrDesc.getValue(6);
} else {
ensureObjectDescriptionSet(this._qsysPath);
date = (Date)this._objDesc.getValue(305);
if (date == null)
date = (Date)this._objDesc.getValue(304);
}
if (date != null)
lastModified = date.getTime() / 1000L;
} catch (Exception e) {
AsyncLogger.traceDebug("getLastModified", e, "path='%s'", new Object[] { this._ifsPath });
lastModified = 0L;
}
return lastModified;
}
public String getDescription() throws Exception {
String descr = null;
try {
descr = super.getDescription();
if (descr != null)
return descr;
if (isQSYSPFMBR()) {
ensureMemberDescriptionSet(this._qsysPath);
descr = (String)this._mbrDesc.getValue(8);
} else {
ensureObjectDescriptionSet(this._qsysPath);
descr = this._objDesc.getValueAsString(203);
}
} catch (Exception e) {
AsyncLogger.traceDebug("getDescription", e, "path='%s'", new Object[] { this._ifsPath });
descr = null;
}
return descr;
}
public String getQSYSPFSubtype() {
String subtype = "PF";
try {
subtype = (new IFSFile(this._system, this._ifsPath)).isSourcePhysicalFile() ? (String.valueOf(subtype) + "-SRC") : (String.valueOf(subtype) + "-DTA");
} catch (Exception e) {
AsyncLogger.traceDebug("getQSYSPFSubtype", e, "path='%s'", new Object[] { this._ifsPath });
subtype = "PF";
}
return subtype;
}
public String getName() {
if (this._isQSYSPFMember)
return this._qsysPath.getMemberName();
if (CommonUtil.hasContent(this._qsysPath.getObjectName()))
return String.valueOf(this._qsysPath.getObjectName()) + "." + this._qsysPath.getObjectType();
return String.valueOf(this._qsysPath.getLibraryName()) + ".LIB";
}
public List<SystemObject> listDir(boolean includeHidden, String pattern, String subtypeFilter) throws Exception {
if (!exists())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { this._ifsPath });
if (!isDirectory())
throw new RestWABadRequestException("Object referenced by the path '%s' is not a directory.", new Object[] { this._ifsPath });
List<SystemObject> list = new ArrayList<>();
SystemObjectQSYS qsysObj = null;
if (isQSYSPF()) {
MemberList memberList = new MemberList(this._system, this._qsysPath);
memberList.addAttribute(8);
memberList.addAttribute(5);
memberList.load();
MemberDescription[] mbrDescList = memberList.getMemberDescriptions();
if (pattern != null) {
if (pattern.toUpperCase().endsWith(".MBR")) {
pattern = pattern.substring(0, pattern.length() - 4);
} else if (pattern.endsWith(".*")) {
pattern = pattern.substring(0, pattern.length() - 2);
}
if (pattern.equals("*") || pattern.isEmpty())
pattern = null;
}
Pattern p = null;
if (pattern != null) {
String regExpr = "";
if (pattern.endsWith("*")) {
pattern = pattern.substring(0, pattern.length() - 1);
regExpr = "(.*)";
}
p = Pattern.compile(String.valueOf(Pattern.quote(pattern)) + regExpr, 2);
}
byte b;
int i;
MemberDescription[] arrayOfMemberDescription1;
for (i = (arrayOfMemberDescription1 = mbrDescList).length, b = 0; b < i; ) {
MemberDescription mbrDesc = arrayOfMemberDescription1[b];
String fileName = (new File(mbrDesc.getPath())).getName();
if (p == null || p.matcher(fileName).matches()) {
qsysObj = new SystemObjectQSYS(this._system, mbrDesc);
if (subtypeFilter == null || subtypeFilter.equalsIgnoreCase("*ALL") || (
qsysObj.getSubtype() != null && qsysObj.getSubtype().equalsIgnoreCase(subtypeFilter)))
list.add(qsysObj);
}
b++;
}
list = sortList(list);
} else {
List<SystemObject> nondirlist = new ArrayList<>();
ObjectList objList = null;
String lib = this._qsysPath.getLibraryName();
if (this._qsysPath.getObjectType().equals("LIB") && this._qsysPath.getLibraryName().equalsIgnoreCase("QSYS"))
lib = this._qsysPath.getObjectName();
String suffix = null;
if (pattern != null) {
int idx = pattern.indexOf('.');
if (idx != -1) {
suffix = "*" + pattern.substring(idx + 1);
pattern = pattern.substring(0, idx);
}
}
if (pattern == null || pattern.equals("*"))
pattern = "*ALL";
if (subtypeFilter == null)
subtypeFilter = (suffix == null) ? "*ALL" : suffix;
objList = new ObjectList(this._system, lib, pattern, subtypeFilter);
ObjectDescription[] objects = objList.getObjects(-1, 0);
SystemObject sysObj = null;
byte b;
int i;
ObjectDescription[] arrayOfObjectDescription1;
for (i = (arrayOfObjectDescription1 = objects).length, b = 0; b < i; ) {
ObjectDescription objDesc = arrayOfObjectDescription1[b];
sysObj = new SystemObjectQSYS(this._system, objDesc);
if (sysObj.isDirectory()) {
list.add(sysObj);
} else {
nondirlist.add(sysObj);
}
b++;
}
list = sortList(list);
list.addAll(sortList(nondirlist));
}
return list;
}
public SystemObject.SystemObjectInfo getInfo(boolean fullDetails) throws Exception {
try {
if (fullDetails || !isQSYSPFMBR()) {
QSYSObjectPathName objdescPath = isQSYSPFMBR() ? getQSYSPhysicalFilePath(this._qsysPath) : this._qsysPath;
ensureObjectDescriptionSet(objdescPath);
}
if (isQSYSPFMBR())
ensureMemberDescriptionSet(this._qsysPath);
String subtype = getSubtype();
if (subtype == null)
return new SystemObject.SystemObjectInfo(this._ifsPath, fullDetails);
if (!isQSYSPFMBR() && subtype.equalsIgnoreCase("PF"))
subtype = getQSYSPFSubtype();
if (!fullDetails)
return new SystemObject.SystemObjectInfo(this._ifsPath, isDirectory(), subtype, getDescription());
int numberOfRecords = -1;
if (isQSYSPFMBR())
numberOfRecords = getNumberOfRecords();
return new SystemObject.SystemObjectInfo(getCCSID(), getOwnerName(),
getLastModified(), this._ifsPath, getSize(), isDirectory(),
subtype, numberOfRecords, getRecordLength(), getDescription());
} catch (ObjectDoesNotExistException e) {
return null;
} catch (ExtendedIOException e) {
AsyncLogger.traceDebug("getSystemObjectInfo", (Throwable)e, "path='%s'", new Object[] { this._ifsPath });
int rc = e.getReturnCode();
AsyncLogger.traceDebug("getSystemObjectInfo", "%s [path=%s] [rc=%d]", new Object[] { e.getMessage(), this._ifsPath, Integer.valueOf(rc) });
if (rc == 2 || rc == 3)
return null;
return new SystemObject.SystemObjectInfo(this._ifsPath, fullDetails);
} catch (AS400Exception e) {
AsyncLogger.traceDebug("getSystemObjectInfo", (Throwable)e, "path='%s'", new Object[] { this._ifsPath });
String errorMsg = e.getMessage();
if (errorMsg != null)
if (errorMsg.indexOf("CPF954F") != -1 ||
errorMsg.indexOf("CPF9551") != -1 ||
errorMsg.indexOf("CPF959E") != -1 ||
errorMsg.indexOf("CPF98A1") != -1 ||
errorMsg.indexOf("CPF980F") != -1 ||
errorMsg.indexOf("CPF9801") != -1 ||
errorMsg.indexOf("CPF9810") != -1 ||
errorMsg.indexOf("CPF9811") != -1 ||
errorMsg.indexOf("CPF9812") != -1 ||
errorMsg.indexOf("CPF9814") != -1 ||
errorMsg.indexOf("CPF9815") != -1)
return null;
return new SystemObject.SystemObjectInfo(this._ifsPath, fullDetails);
}
}
private void ensureMemberDescriptionSet(QSYSObjectPathName qsysPath) throws Exception {
if (this._mbrDesc == null) {
this._mbrDesc = new MemberDescription(this._system, qsysPath);
this._mbrDesc.refresh();
}
}
private void ensureObjectDescriptionSet(QSYSObjectPathName qsysPath) throws Exception {
if (this._objDesc == null) {
this._objDesc = new ObjectDescription(this._system, qsysPath);
this._objDesc.refresh();
}
}
}

View File

@@ -0,0 +1,78 @@
package dev.alexzaw.rest4i.util;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Message;
import com.ibm.as400.access.MessageFile;
import dev.alexzaw.rest4i.exception.RestWABadRequestException;
import dev.alexzaw.rest4i.exception.RestWAForbiddenException;
import dev.alexzaw.rest4i.exception.RestWAInternalServerErrorException;
import dev.alexzaw.rest4i.exception.RestWANotFoundException;
public class UNIXErrno {
public static final int EINVAL = 3021;
public static final int ENOENT = 3025;
public static final int EPERM = 3027;
public static final int EBUSY = 3029;
public static final int EACCES = 3401;
public static final int ENOTDIR = 3403;
public static final int EOPNOTSUPP = 3440;
public static final int ENOENT1 = 3465;
public static final int EISDIR = 3471;
public static final int ENOTEMPTY = 3488;
public static final int EBADDIR = 3494;
public static final int EROOBJ = 3500;
public static final int ENOTAVAIL = 3535;
int errno_;
public UNIXErrno(int errno) {
this.errno_ = errno;
}
private String getMessage(AS400 system) {
String theMessage = "";
try {
MessageFile messageFile = new MessageFile(system);
messageFile.setPath("%LIBL%/QCPFMSG.MSGF");
AS400Message as400msg = messageFile.getMessage("CPE" + this.errno_);
theMessage = as400msg.getText();
} catch (Exception exception) {}
return String.valueOf(theMessage) + "(CPE" + this.errno_ + ")";
}
public boolean isNotFoundError() {
return !(this.errno_ != 3025 && this.errno_ != 3535 && this.errno_ != 3465 && this.errno_ != 3494);
}
public boolean isPermissionError() {
return !(this.errno_ != 3027 && this.errno_ != 3401);
}
public boolean isInvalidError() {
return !(this.errno_ != 3021 && this.errno_ != 3403 && this.errno_ != 3440 && this.errno_ != 3471 && this.errno_ != 3488 && this.errno_ != 3500);
}
public void generateException(AS400 system, String path) {
String message = getMessage(system);
if (isInvalidError())
throw new RestWABadRequestException("Operation on object referenced by path '%s' failed. %s", new Object[] { path, message });
if (isNotFoundError())
throw new RestWANotFoundException("Object referenced by the path '%s' is not found or inaccessible.", new Object[] { path });
if (isPermissionError())
throw new RestWAForbiddenException("Operation on object referenced by path '%s' failed. %s", new Object[] { path, message });
throw new RestWAInternalServerErrorException("Operation on object referenced by path '%s' failed. %s", new Object[] { path, message });
}
}

View File

@@ -0,0 +1,497 @@
package dev.alexzaw.rest4i.util;
import dev.alexzaw.rest4i.model.ErrorInfo.ValidationError;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* Validation utilities for input validation across REST4i integration services.
*
* Provides comprehensive validation methods for common data types, formats,
* and business rules while maintaining compatibility with IBM i constraints.
*
* @author alexzaw
*/
public class ValidationUtils {
// Common regex patterns
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
);
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^[\\+]?[1-9]?[0-9]{7,15}$"
);
private static final Pattern IP_ADDRESS_PATTERN = Pattern.compile(
"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
);
private static final Pattern HOSTNAME_PATTERN = Pattern.compile(
"^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"
);
// IBM i specific patterns
private static final Pattern IBMI_OBJECT_NAME_PATTERN = Pattern.compile(
"^[A-Za-z][A-Za-z0-9_@#$]{0,9}$"
);
private static final Pattern IBMI_LIBRARY_NAME_PATTERN = Pattern.compile(
"^[A-Za-z][A-Za-z0-9_@#$]{0,9}$"
);
private static final Pattern IBMI_USER_ID_PATTERN = Pattern.compile(
"^[A-Za-z][A-Za-z0-9_]{0,9}$"
);
// SQL validation patterns
private static final Pattern[] DANGEROUS_SQL_PATTERNS = {
Pattern.compile("(?i)\\b(DROP|DELETE|TRUNCATE|ALTER|CREATE)\\s+", Pattern.CASE_INSENSITIVE),
Pattern.compile("(?i)\\b(EXEC|EXECUTE|SP_|XP_)\\s*\\(", Pattern.CASE_INSENSITIVE),
Pattern.compile("(?i)\\b(UNION|INFORMATION_SCHEMA|SYSOBJECTS|SYSCOLUMNS)\\b", Pattern.CASE_INSENSITIVE),
Pattern.compile("(?i)(--|\\/\\*|\\*\\/|;\\s*$)", Pattern.CASE_INSENSITIVE)
};
/**
* Validation result container.
*/
public static class ValidationResult {
private boolean valid;
private List<ValidationError> errors;
public ValidationResult() {
this.valid = true;
this.errors = new ArrayList<>();
}
public ValidationResult(boolean valid) {
this();
this.valid = valid;
}
public void addError(String field, String message) {
this.valid = false;
this.errors.add(new ValidationError(field, message));
}
public void addError(String field, String message, Object rejectedValue) {
this.valid = false;
this.errors.add(new ValidationError(field, message, rejectedValue));
}
public void addError(ValidationError error) {
this.valid = false;
this.errors.add(error);
}
public boolean isValid() {
return valid && errors.isEmpty();
}
public List<ValidationError> getErrors() {
return errors;
}
public String getFirstErrorMessage() {
return errors.isEmpty() ? null : errors.get(0).getMessage();
}
}
/**
* Validates that a string is not null or empty.
*
* @param value The string to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateRequired(String value, String fieldName) {
ValidationResult result = new ValidationResult();
if (value == null || value.trim().isEmpty()) {
result.addError(fieldName, fieldName + " is required");
}
return result;
}
/**
* Validates that an object is not null.
*
* @param value The object to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateRequired(Object value, String fieldName) {
ValidationResult result = new ValidationResult();
if (value == null) {
result.addError(fieldName, fieldName + " is required");
}
return result;
}
/**
* Validates string length constraints.
*
* @param value The string to validate
* @param fieldName The field name for error messages
* @param minLength Minimum length (inclusive)
* @param maxLength Maximum length (inclusive)
* @return ValidationResult
*/
public static ValidationResult validateLength(String value, String fieldName, int minLength, int maxLength) {
ValidationResult result = new ValidationResult();
if (value != null) {
int length = value.length();
if (length < minLength) {
result.addError(fieldName, String.format("%s must be at least %d characters long", fieldName, minLength), value);
} else if (length > maxLength) {
result.addError(fieldName, String.format("%s must not exceed %d characters", fieldName, maxLength), value);
}
}
return result;
}
/**
* Validates numeric range constraints.
*
* @param value The number to validate
* @param fieldName The field name for error messages
* @param min Minimum value (inclusive)
* @param max Maximum value (inclusive)
* @return ValidationResult
*/
public static ValidationResult validateRange(Number value, String fieldName, Number min, Number max) {
ValidationResult result = new ValidationResult();
if (value != null) {
double doubleValue = value.doubleValue();
double minValue = min.doubleValue();
double maxValue = max.doubleValue();
if (doubleValue < minValue) {
result.addError(fieldName, String.format("%s must be at least %s", fieldName, min), value);
} else if (doubleValue > maxValue) {
result.addError(fieldName, String.format("%s must not exceed %s", fieldName, max), value);
}
}
return result;
}
/**
* Validates email format.
*
* @param email The email to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateEmail(String email, String fieldName) {
ValidationResult result = new ValidationResult();
if (email != null && !email.trim().isEmpty()) {
if (!EMAIL_PATTERN.matcher(email.trim()).matches()) {
result.addError(fieldName, fieldName + " is not a valid email address", email);
}
}
return result;
}
/**
* Validates phone number format.
*
* @param phone The phone number to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validatePhone(String phone, String fieldName) {
ValidationResult result = new ValidationResult();
if (phone != null && !phone.trim().isEmpty()) {
String cleanPhone = phone.replaceAll("[\\s\\-\\(\\)]", "");
if (!PHONE_PATTERN.matcher(cleanPhone).matches()) {
result.addError(fieldName, fieldName + " is not a valid phone number", phone);
}
}
return result;
}
/**
* Validates IP address format.
*
* @param ipAddress The IP address to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateIpAddress(String ipAddress, String fieldName) {
ValidationResult result = new ValidationResult();
if (ipAddress != null && !ipAddress.trim().isEmpty()) {
if (!IP_ADDRESS_PATTERN.matcher(ipAddress.trim()).matches()) {
result.addError(fieldName, fieldName + " is not a valid IP address", ipAddress);
}
}
return result;
}
/**
* Validates hostname format.
*
* @param hostname The hostname to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateHostname(String hostname, String fieldName) {
ValidationResult result = new ValidationResult();
if (hostname != null && !hostname.trim().isEmpty()) {
String trimmedHostname = hostname.trim();
if (!HOSTNAME_PATTERN.matcher(trimmedHostname).matches() &&
!IP_ADDRESS_PATTERN.matcher(trimmedHostname).matches() &&
!trimmedHostname.equalsIgnoreCase("localhost")) {
result.addError(fieldName, fieldName + " is not a valid hostname or IP address", hostname);
}
}
return result;
}
/**
* Validates IBM i object name format.
*
* @param objectName The object name to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateIBMiObjectName(String objectName, String fieldName) {
ValidationResult result = new ValidationResult();
if (objectName != null && !objectName.trim().isEmpty()) {
if (!IBMI_OBJECT_NAME_PATTERN.matcher(objectName.trim().toUpperCase()).matches()) {
result.addError(fieldName,
fieldName + " must be a valid IBM i object name (1-10 characters, starting with letter)",
objectName);
}
}
return result;
}
/**
* Validates IBM i library name format.
*
* @param libraryName The library name to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateIBMiLibraryName(String libraryName, String fieldName) {
ValidationResult result = new ValidationResult();
if (libraryName != null && !libraryName.trim().isEmpty()) {
String trimmedName = libraryName.trim().toUpperCase();
if (!trimmedName.equals("*LIBL") && !trimmedName.equals("*CURLIB") &&
!IBMI_LIBRARY_NAME_PATTERN.matcher(trimmedName).matches()) {
result.addError(fieldName,
fieldName + " must be a valid IBM i library name (*LIBL, *CURLIB, or 1-10 characters starting with letter)",
libraryName);
}
}
return result;
}
/**
* Validates IBM i user ID format.
*
* @param userId The user ID to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateIBMiUserId(String userId, String fieldName) {
ValidationResult result = new ValidationResult();
if (userId != null && !userId.trim().isEmpty()) {
if (!IBMI_USER_ID_PATTERN.matcher(userId.trim().toUpperCase()).matches()) {
result.addError(fieldName,
fieldName + " must be a valid IBM i user ID (1-10 characters, starting with letter)",
userId);
}
}
return result;
}
/**
* Validates SQL query for potential security issues.
*
* @param sqlQuery The SQL query to validate
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateSqlQuery(String sqlQuery, String fieldName) {
ValidationResult result = new ValidationResult();
if (sqlQuery != null && !sqlQuery.trim().isEmpty()) {
String upperQuery = sqlQuery.toUpperCase().trim();
// Check for dangerous SQL patterns
for (Pattern pattern : DANGEROUS_SQL_PATTERNS) {
if (pattern.matcher(upperQuery).find()) {
result.addError(fieldName,
fieldName + " contains potentially dangerous SQL statements",
sqlQuery);
break;
}
}
// Check for basic SELECT requirement
if (!upperQuery.startsWith("SELECT") && !upperQuery.startsWith("WITH")) {
result.addError(fieldName,
fieldName + " must be a SELECT query",
sqlQuery);
}
}
return result;
}
/**
* Validates regex pattern.
*
* @param value The value to validate against pattern
* @param pattern The regex pattern
* @param fieldName The field name for error messages
* @param errorMessage Custom error message
* @return ValidationResult
*/
public static ValidationResult validatePattern(String value, Pattern pattern, String fieldName, String errorMessage) {
ValidationResult result = new ValidationResult();
if (value != null && !value.trim().isEmpty()) {
if (!pattern.matcher(value.trim()).matches()) {
result.addError(fieldName, errorMessage, value);
}
}
return result;
}
/**
* Validates regex pattern with default error message.
*
* @param value The value to validate against pattern
* @param pattern The regex pattern
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validatePattern(String value, Pattern pattern, String fieldName) {
return validatePattern(value, pattern, fieldName, fieldName + " format is invalid");
}
/**
* Validates that a value is one of the allowed values.
*
* @param value The value to validate
* @param allowedValues Array of allowed values
* @param fieldName The field name for error messages
* @return ValidationResult
*/
public static ValidationResult validateAllowedValues(String value, String[] allowedValues, String fieldName) {
ValidationResult result = new ValidationResult();
if (value != null && !value.trim().isEmpty()) {
boolean found = false;
for (String allowed : allowedValues) {
if (value.trim().equalsIgnoreCase(allowed)) {
found = true;
break;
}
}
if (!found) {
result.addError(fieldName,
String.format("%s must be one of: %s", fieldName, String.join(", ", allowedValues)),
value);
}
}
return result;
}
/**
* Combines multiple validation results into one.
*
* @param results Variable number of validation results to combine
* @return Combined validation result
*/
public static ValidationResult combine(ValidationResult... results) {
ValidationResult combined = new ValidationResult();
for (ValidationResult result : results) {
if (!result.isValid()) {
for (ValidationError error : result.getErrors()) {
combined.addError(error);
}
}
}
return combined;
}
/**
* Creates a validation error for a specific field.
*
* @param field The field name
* @param message The error message
* @return ValidationError instance
*/
public static ValidationError createError(String field, String message) {
return new ValidationError(field, message);
}
/**
* Creates a validation error for a specific field with rejected value.
*
* @param field The field name
* @param message The error message
* @param rejectedValue The value that was rejected
* @return ValidationError instance
*/
public static ValidationError createError(String field, String message, Object rejectedValue) {
return new ValidationError(field, message, rejectedValue);
}
/**
* Checks if a string has content (not null and not empty after trimming).
*
* @param value The string to check
* @return true if the string has content
*/
public static boolean hasContent(String value) {
return value != null && !value.trim().isEmpty();
}
/**
* Safely trims a string, returning null if the input is null.
*
* @param value The string to trim
* @return Trimmed string or null
*/
public static String safeTrim(String value) {
return value != null ? value.trim() : null;
}
/**
* Normalizes a string for validation (trim and convert to uppercase).
*
* @param value The string to normalize
* @return Normalized string or null
*/
public static String normalize(String value) {
return value != null ? value.trim().toUpperCase() : null;
}
}