upload current sources
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/src/main/webapp/WEB-INF/classes
|
||||||
|
/lib
|
||||||
|
/target
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
||||||
78
pom.xml
Normal file
78
pom.xml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>dev.alexzaw</groupId>
|
||||||
|
<artifactId>fetchapi</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>war</packaging>
|
||||||
|
<name>FetchAPI</name>
|
||||||
|
<description>API for fetching data from AS400 database</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
|
<poi.version>5.2.3</poi.version>
|
||||||
|
<jt400.version>10.7</jt400.version>
|
||||||
|
<json.version>20231013</json.version>
|
||||||
|
<servlet.version>4.0.1</servlet.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sf.jt400</groupId>
|
||||||
|
<artifactId>jt400</artifactId>
|
||||||
|
<version>${jt400.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>poi</artifactId>
|
||||||
|
<version>${poi.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>poi-ooxml</artifactId>
|
||||||
|
<version>${poi.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
<version>${json.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>${servlet.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.xml.bind</groupId>
|
||||||
|
<artifactId>jaxb-api</artifactId>
|
||||||
|
<version>2.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<finalName>FetchAPI</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-war-plugin</artifactId>
|
||||||
|
<version>3.3.2</version>
|
||||||
|
<configuration>
|
||||||
|
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import com.ibm.as400.access.AS400;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
public class AuthenticationService {
|
||||||
|
private static final Logger logger = Logger.getLogger(AuthenticationService.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
private Properties APITokens;
|
||||||
|
|
||||||
|
public AuthenticationService(Properties configProps, Properties APITokens) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
this.APITokens = APITokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticate(HttpServletRequest req) {
|
||||||
|
String userId = req.getParameter("userId");
|
||||||
|
String password = req.getParameter("password");
|
||||||
|
String apiToken = req.getParameter("apiToken");
|
||||||
|
|
||||||
|
if ((userId == null || password == null) && apiToken == null) {
|
||||||
|
logger.warning("No credentials provided");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId != null && password != null
|
||||||
|
? authenticateWithCredentials(userId, password)
|
||||||
|
: apiToken != null && validateApiToken(apiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticateWithCredentials(String userId, String password) {
|
||||||
|
AS400 system = null;
|
||||||
|
try {
|
||||||
|
system = new AS400(
|
||||||
|
configProps.getProperty("db.server"),
|
||||||
|
userId,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
system.validateSignon();
|
||||||
|
logger.info("User authenticated successfully: " + userId);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.log(Level.WARNING, "Authentication failed", e);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (system != null) {
|
||||||
|
system.disconnectAllServices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateApiToken(String tokenToValidate) {
|
||||||
|
try {
|
||||||
|
String decodedToken = Utils.decodeBase64(tokenToValidate);
|
||||||
|
String[] parts = decodedToken.split(":");
|
||||||
|
|
||||||
|
if (parts.length != 2) {
|
||||||
|
logger.warning("Invalid token format");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String identifier = parts[0];
|
||||||
|
String uuid = parts[1];
|
||||||
|
String validToken = APITokens.getProperty(identifier);
|
||||||
|
|
||||||
|
if (validToken == null || validToken.trim().isEmpty()) {
|
||||||
|
logger.warning("No API token configured for: " + identifier);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validToken.equals(uuid)) {
|
||||||
|
logger.info("API token validated for: " + identifier);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warning("Invalid token attempted for: " + identifier);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.log(Level.SEVERE, "Error validating API token", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import javax.servlet.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class CharacterEncodingFilter implements Filter {
|
||||||
|
private String encoding;
|
||||||
|
|
||||||
|
public void init(FilterConfig config) throws ServletException {
|
||||||
|
encoding = config.getInitParameter("encoding");
|
||||||
|
if (encoding == null) {
|
||||||
|
encoding = "UTF-8";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response,
|
||||||
|
FilterChain chain) throws IOException, ServletException {
|
||||||
|
request.setCharacterEncoding(encoding);
|
||||||
|
response.setCharacterEncoding(encoding);
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void destroy() {
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/main/java/dev/alexzaw/fetchapi/DatabaseManager.java
Normal file
164
src/main/java/dev/alexzaw/fetchapi/DatabaseManager.java
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import com.ibm.as400.access.*;
|
||||||
|
import java.beans.PropertyVetoException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import static dev.alexzaw.fetchapi.Utils.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class DatabaseManager {
|
||||||
|
private static final String JDBC_DRIVER = "com.ibm.as400.access.AS400JDBCDriver";
|
||||||
|
private static final Logger logger = Logger.getLogger(DatabaseManager.class.getName());
|
||||||
|
|
||||||
|
private AS400JDBCConnectionPool pool;
|
||||||
|
private Properties configProps;
|
||||||
|
|
||||||
|
public DatabaseManager(Properties configProps) throws Exception {
|
||||||
|
this.configProps = configProps;
|
||||||
|
Class.forName(JDBC_DRIVER);
|
||||||
|
initializeConnectionPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeConnectionPool() throws Exception {
|
||||||
|
try {
|
||||||
|
String username = configProps.getProperty("db.username");
|
||||||
|
String password = decodeBase64(configProps.getProperty("db.password"));
|
||||||
|
String serverName = configProps.getProperty("db.server", "localhost");
|
||||||
|
String jdbcString = configProps.getProperty("db.jdbcString");
|
||||||
|
String libraries = configProps.getProperty("db.libraries", "QGPL,QTEMP");
|
||||||
|
|
||||||
|
validateConfig(username, password);
|
||||||
|
AS400JDBCConnectionPoolDataSource dataSource = createDataSource(
|
||||||
|
serverName,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
jdbcString,
|
||||||
|
libraries
|
||||||
|
);
|
||||||
|
pool = new AS400JDBCConnectionPool(dataSource);
|
||||||
|
configurePool(pool);
|
||||||
|
} catch (PropertyVetoException | ConnectionPoolException e) {
|
||||||
|
throw new Exception("Failed to initialize connection pool", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateConfig(String username, String password) throws Exception {
|
||||||
|
if (username == null || password == null) {
|
||||||
|
throw new Exception("Database username or password not provided in properties file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AS400JDBCConnectionPoolDataSource createDataSource(
|
||||||
|
String serverName,
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String jdbcString,
|
||||||
|
String libraries
|
||||||
|
) throws PropertyVetoException {
|
||||||
|
AS400JDBCConnectionPoolDataSource dataSource = new AS400JDBCConnectionPoolDataSource();
|
||||||
|
dataSource.setServerName(serverName);
|
||||||
|
dataSource.setUser(username);
|
||||||
|
dataSource.setPassword(password);
|
||||||
|
dataSource.setProperties(jdbcString);
|
||||||
|
dataSource.setLibraries(libraries);
|
||||||
|
dataSource.setExtendedMetaData(true);
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configurePool(AS400JDBCConnectionPool pool) throws ConnectionPoolException {
|
||||||
|
pool.setMaxConnections(40);
|
||||||
|
pool.fill(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryResult executeQuery(String sqlQuery) throws SQLException, ConnectionPoolException {
|
||||||
|
// Log connection pool status
|
||||||
|
logger.info("Connection pool before query - Active connections: " + pool.getActiveConnectionCount() +
|
||||||
|
", Available connections: " + pool.getAvailableConnectionCount());
|
||||||
|
|
||||||
|
Connection connection = pool.getConnection();
|
||||||
|
logger.info("Got new connection from pool: " + connection.hashCode());
|
||||||
|
|
||||||
|
PreparedStatement statement = null;
|
||||||
|
ResultSet resultSet = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
statement = connection.prepareStatement(
|
||||||
|
sqlQuery,
|
||||||
|
ResultSet.TYPE_FORWARD_ONLY,
|
||||||
|
ResultSet.CONCUR_READ_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
statement.setFetchSize(5000);
|
||||||
|
statement.setQueryTimeout(Integer.parseInt(
|
||||||
|
configProps.getProperty("query.timeout.seconds", "600")
|
||||||
|
));
|
||||||
|
|
||||||
|
// Execute the statement and check if it returns a ResultSet
|
||||||
|
boolean hasResultSet = statement.execute();
|
||||||
|
|
||||||
|
if (hasResultSet) {
|
||||||
|
// Handle SELECT queries
|
||||||
|
resultSet = statement.getResultSet();
|
||||||
|
String[] headers = getQueryHeaders(resultSet);
|
||||||
|
ResultSetMetaData md = resultSet.getMetaData();
|
||||||
|
StringBuilder sb = new StringBuilder("MD: ");
|
||||||
|
for (int i = 1; i <= md.getColumnCount(); i++) {
|
||||||
|
sb.append("[").append(i)
|
||||||
|
.append(" label=").append(md.getColumnLabel(i))
|
||||||
|
.append(" name=").append(md.getColumnName(i))
|
||||||
|
.append("]");
|
||||||
|
}
|
||||||
|
logger.info(sb.toString());
|
||||||
|
return new QueryResult(
|
||||||
|
connection,
|
||||||
|
statement,
|
||||||
|
resultSet,
|
||||||
|
headers,
|
||||||
|
resultSet.getMetaData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Handle non-SELECT queries (CREATE, INSERT, etc.)
|
||||||
|
return new QueryResult(
|
||||||
|
connection,
|
||||||
|
statement,
|
||||||
|
null,
|
||||||
|
new String[0],
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// Clean up resources in case of error
|
||||||
|
if (resultSet != null) resultSet.close();
|
||||||
|
if (statement != null) statement.close();
|
||||||
|
if (connection != null) connection.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getQueryHeaders(ResultSet rs) throws SQLException {
|
||||||
|
ResultSetMetaData rsmd = rs.getMetaData();
|
||||||
|
int columnCount = rsmd.getColumnCount();
|
||||||
|
String[] headers = new String[columnCount];
|
||||||
|
|
||||||
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
// Prefer alias/label if defined
|
||||||
|
String label = rsmd.getColumnLabel(i);
|
||||||
|
if (label == null || label.trim().isEmpty()) {
|
||||||
|
label = rsmd.getColumnName(i);
|
||||||
|
}
|
||||||
|
// Title-case only for pretty outputs (HTML/Excel)
|
||||||
|
headers[i - 1] = toTitleCase(label.trim());
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void closePool() {
|
||||||
|
if (pool != null) {
|
||||||
|
pool.close();
|
||||||
|
logger.info("Connection pool closed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/main/java/dev/alexzaw/fetchapi/FetchAPIServlet.java
Normal file
163
src/main/java/dev/alexzaw/fetchapi/FetchAPIServlet.java
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.*;
|
||||||
|
import javax.servlet.http.*;
|
||||||
|
|
||||||
|
public class FetchAPIServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(FetchAPIServlet.class.getName());
|
||||||
|
|
||||||
|
private DatabaseManager dbManager;
|
||||||
|
private OutputFormatter outputFormatter;
|
||||||
|
private AuthenticationService authService;
|
||||||
|
private Properties configProps;
|
||||||
|
private Properties APITokens;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws ServletException {
|
||||||
|
System.setProperty(
|
||||||
|
"org.apache.logging.log4j.simplelog.StatusLogger.level",
|
||||||
|
"OFF"
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
configProps = loadProperties("/etc/misc/configs/FetchAPI.properties");
|
||||||
|
APITokens = loadProperties("/etc/misc/configs/APITokens.properties");
|
||||||
|
|
||||||
|
dbManager = new DatabaseManager(configProps);
|
||||||
|
outputFormatter = new OutputFormatter(configProps);
|
||||||
|
authService = new AuthenticationService(configProps, APITokens);
|
||||||
|
|
||||||
|
logger.info("FetchAPI initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.log(Level.SEVERE, "Failed to initialize FetchAPI", e);
|
||||||
|
throw new ServletException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties loadProperties(String propsPath) throws ServletException {
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream in = new FileInputStream(propsPath)) {
|
||||||
|
props.load(in);
|
||||||
|
return props;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServletException("Failed to load properties file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
processRequest(request, response, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
processRequest(request, response, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRequest(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
String method
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
QueryResult result = null;
|
||||||
|
request.setCharacterEncoding("UTF-8");
|
||||||
|
Utils.addCorsHeaders(response);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!authService.authenticate(request)) {
|
||||||
|
outputFormatter.sendErrorResponse(
|
||||||
|
response,
|
||||||
|
HttpServletResponse.SC_UNAUTHORIZED,
|
||||||
|
"Not Authorized!"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Works for both GET query params and POST form data
|
||||||
|
String format = request.getParameter("format");
|
||||||
|
String sqlQuery = request.getParameter("sql");
|
||||||
|
if (!validateQuery(sqlQuery)) {
|
||||||
|
outputFormatter.sendErrorResponse(
|
||||||
|
response,
|
||||||
|
HttpServletResponse.SC_BAD_REQUEST,
|
||||||
|
"Invalid or dangerous SQL query"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("Executing query: " + sqlQuery);
|
||||||
|
result = dbManager.executeQuery(sqlQuery);
|
||||||
|
|
||||||
|
if (result.getResultSet() == null) {
|
||||||
|
// Non-query operation completed successfully
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"success\":true,\"message\":\"SQL executed successfully\"}");
|
||||||
|
logger.info("Non-query SQL executed successfully");
|
||||||
|
} else {
|
||||||
|
// Query operation - format the results
|
||||||
|
if ("excel".equalsIgnoreCase(format)) {
|
||||||
|
outputFormatter.outputExcel(response, result);
|
||||||
|
} else if ("html".equalsIgnoreCase(format)) {
|
||||||
|
outputFormatter.outputHtml(response, result);
|
||||||
|
} else if ("csv".equalsIgnoreCase(format)) {
|
||||||
|
outputFormatter.outputCsv(response, result);
|
||||||
|
} else if ("xml".equalsIgnoreCase(format)) {
|
||||||
|
outputFormatter.outputXml(response, result);
|
||||||
|
} else {
|
||||||
|
// Default to JSON
|
||||||
|
outputFormatter.outputJson(response, result);
|
||||||
|
}
|
||||||
|
logger.info("Query results formatted successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.log(Level.SEVERE, "Error processing request", e);
|
||||||
|
outputFormatter.sendErrorResponse(
|
||||||
|
response,
|
||||||
|
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||||
|
e.getMessage()
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (result != null) {
|
||||||
|
try {
|
||||||
|
result.close();
|
||||||
|
logger.info("Result closed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.log(Level.WARNING, "Error closing database resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private boolean validateQuery(String sqlQuery) {
|
||||||
|
if (sqlQuery == null || sqlQuery.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lowercaseQuery = sqlQuery.toLowerCase();
|
||||||
|
return !(
|
||||||
|
lowercaseQuery.contains("truncate") ||
|
||||||
|
lowercaseQuery.contains("drop") ||
|
||||||
|
lowercaseQuery.contains("delete from")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
if (dbManager != null) {
|
||||||
|
dbManager.closePool();
|
||||||
|
logger.info("Database connection pool closed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
src/main/java/dev/alexzaw/fetchapi/OutputFormatter.java
Normal file
47
src/main/java/dev/alexzaw/fetchapi/OutputFormatter.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.formatter.*;
|
||||||
|
|
||||||
|
public class OutputFormatter {
|
||||||
|
private Properties configProps;
|
||||||
|
private ErrorFormatter errorFormatter;
|
||||||
|
|
||||||
|
public OutputFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
this.errorFormatter = FormatterFactory.createErrorFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outputJson(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
ResponseFormatter formatter = FormatterFactory.getFormatter("json", configProps);
|
||||||
|
formatter.format(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outputExcel(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
ResponseFormatter formatter = FormatterFactory.getFormatter("excel", configProps);
|
||||||
|
formatter.format(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outputHtml(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
ResponseFormatter formatter = FormatterFactory.getFormatter("html", configProps);
|
||||||
|
formatter.format(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outputCsv(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
ResponseFormatter formatter = FormatterFactory.getFormatter("csv", configProps);
|
||||||
|
formatter.format(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outputXml(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
ResponseFormatter formatter = FormatterFactory.getFormatter("xml", configProps);
|
||||||
|
formatter.format(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
|
||||||
|
errorFormatter.sendErrorResponse(response, status, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/java/dev/alexzaw/fetchapi/QueryResult.java
Normal file
62
src/main/java/dev/alexzaw/fetchapi/QueryResult.java
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.ResultSetMetaData;
|
||||||
|
|
||||||
|
public class QueryResult implements AutoCloseable {
|
||||||
|
|
||||||
|
public final ResultSet resultSet;
|
||||||
|
public final String[] headers;
|
||||||
|
public final ResultSetMetaData metadata;
|
||||||
|
public final Connection connection;
|
||||||
|
public final PreparedStatement statement;
|
||||||
|
|
||||||
|
public QueryResult(
|
||||||
|
Connection conn,
|
||||||
|
PreparedStatement stmt,
|
||||||
|
ResultSet rs,
|
||||||
|
String[] headers,
|
||||||
|
ResultSetMetaData metadata
|
||||||
|
) {
|
||||||
|
this.connection = conn;
|
||||||
|
this.statement = stmt;
|
||||||
|
this.resultSet = rs;
|
||||||
|
this.headers = headers;
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultSet getResultSet() {
|
||||||
|
return resultSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultSetMetaData getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedStatement getStatement() {
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
try {
|
||||||
|
if (resultSet != null) resultSet.close();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (statement != null) statement.close();
|
||||||
|
} finally {
|
||||||
|
if (connection != null) connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/main/java/dev/alexzaw/fetchapi/StyleManager.java
Normal file
127
src/main/java/dev/alexzaw/fetchapi/StyleManager.java
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import java.sql.ResultSetMetaData;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
|
||||||
|
public class StyleManager {
|
||||||
|
private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();
|
||||||
|
private Properties configProps;
|
||||||
|
|
||||||
|
public StyleManager(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellStyle createHeaderStyle(Workbook workbook) {
|
||||||
|
CellStyle headerStyle = workbook.createCellStyle();
|
||||||
|
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
|
||||||
|
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
headerStyle.setWrapText(true);
|
||||||
|
|
||||||
|
Font headerFont = workbook.createFont();
|
||||||
|
headerFont.setColor(IndexedColors.WHITE.getIndex());
|
||||||
|
headerFont.setBold(true);
|
||||||
|
headerStyle.setFont(headerFont);
|
||||||
|
|
||||||
|
return headerStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellStyle getCellStyle(String styleType, Workbook workbook) {
|
||||||
|
return styleCache.computeIfAbsent(styleType, type -> createCellStyle(type, workbook));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellStyle createCellStyle(String styleType, Workbook workbook) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
|
||||||
|
// Set alignment
|
||||||
|
String alignmentProp = configProps.getProperty("style." + styleType + ".alignment", "LEFT");
|
||||||
|
HorizontalAlignment alignment = HorizontalAlignment.valueOf(alignmentProp);
|
||||||
|
style.setAlignment(alignment);
|
||||||
|
|
||||||
|
// Set format if exists
|
||||||
|
String format = configProps.getProperty("style." + styleType + ".format");
|
||||||
|
if (format != null) {
|
||||||
|
DataFormat dataFormat = workbook.createDataFormat();
|
||||||
|
style.setDataFormat(dataFormat.getFormat(format));
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColumnStyles(Sheet sheet, ResultSetMetaData metadata) throws SQLException {
|
||||||
|
for (int i = 1; i <= metadata.getColumnCount(); i++) {
|
||||||
|
int sqlType = metadata.getColumnType(i);
|
||||||
|
int scale = metadata.getScale(i);
|
||||||
|
|
||||||
|
String styleType = determineStyleType(sqlType, scale);
|
||||||
|
CellStyle style = getCellStyle(styleType, sheet.getWorkbook());
|
||||||
|
sheet.setDefaultColumnStyle(i - 1, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String determineStyleType(int sqlType, int scale) throws SQLException {
|
||||||
|
switch (sqlType) {
|
||||||
|
case Types.CHAR:
|
||||||
|
case Types.VARCHAR:
|
||||||
|
case Types.LONGVARCHAR:
|
||||||
|
return "text";
|
||||||
|
|
||||||
|
case Types.DECIMAL:
|
||||||
|
case Types.NUMERIC:
|
||||||
|
return scale > 0 ? "number" : "integer";
|
||||||
|
|
||||||
|
case Types.DOUBLE:
|
||||||
|
case Types.FLOAT:
|
||||||
|
case Types.REAL:
|
||||||
|
return "number"; // Always decimal for floating point types
|
||||||
|
|
||||||
|
case Types.BIGINT:
|
||||||
|
case Types.INTEGER:
|
||||||
|
case Types.SMALLINT:
|
||||||
|
case Types.TINYINT:
|
||||||
|
return "integer";
|
||||||
|
|
||||||
|
case Types.DATE:
|
||||||
|
return "date";
|
||||||
|
|
||||||
|
case Types.TIMESTAMP:
|
||||||
|
return "timestamp";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptimalColumnWidths(Sheet sheet, String[] headers, ResultSetMetaData metadata) throws SQLException {
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
int columnWidth = metadata.getColumnDisplaySize(i + 1);
|
||||||
|
int excelWidth = getOptimalColumnLength(headers[i], columnWidth);
|
||||||
|
sheet.setColumnWidth(i, excelWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getOptimalColumnLength(String header, int dataLength) {
|
||||||
|
final int DEFAULT_MAX = Integer.parseInt(
|
||||||
|
configProps.getProperty("excel.column.max.width", "40")
|
||||||
|
);
|
||||||
|
final int DEFAULT_MIN = Integer.parseInt(
|
||||||
|
configProps.getProperty("excel.column.min.width", "10")
|
||||||
|
);
|
||||||
|
int maxLength = 0;
|
||||||
|
|
||||||
|
for (String word : header.split("\\s+")) {
|
||||||
|
maxLength = Math.max(maxLength, word.length());
|
||||||
|
}
|
||||||
|
maxLength = Math.max(maxLength, dataLength);
|
||||||
|
maxLength = Math.max(DEFAULT_MIN, Math.min(maxLength, DEFAULT_MAX));
|
||||||
|
return maxLength * 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
styleCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/main/java/dev/alexzaw/fetchapi/Utils.java
Normal file
83
src/main/java/dev/alexzaw/fetchapi/Utils.java
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class Utils {
|
||||||
|
|
||||||
|
public static String toTitleCase(String input) {
|
||||||
|
if (input == null || input.isEmpty()) return input;
|
||||||
|
|
||||||
|
// 1) lower-case everything
|
||||||
|
String normalized = input.toLowerCase();
|
||||||
|
|
||||||
|
// 2) replace whitespace, '+' , '_' , '-' with a single space
|
||||||
|
normalized = normalized.replaceAll("[\\s+_-]", " ");
|
||||||
|
|
||||||
|
// 3) split on one-or-more spaces (collapsing runs)
|
||||||
|
String[] parts = normalized.trim().split("\\s+");
|
||||||
|
|
||||||
|
// 4) capitalize first char of each fragment; 5) join with single spaces
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
for (int i = 0; i < parts.length; i++) {
|
||||||
|
String w = parts[i];
|
||||||
|
if (!w.isEmpty()) {
|
||||||
|
out.append(Character.toUpperCase(w.charAt(0)));
|
||||||
|
if (w.length() > 1) out.append(w.substring(1)); // already lower-case
|
||||||
|
if (i < parts.length - 1) out.append(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 6) trim (already trimmed by construction)
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String rightTrim(String s) {
|
||||||
|
int i = s.length() - 1;
|
||||||
|
while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
return s.substring(0, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decodeBase64(String encodedString) {
|
||||||
|
return new String(
|
||||||
|
Base64.getDecoder().decode(encodedString),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String escapeHtml(String input) {
|
||||||
|
if (input == null) return "";
|
||||||
|
return input
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
.replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String escapeJson(String str) {
|
||||||
|
if (str == null) return "";
|
||||||
|
return str
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addCorsHeaders(HttpServletResponse resp) {
|
||||||
|
resp.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
resp.setHeader("Access-Control-Max-Age", "3600");
|
||||||
|
resp.setHeader("Connection", "close");
|
||||||
|
resp.setHeader("Keep-Alive", "timeout=0, max=0");
|
||||||
|
resp.setHeader("Pragma", "no-cache");
|
||||||
|
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
|
||||||
|
// Add a unique timestamp to prevent caching
|
||||||
|
resp.setHeader("X-Timestamp", String.valueOf(System.currentTimeMillis()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
|
public class CsvFormatter implements ResponseFormatter {
|
||||||
|
private static final Logger logger = Logger.getLogger(CsvFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
private static final char DEFAULT_DELIMITER = ',';
|
||||||
|
private static final char DEFAULT_QUOTE = '"';
|
||||||
|
|
||||||
|
public CsvFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
response.setContentType("text/csv");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=data.csv");
|
||||||
|
response.setBufferSize(8192);
|
||||||
|
|
||||||
|
char delimiter = getDelimiter();
|
||||||
|
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8), 8192)) {
|
||||||
|
|
||||||
|
// Write header row
|
||||||
|
writeRow(writer, result.headers, delimiter);
|
||||||
|
|
||||||
|
// Write data rows
|
||||||
|
while (result.resultSet.next()) {
|
||||||
|
String[] rowData = new String[result.metadata.getColumnCount()];
|
||||||
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
|
String value = result.resultSet.getString(i);
|
||||||
|
rowData[i-1] = value != null ? value.trim() : "";
|
||||||
|
}
|
||||||
|
writeRow(writer, rowData, delimiter);
|
||||||
|
|
||||||
|
// Flush periodically
|
||||||
|
if (result.resultSet.getRow() % 1000 == 0) {
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private char getDelimiter() {
|
||||||
|
String delimiterStr = configProps.getProperty("csv.delimiter", ",");
|
||||||
|
return delimiterStr.isEmpty() ? DEFAULT_DELIMITER : delimiterStr.charAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeRow(BufferedWriter writer, String[] values, char delimiter) throws IOException {
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
writer.write(delimiter);
|
||||||
|
}
|
||||||
|
writer.write(escapeField(values[i], delimiter));
|
||||||
|
}
|
||||||
|
writer.write("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeField(String field, char delimiter) {
|
||||||
|
if (field == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field contains delimiter, quotes, or newlines, it needs to be quoted
|
||||||
|
if (field.indexOf(delimiter) >= 0 || field.indexOf(DEFAULT_QUOTE) >= 0 ||
|
||||||
|
field.indexOf('\n') >= 0 || field.indexOf('\r') >= 0) {
|
||||||
|
|
||||||
|
// Double up any quotes in the field
|
||||||
|
field = field.replace(String.valueOf(DEFAULT_QUOTE),
|
||||||
|
String.valueOf(DEFAULT_QUOTE) + String.valueOf(DEFAULT_QUOTE));
|
||||||
|
|
||||||
|
// Wrap the field in quotes
|
||||||
|
return DEFAULT_QUOTE + field + DEFAULT_QUOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import static dev.alexzaw.fetchapi.Utils.*;
|
||||||
|
|
||||||
|
public class ErrorFormatter {
|
||||||
|
|
||||||
|
public void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
|
||||||
|
response.setStatus(status);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
String json = String.format(
|
||||||
|
"{ \"error\": { \"status\": %d, \"message\": \"%s\" } }",
|
||||||
|
status,
|
||||||
|
escapeJson(message)
|
||||||
|
);
|
||||||
|
|
||||||
|
try (PrintWriter out = response.getWriter()) {
|
||||||
|
out.print(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
136
src/main/java/dev/alexzaw/fetchapi/formatter/ExcelFormatter.java
Normal file
136
src/main/java/dev/alexzaw/fetchapi/formatter/ExcelFormatter.java
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.ResultSetMetaData;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
import dev.alexzaw.fetchapi.StyleManager;
|
||||||
|
|
||||||
|
public class ExcelFormatter implements ResponseFormatter {
|
||||||
|
private static final Logger logger = Logger.getLogger(ExcelFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
private StyleManager styleManager;
|
||||||
|
|
||||||
|
public ExcelFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
this.styleManager = new StyleManager(configProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
logger.info("Starting Excel generation...");
|
||||||
|
|
||||||
|
// Don't set content type and headers until we're ready to write
|
||||||
|
response.reset();
|
||||||
|
response.setBufferSize(32768); // Increase buffer size
|
||||||
|
|
||||||
|
try (SXSSFWorkbook workbook = generateExcelWorkbook(result)) {
|
||||||
|
// Now that we have all data, set response headers
|
||||||
|
response.setContentType(
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
);
|
||||||
|
response.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
"attachment; filename=ExcelExport.xlsx"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write to response with larger buffer
|
||||||
|
try (
|
||||||
|
BufferedOutputStream out = new BufferedOutputStream(
|
||||||
|
response.getOutputStream(),
|
||||||
|
32768
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
workbook.write(out);
|
||||||
|
out.flush();
|
||||||
|
response.flushBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the style cache and dispose workbook
|
||||||
|
styleManager.clearCache();
|
||||||
|
workbook.dispose();
|
||||||
|
logger.info("Excel file generated successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("Error generating Excel: " + e.getMessage());
|
||||||
|
// If error occurs before headers are sent, we can still send error response
|
||||||
|
if (!response.isCommitted()) {
|
||||||
|
response.reset();
|
||||||
|
response.setContentType("text/plain");
|
||||||
|
response
|
||||||
|
.getWriter()
|
||||||
|
.println("Error generating Excel: " + e.getMessage());
|
||||||
|
}
|
||||||
|
throw e; // Rethrow to be handled by caller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SXSSFWorkbook generateExcelWorkbook(QueryResult result) throws SQLException, IOException {
|
||||||
|
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||||
|
workbook.setCompressTempFiles(true);
|
||||||
|
Sheet sheet = workbook.createSheet("Data");
|
||||||
|
|
||||||
|
CellStyle headerStyle = styleManager.createHeaderStyle(workbook);
|
||||||
|
createHeaderRow(sheet, result.headers, headerStyle);
|
||||||
|
|
||||||
|
styleManager.setColumnStyles(sheet, result.metadata);
|
||||||
|
styleManager.setOptimalColumnWidths(sheet, result.headers, result.metadata);
|
||||||
|
|
||||||
|
int rowNum = 1;
|
||||||
|
int batchSize = Integer.parseInt(configProps.getProperty("excel.batch.size", "1000"));
|
||||||
|
int currentBatch = 0;
|
||||||
|
|
||||||
|
while (result.resultSet.next()) {
|
||||||
|
Row row = sheet.createRow(rowNum++);
|
||||||
|
populateDataRow(row, result.resultSet, result.metadata);
|
||||||
|
|
||||||
|
currentBatch++;
|
||||||
|
if (currentBatch >= batchSize) {
|
||||||
|
((SXSSFSheet) sheet).flushRows(batchSize);
|
||||||
|
currentBatch = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workbook;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createHeaderRow(Sheet sheet, String[] headers, CellStyle headerStyle) {
|
||||||
|
Row headerRow = sheet.createRow(0);
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
Cell cell = headerRow.createCell(i);
|
||||||
|
cell.setCellValue(headers[i]);
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateDataRow(Row row, ResultSet rs, ResultSetMetaData metadata) throws SQLException {
|
||||||
|
for (int i = 1; i <= metadata.getColumnCount(); i++) {
|
||||||
|
Cell cell = row.createCell(i-1);
|
||||||
|
Object value = rs.getObject(i);
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Number) {
|
||||||
|
cell.setCellValue(((Number) value).doubleValue());
|
||||||
|
} else if (value instanceof java.sql.Date) {
|
||||||
|
cell.setCellValue((java.sql.Date) value);
|
||||||
|
} else if (value instanceof java.sql.Timestamp) {
|
||||||
|
cell.setCellValue((java.sql.Timestamp) value);
|
||||||
|
} else if (value instanceof Boolean) {
|
||||||
|
cell.setCellValue((Boolean) value);
|
||||||
|
} else {
|
||||||
|
cell.setCellValue(value instanceof String ? ((String)value).trim() : value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class FormatterFactory {
|
||||||
|
|
||||||
|
public static ResponseFormatter getFormatter(String format, Properties configProps) {
|
||||||
|
if ("excel".equalsIgnoreCase(format)) {
|
||||||
|
return new ExcelFormatter(configProps);
|
||||||
|
} else if ("html".equalsIgnoreCase(format)) {
|
||||||
|
return new HtmlFormatter(configProps);
|
||||||
|
} else if ("csv".equalsIgnoreCase(format)) {
|
||||||
|
return new CsvFormatter(configProps);
|
||||||
|
} else if ("xml".equalsIgnoreCase(format)) {
|
||||||
|
return new XmlFormatter(configProps);
|
||||||
|
} else {
|
||||||
|
// Default to JSON
|
||||||
|
return new JsonFormatter(configProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ErrorFormatter createErrorFormatter() {
|
||||||
|
return new ErrorFormatter();
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/main/java/dev/alexzaw/fetchapi/formatter/HtmlFormatter.java
Normal file
178
src/main/java/dev/alexzaw/fetchapi/formatter/HtmlFormatter.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
import dev.alexzaw.fetchapi.Utils;
|
||||||
|
|
||||||
|
public class HtmlFormatter implements ResponseFormatter {
|
||||||
|
private static final Logger logger = Logger.getLogger(HtmlFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
|
||||||
|
public HtmlFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
response.setContentType("text/html");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
try (PrintWriter out = response.getWriter()) {
|
||||||
|
// Start HTML document
|
||||||
|
out.println("<!DOCTYPE html>");
|
||||||
|
out.println("<html lang=\"en\">");
|
||||||
|
out.println("<head>");
|
||||||
|
out.println(" <meta charset=\"UTF-8\">");
|
||||||
|
out.println(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||||||
|
out.println(" <title>Query Results</title>");
|
||||||
|
writeHtmlStyles(out);
|
||||||
|
out.println("</head>");
|
||||||
|
out.println("<body>");
|
||||||
|
|
||||||
|
// Container that mimics the design in the screenshot
|
||||||
|
out.println(" <div class=\"container\">");
|
||||||
|
out.println(" <div class=\"table-wrapper\">");
|
||||||
|
out.println(" <table class=\"data-table\">");
|
||||||
|
|
||||||
|
// Write table headers and body
|
||||||
|
writeHtmlHeaders(out, result.headers);
|
||||||
|
writeHtmlBody(out, result);
|
||||||
|
|
||||||
|
out.println(" </table>");
|
||||||
|
out.println(" </div>");
|
||||||
|
out.println(" </div>");
|
||||||
|
out.println("</body>");
|
||||||
|
out.println("</html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHtmlStyles(PrintWriter out) {
|
||||||
|
out.println(" <style>");
|
||||||
|
// Base styles
|
||||||
|
out.println(" body {");
|
||||||
|
out.println(" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;");
|
||||||
|
out.println(" background-color: #f9fafb;");
|
||||||
|
out.println(" margin: 0;");
|
||||||
|
out.println(" padding: 0;");
|
||||||
|
out.println(" font-size: 14px;");
|
||||||
|
out.println(" color: #333;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Container with no padding to maximize space for many columns
|
||||||
|
out.println(" .container {");
|
||||||
|
out.println(" width: 100%;");
|
||||||
|
out.println(" max-width: 100%;");
|
||||||
|
out.println(" margin: 0;");
|
||||||
|
out.println(" padding: 0;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Table wrapper with enhanced horizontal scrolling for many columns
|
||||||
|
out.println(" .table-wrapper {");
|
||||||
|
out.println(" background: white;");
|
||||||
|
out.println(" width: 100%;");
|
||||||
|
out.println(" overflow-x: auto;");
|
||||||
|
out.println(" white-space: nowrap;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Data table optimized for many columns
|
||||||
|
out.println(" .data-table {");
|
||||||
|
out.println(" width: 100%;");
|
||||||
|
out.println(" min-width: max-content;"); // Ensures table expands to fit all columns
|
||||||
|
out.println(" border-collapse: collapse;");
|
||||||
|
out.println(" border-spacing: 0;");
|
||||||
|
out.println(" table-layout: auto;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Header cells optimized for many columns
|
||||||
|
out.println(" .data-table th {");
|
||||||
|
out.println(" background-color: #f9fafb;");
|
||||||
|
out.println(" color: #4b5563;");
|
||||||
|
out.println(" font-weight: 600;");
|
||||||
|
out.println(" text-align: left;");
|
||||||
|
out.println(" padding: 8px 10px;"); // Reduced padding
|
||||||
|
out.println(" border: 1px solid #e5e7eb;");
|
||||||
|
out.println(" white-space: normal;");
|
||||||
|
out.println(" line-height: 1.2;");
|
||||||
|
out.println(" font-size: 13px;"); // Slightly smaller font
|
||||||
|
out.println(" position: sticky;");
|
||||||
|
out.println(" top: 0;");
|
||||||
|
out.println(" z-index: 10;"); // Ensure headers stay on top
|
||||||
|
out.println(" min-width: 80px;"); // Minimum column width
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Table cells optimized for many columns
|
||||||
|
out.println(" .data-table td {");
|
||||||
|
out.println(" padding: 4px 10px;"); // Reduced padding for compactness
|
||||||
|
out.println(" border: 1px solid #e5e7eb;");
|
||||||
|
out.println(" text-align: left;");
|
||||||
|
out.println(" white-space: nowrap;");
|
||||||
|
out.println(" overflow: hidden;");
|
||||||
|
out.println(" text-overflow: ellipsis;");
|
||||||
|
out.println(" max-width: 150px;"); // Slightly narrower max width
|
||||||
|
out.println(" font-size: 13px;"); // Slightly smaller font
|
||||||
|
out.println(" line-height: 1;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Row hover effect
|
||||||
|
out.println(" .data-table tr:hover {");
|
||||||
|
out.println(" background-color: #f3f4f6;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
// Row styling for alternating rows
|
||||||
|
out.println(" .data-table .even-row {");
|
||||||
|
out.println(" background-color: #ffffff;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
out.println(" .data-table .odd-row {");
|
||||||
|
out.println(" background-color: #f8fafc;");
|
||||||
|
out.println(" }");
|
||||||
|
|
||||||
|
out.println(" </style>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHtmlHeaders(PrintWriter out, String[] headers) {
|
||||||
|
out.println(" <thead>");
|
||||||
|
out.println(" <tr>");
|
||||||
|
for (String header : headers) {
|
||||||
|
out.println(" <th>" + Utils.escapeHtml(header) + "</th>");
|
||||||
|
}
|
||||||
|
out.println(" </tr>");
|
||||||
|
out.println(" </thead>");
|
||||||
|
out.println(" <tbody>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHtmlBody(PrintWriter out, QueryResult result) throws SQLException {
|
||||||
|
int batchSize = Integer.parseInt(
|
||||||
|
configProps.getProperty("html.batch.size", "1000")
|
||||||
|
);
|
||||||
|
int currentBatch = 0;
|
||||||
|
int rowCount = 0;
|
||||||
|
|
||||||
|
while (result.resultSet.next()) {
|
||||||
|
// Alternate row styling for better readability with many columns
|
||||||
|
String rowClass = (rowCount % 2 == 0) ? "even-row" : "odd-row";
|
||||||
|
out.println(" <tr class=\"" + rowClass + "\">");
|
||||||
|
|
||||||
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
|
String value = result.resultSet.getString(i);
|
||||||
|
String escapedValue = Utils.escapeHtml(value != null ? value : "");
|
||||||
|
out.println(" <td>" + escapedValue + "</td>");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.println(" </tr>");
|
||||||
|
rowCount++;
|
||||||
|
|
||||||
|
if (++currentBatch >= batchSize) {
|
||||||
|
out.flush();
|
||||||
|
currentBatch = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.println(" </tbody>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
import dev.alexzaw.fetchapi.Utils;
|
||||||
|
|
||||||
|
public class JsonFormatter implements ResponseFormatter {
|
||||||
|
private static final Logger logger = Logger.getLogger(JsonFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
|
||||||
|
public JsonFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
// Increase buffer size for better performance
|
||||||
|
response.setBufferSize(8192);
|
||||||
|
|
||||||
|
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream(), 8192);
|
||||||
|
OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
|
||||||
|
|
||||||
|
osw.write("[");
|
||||||
|
boolean first = true;
|
||||||
|
|
||||||
|
while (result.resultSet.next()) {
|
||||||
|
if (!first) {
|
||||||
|
osw.write(",");
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
JSONObject obj = new JSONObject();
|
||||||
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
|
String columnName = result.metadata.getColumnName(i);
|
||||||
|
String value = result.resultSet.getString(i);
|
||||||
|
obj.put(columnName, value != null ? Utils.rightTrim(value) : JSONObject.NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
osw.write(obj.toString());
|
||||||
|
|
||||||
|
// Flush periodically to avoid holding too much in memory
|
||||||
|
if (result.resultSet.getRow() % 1000 == 0) {
|
||||||
|
osw.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
osw.write("]");
|
||||||
|
osw.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
|
public interface ResponseFormatter {
|
||||||
|
void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.xml.stream.XMLOutputFactory;
|
||||||
|
import javax.xml.stream.XMLStreamException;
|
||||||
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
|
public class XmlFormatter implements ResponseFormatter {
|
||||||
|
private static final Logger logger = Logger.getLogger(XmlFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
|
|
||||||
|
public XmlFormatter(Properties configProps) {
|
||||||
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
|
response.setContentType("application/xml");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setBufferSize(8192);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create XML stream writer
|
||||||
|
XMLOutputFactory factory = XMLOutputFactory.newInstance();
|
||||||
|
XMLStreamWriter writer = factory.createXMLStreamWriter(
|
||||||
|
response.getOutputStream(), "UTF-8");
|
||||||
|
|
||||||
|
// Start document
|
||||||
|
writer.writeStartDocument("UTF-8", "1.0");
|
||||||
|
writer.writeStartElement("data");
|
||||||
|
|
||||||
|
// Write data rows
|
||||||
|
int rowCount = 0;
|
||||||
|
while (result.resultSet.next()) {
|
||||||
|
writer.writeStartElement("row");
|
||||||
|
|
||||||
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
|
String columnName = sanitizeXmlTag(result.metadata.getColumnName(i));
|
||||||
|
String value = result.resultSet.getString(i);
|
||||||
|
|
||||||
|
writer.writeStartElement(columnName);
|
||||||
|
if (value != null) {
|
||||||
|
writer.writeCharacters(value.trim());
|
||||||
|
}
|
||||||
|
writer.writeEndElement(); // column
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeEndElement(); // row
|
||||||
|
|
||||||
|
// Flush periodically
|
||||||
|
if (++rowCount % 1000 == 0) {
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End document
|
||||||
|
writer.writeEndElement(); // data
|
||||||
|
writer.writeEndDocument();
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
} catch (XMLStreamException e) {
|
||||||
|
logger.severe("Error generating XML: " + e.getMessage());
|
||||||
|
throw new IOException("Error generating XML", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeXmlTag(String tag) {
|
||||||
|
// XML element names must start with a letter or underscore and can contain
|
||||||
|
// letters, digits, hyphens, underscores, and periods
|
||||||
|
if (tag == null || tag.isEmpty()) {
|
||||||
|
return "_column";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure first character is valid
|
||||||
|
char firstChar = tag.charAt(0);
|
||||||
|
if (!Character.isLetter(firstChar) && firstChar != '_') {
|
||||||
|
tag = "_" + tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace invalid characters
|
||||||
|
return tag.replaceAll("[^a-zA-Z0-9_.-]", "_");
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/webapp/WEB-INF/web.xml
Normal file
31
src/main/webapp/WEB-INF/web.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
|
||||||
|
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
|
||||||
|
version="3.1">
|
||||||
|
<filter>
|
||||||
|
<filter-name>EncodingFilter</filter-name>
|
||||||
|
<filter-class>dev.alexzaw.fetchapi.CharacterEncodingFilter</filter-class>
|
||||||
|
<init-param>
|
||||||
|
<param-name>encoding</param-name>
|
||||||
|
<param-value>UTF-8</param-value>
|
||||||
|
</init-param>
|
||||||
|
<init-param>
|
||||||
|
<param-name>forceEncoding</param-name>
|
||||||
|
<param-value>true</param-value>
|
||||||
|
</init-param>
|
||||||
|
</filter>
|
||||||
|
<filter-mapping>
|
||||||
|
<filter-name>EncodingFilter</filter-name>
|
||||||
|
<url-pattern>/*</url-pattern>
|
||||||
|
</filter-mapping>
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>FetchAPI</servlet-name>
|
||||||
|
<servlet-class>dev.alexzaw.fetchapi.FetchAPIServlet</servlet-class>
|
||||||
|
<load-on-startup>1</load-on-startup>
|
||||||
|
</servlet>
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>FetchAPI</servlet-name>
|
||||||
|
<url-pattern>/</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
</web-app>
|
||||||
Reference in New Issue
Block a user