Compare commits
2
Commits
main
..
45fac304bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45fac304bd | ||
|
|
1808624237 |
+3
-53
@@ -1,53 +1,3 @@
|
|||||||
# added by gitlab-sync
|
/src/main/webapp/WEB-INF/classes
|
||||||
.DS_Store
|
/lib
|
||||||
Thumbs.db
|
/target
|
||||||
desktop.ini
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
target/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
out/
|
|
||||||
*.class
|
|
||||||
.gradle/
|
|
||||||
node_modules/
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
coverage/
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
.claude/
|
|
||||||
CLAUDE.md
|
|
||||||
CLAUDE.local.md
|
|
||||||
.mcp.json
|
|
||||||
.remember/
|
|
||||||
.cursor/
|
|
||||||
.cursorrules
|
|
||||||
.cursorignore
|
|
||||||
.windsurf/
|
|
||||||
.windsurfrules
|
|
||||||
.codeium/
|
|
||||||
.github/copilot-instructions.md
|
|
||||||
.copilot/
|
|
||||||
.codex/
|
|
||||||
AGENTS.md
|
|
||||||
.gemini/
|
|
||||||
GEMINI.md
|
|
||||||
.aider*
|
|
||||||
.continue/
|
|
||||||
.roo/
|
|
||||||
.roomodes
|
|
||||||
.cline/
|
|
||||||
.clinerules
|
|
||||||
.kiro/
|
|
||||||
.amazonq/
|
|
||||||
.tabnine/
|
|
||||||
.specstory/
|
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>com.minicircuits</groupId>
|
<groupId>dev.alexzaw</groupId>
|
||||||
<artifactId>fetch-api</artifactId>
|
<artifactId>fetchapi</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
<packaging>war</packaging>
|
<packaging>war</packaging>
|
||||||
<name>FetchAPI</name>
|
<name>FetchAPI</name>
|
||||||
@@ -12,23 +12,19 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
<poi.version>5.2.3</poi.version>
|
<poi.version>5.2.3</poi.version>
|
||||||
<!-- Same JTOpen as /QIBM/ProdData/OS400/jt400/lib/jt400Native.jar on the server. -->
|
<jt400.version>10.7</jt400.version>
|
||||||
<jt400.version>21.0.6</jt400.version>
|
|
||||||
<json.version>20231013</json.version>
|
<json.version>20231013</json.version>
|
||||||
<servlet.version>4.0.1</servlet.version>
|
<servlet.version>4.0.1</servlet.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Provided by IAS (ERPAPIS server.xml: classloader commonLibraryRef="IntegratedWebServicesLibs").
|
|
||||||
Only one classloader per JVM may own jt400's native library, so it must not be bundled. -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sf.jt400</groupId>
|
<groupId>net.sf.jt400</groupId>
|
||||||
<artifactId>jt400</artifactId>
|
<artifactId>jt400</artifactId>
|
||||||
<version>${jt400.version}</version>
|
<version>${jt400.version}</version>
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
package com.minicircuits;
|
|
||||||
|
|
||||||
import com.ibm.as400.access.AS400;
|
|
||||||
import com.ibm.as400.access.AS400SecurityException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import javax.servlet.http.Cookie;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class AuthenticationService {
|
|
||||||
private static final Logger logger = Logger.getLogger(AuthenticationService.class.getName());
|
|
||||||
private static final String SESSION_COOKIE_NAME = "FETCHAPI_SESSION";
|
|
||||||
private static final int CLEANUP_THRESHOLD = 1000;
|
|
||||||
|
|
||||||
private final Properties configProps;
|
|
||||||
private final Properties APITokens;
|
|
||||||
private final ConcurrentHashMap<String, SessionInfo> sessionCache = new ConcurrentHashMap<>();
|
|
||||||
private final int ttlMinutes;
|
|
||||||
|
|
||||||
private static class SessionInfo {
|
|
||||||
final String userId;
|
|
||||||
final long expiresAt;
|
|
||||||
final String hmacSecret;
|
|
||||||
|
|
||||||
SessionInfo(String userId, long expiresAt, String hmacSecret) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.expiresAt = expiresAt;
|
|
||||||
this.hmacSecret = hmacSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isExpired() {
|
|
||||||
return System.currentTimeMillis() > expiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationService(Properties configProps, Properties APITokens) {
|
|
||||||
this.configProps = configProps;
|
|
||||||
this.APITokens = APITokens;
|
|
||||||
int ttl;
|
|
||||||
try {
|
|
||||||
ttl = Integer.parseInt(configProps.getProperty("session.ttl.minutes", "30"));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
ttl = 30;
|
|
||||||
}
|
|
||||||
this.ttlMinutes = ttl;
|
|
||||||
|
|
||||||
logger.log(Level.INFO, "Session TTL configured to {0} minutes", this.ttlMinutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backward-compatible overload for callers that do not have an HttpServletResponse.
|
|
||||||
* No session cookie will be issued, but existing auth logic still works.
|
|
||||||
*/
|
|
||||||
public boolean authenticate(HttpServletRequest req) {
|
|
||||||
return authenticate(req, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean authenticate(HttpServletRequest req, HttpServletResponse resp) {
|
|
||||||
// Lazy cleanup when the cache grows large
|
|
||||||
if (sessionCache.size() > CLEANUP_THRESHOLD) {
|
|
||||||
cleanupExpiredSessions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Check for existing session cookie
|
|
||||||
String existingToken = getSessionCookie(req);
|
|
||||||
if (existingToken != null) {
|
|
||||||
SessionInfo cachedSession = sessionCache.get(existingToken);
|
|
||||||
if (cachedSession != null && !cachedSession.isExpired()) {
|
|
||||||
// Verify HMAC with the cached secret
|
|
||||||
if (verifyToken(existingToken, cachedSession.hmacSecret)) {
|
|
||||||
logger.log(Level.FINE, "Verified session for user: {0}", cachedSession.userId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sessionCache.remove(existingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fall back to credential / API-token authentication
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean authenticated = userId != null && password != null
|
|
||||||
? authenticateWithCredentials(userId, password)
|
|
||||||
: apiToken != null && validateApiToken(apiToken);
|
|
||||||
|
|
||||||
// 3. On success, create session and set cookie
|
|
||||||
if (authenticated && resp != null) {
|
|
||||||
String identity = (userId != null) ? userId : "apiToken";
|
|
||||||
// Use credentials as HMAC key: userId:password for credentials, raw API token for token auth
|
|
||||||
String hmacKey = (userId != null && password != null)
|
|
||||||
? userId + ":" + password
|
|
||||||
: apiToken;
|
|
||||||
String sessionToken = createSession(identity, hmacKey);
|
|
||||||
resp.setHeader("Set-Cookie", SESSION_COOKIE_NAME + "=" + sessionToken
|
|
||||||
+ "; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=" + (ttlMinutes * 60));
|
|
||||||
logger.log(Level.INFO, "Session created for: {0}", identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean authenticateWithCredentials(String userId, String password) {
|
|
||||||
AS400 system = null;
|
|
||||||
try {
|
|
||||||
system = new AS400(
|
|
||||||
configProps.getProperty("db.server"),
|
|
||||||
userId,
|
|
||||||
password
|
|
||||||
);
|
|
||||||
system.validateSignon();
|
|
||||||
logger.log(Level.INFO, "User authenticated successfully: {0}", userId);
|
|
||||||
return true;
|
|
||||||
} catch (AS400SecurityException | IOException e) {
|
|
||||||
logger.log(Level.WARNING, "Authentication failed", e);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (system != null) {
|
|
||||||
system.disconnectAllServices();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSessionCookie(HttpServletRequest req) {
|
|
||||||
Cookie[] cookies = req.getCookies();
|
|
||||||
if (cookies != null) {
|
|
||||||
for (Cookie cookie : cookies) {
|
|
||||||
if (SESSION_COOKIE_NAME.equals(cookie.getName())) {
|
|
||||||
return cookie.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createSession(String userId, String hmacSecret) {
|
|
||||||
long createdAt = System.currentTimeMillis();
|
|
||||||
long expiresAt = createdAt + ((long) ttlMinutes * 60 * 1000);
|
|
||||||
String serverName = configProps.getProperty("db.server", "localhost");
|
|
||||||
|
|
||||||
// HMAC signs the core fields
|
|
||||||
String hmacData = userId + ":" + serverName + ":" + createdAt + ":" + expiresAt;
|
|
||||||
String signature = hmacSha256(hmacData, hmacSecret);
|
|
||||||
|
|
||||||
// Build JSON payload
|
|
||||||
JSONObject tokenJson = new JSONObject();
|
|
||||||
tokenJson.put("userId", userId);
|
|
||||||
tokenJson.put("serverName", serverName);
|
|
||||||
tokenJson.put("createdAt", createdAt);
|
|
||||||
tokenJson.put("expiresAt", expiresAt);
|
|
||||||
tokenJson.put("signature", signature);
|
|
||||||
|
|
||||||
// Base64 encode the entire JSON string
|
|
||||||
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(
|
|
||||||
tokenJson.toString().getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
sessionCache.put(token, new SessionInfo(userId, expiresAt, hmacSecret));
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String hmacSha256(String data, String key) {
|
|
||||||
try {
|
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
|
||||||
SecretKeySpec secretKey = new SecretKeySpec(
|
|
||||||
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
|
||||||
mac.init(secretKey);
|
|
||||||
byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(hmacBytes);
|
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
|
||||||
throw new RuntimeException("Failed to compute HMAC", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean verifyToken(String base64Token, String hmacSecret) {
|
|
||||||
try {
|
|
||||||
String decoded = new String(Base64.getUrlDecoder().decode(base64Token), StandardCharsets.UTF_8);
|
|
||||||
JSONObject tokenJson = new JSONObject(decoded);
|
|
||||||
|
|
||||||
String userId = tokenJson.getString("userId");
|
|
||||||
String serverName = tokenJson.getString("serverName");
|
|
||||||
long createdAt = tokenJson.getLong("createdAt");
|
|
||||||
long expiresAt = tokenJson.getLong("expiresAt");
|
|
||||||
String signature = tokenJson.getString("signature");
|
|
||||||
|
|
||||||
// Check expiry
|
|
||||||
if (System.currentTimeMillis() > expiresAt) return false;
|
|
||||||
|
|
||||||
// Verify server
|
|
||||||
String currentServer = configProps.getProperty("db.server", "localhost");
|
|
||||||
if (!serverName.equals(currentServer)) return false;
|
|
||||||
|
|
||||||
// Verify HMAC
|
|
||||||
String hmacData = userId + ":" + serverName + ":" + createdAt + ":" + expiresAt;
|
|
||||||
String expectedSignature = hmacSha256(hmacData, hmacSecret);
|
|
||||||
|
|
||||||
return MessageDigest.isEqual(
|
|
||||||
signature.getBytes(StandardCharsets.UTF_8),
|
|
||||||
expectedSignature.getBytes(StandardCharsets.UTF_8));
|
|
||||||
} catch (IllegalArgumentException | org.json.JSONException e) {
|
|
||||||
logger.warning("Failed to parse session token");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupExpiredSessions() {
|
|
||||||
int removed = 0;
|
|
||||||
Iterator<Map.Entry<String, SessionInfo>> it = sessionCache.entrySet().iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Map.Entry<String, SessionInfo> entry = it.next();
|
|
||||||
if (entry.getValue().isExpired()) {
|
|
||||||
it.remove();
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (removed > 0) {
|
|
||||||
logger.log(Level.INFO, "Cleaned up {0} expired sessions, {1} remaining",
|
|
||||||
new Object[]{removed, sessionCache.size()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.log(Level.WARNING, "No API token configured for: {0}", identifier);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validToken.equals(uuid)) {
|
|
||||||
logger.log(Level.INFO, "API token validated for: {0}", identifier);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(Level.WARNING, "Invalid token attempted for: {0}", identifier);
|
|
||||||
return false;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.log(Level.SEVERE, "Invalid Base64 in API token", e);
|
|
||||||
return false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.log(Level.SEVERE, "Error validating API token", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package com.minicircuits;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import javax.servlet.ServletOutputStream;
|
|
||||||
import javax.servlet.WriteListener;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletResponseWrapper;
|
|
||||||
|
|
||||||
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
|
|
||||||
private final Path tempFile;
|
|
||||||
private final FileOutputStream fileOutputStream;
|
|
||||||
private final ServletOutputStream servletOutputStream;
|
|
||||||
private PrintWriter writer;
|
|
||||||
|
|
||||||
public BufferedResponseWrapper(HttpServletResponse response) throws IOException {
|
|
||||||
super(response);
|
|
||||||
this.tempFile = Files.createTempFile("fetchapi_", ".tmp");
|
|
||||||
this.fileOutputStream = new FileOutputStream(tempFile.toFile());
|
|
||||||
this.servletOutputStream = new ServletOutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
fileOutputStream.write(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) throws IOException {
|
|
||||||
fileOutputStream.write(b, off, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flush() throws IOException {
|
|
||||||
fileOutputStream.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
fileOutputStream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReady() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setWriteListener(WriteListener writeListener) {
|
|
||||||
// Not used for blocking I/O
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ServletOutputStream getOutputStream() {
|
|
||||||
return servletOutputStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PrintWriter getWriter() throws IOException {
|
|
||||||
if (writer == null) {
|
|
||||||
String charset = getCharacterEncoding();
|
|
||||||
writer = new PrintWriter(new OutputStreamWriter(fileOutputStream,
|
|
||||||
charset != null ? charset : "UTF-8"));
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flushBuffer() {
|
|
||||||
// Don't flush to the real response — we're buffering
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reset() {
|
|
||||||
// Don't reset the real response during buffering
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resetBuffer() {
|
|
||||||
// Don't reset during buffering
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush any remaining data, then stream the temp file to the real response.
|
|
||||||
* Sets Content-Length. Deletes the temp file when done.
|
|
||||||
*/
|
|
||||||
public void writeToResponse() throws IOException {
|
|
||||||
// Flush and close writer/stream
|
|
||||||
if (writer != null) {
|
|
||||||
writer.close();
|
|
||||||
} else {
|
|
||||||
fileOutputStream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content length from temp file size
|
|
||||||
long fileSize = Files.size(tempFile);
|
|
||||||
if (fileSize <= Integer.MAX_VALUE) {
|
|
||||||
getResponse().setContentLength((int) fileSize);
|
|
||||||
} else {
|
|
||||||
((HttpServletResponse) getResponse()).setHeader("Content-Length", Long.toString(fileSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream temp file to real response
|
|
||||||
HttpServletResponse realResponse = (HttpServletResponse) getResponse();
|
|
||||||
try (InputStream in = new BufferedInputStream(new FileInputStream(tempFile.toFile()), 32768);
|
|
||||||
OutputStream out = new BufferedOutputStream(realResponse.getOutputStream(), 32768)) {
|
|
||||||
byte[] buffer = new byte[32768];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
out.flush();
|
|
||||||
} finally {
|
|
||||||
// Always delete the temp file
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up temp file if writeToResponse was never called (e.g., on error).
|
|
||||||
*/
|
|
||||||
public void cleanup() {
|
|
||||||
try {
|
|
||||||
if (fileOutputStream != null) {
|
|
||||||
fileOutputStream.close();
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
package com.minicircuits;
|
|
||||||
|
|
||||||
import com.minicircuits.formatter.*;
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import com.ibm.as400.access.ConnectionPoolException;
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.*;
|
|
||||||
|
|
||||||
public class FetchAPI extends HttpServlet {
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(FetchAPI.class.getName());
|
|
||||||
|
|
||||||
private static final Pattern DANGEROUS_SQL = Pattern.compile(
|
|
||||||
"\\b(TRUNCATE|DROP\\s+(TABLE|VIEW|INDEX|SCHEMA|FUNCTION|PROCEDURE|TRIGGER|SEQUENCE|ALIAS|TYPE|VARIABLE|PACKAGE)|DELETE\\s+FROM|ALTER|GRANT|REVOKE|RENAME)\\b",
|
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.DOTALL
|
|
||||||
);
|
|
||||||
|
|
||||||
private DatabaseManager dbManager;
|
|
||||||
private AuthenticationService authService;
|
|
||||||
private Properties configProps;
|
|
||||||
private Properties APITokens;
|
|
||||||
private ErrorFormatter errorFormatter;
|
|
||||||
private SuccessFormatter successFormatter;
|
|
||||||
|
|
||||||
@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);
|
|
||||||
authService = new AuthenticationService(configProps, APITokens);
|
|
||||||
errorFormatter = FormatterFactory.createErrorFormatter();
|
|
||||||
successFormatter = FormatterFactory.createSuccessFormatter();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doPost(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response
|
|
||||||
) throws ServletException, IOException {
|
|
||||||
processRequest(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOutputFormat(HttpServletRequest request) {
|
|
||||||
String format = request.getParameter("format");
|
|
||||||
if (format == null || format.isBlank()) {
|
|
||||||
String acceptHeader = request.getHeader("Accept");
|
|
||||||
if (acceptHeader != null) {
|
|
||||||
if (acceptHeader.contains("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
|
|
||||||
return "excel";
|
|
||||||
} else if (acceptHeader.contains("text/csv")) {
|
|
||||||
return "csv";
|
|
||||||
} else if (acceptHeader.equals("application/xml") || acceptHeader.equals("text/xml")) {
|
|
||||||
return "xml";
|
|
||||||
} else if (acceptHeader.equals("text/html")) {
|
|
||||||
return "html";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "json"; // default
|
|
||||||
}
|
|
||||||
return format.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processRequest(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response
|
|
||||||
) throws ServletException, IOException {
|
|
||||||
QueryResult result = null;
|
|
||||||
request.setCharacterEncoding("UTF-8");
|
|
||||||
Utils.addCorsHeaders(response);
|
|
||||||
|
|
||||||
String format = getOutputFormat(request);
|
|
||||||
|
|
||||||
if (!authService.authenticate(request, response)) {
|
|
||||||
errorFormatter.format(
|
|
||||||
response,
|
|
||||||
HttpServletResponse.SC_UNAUTHORIZED,
|
|
||||||
"Not Authorized!",
|
|
||||||
format
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Works for both GET query params and POST form data
|
|
||||||
String sqlQuery = request.getParameter("sql");
|
|
||||||
if (!validateQuery(sqlQuery)) {
|
|
||||||
errorFormatter.format(
|
|
||||||
response,
|
|
||||||
HttpServletResponse.SC_BAD_REQUEST,
|
|
||||||
"Invalid or dangerous SQL query",
|
|
||||||
format
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.log(Level.INFO, "Executing query: {0}", sqlQuery);
|
|
||||||
result = dbManager.executeQuery(sqlQuery);
|
|
||||||
|
|
||||||
if (result.getResultSet() == null) {
|
|
||||||
// Non-query operation — write directly, no temp file needed
|
|
||||||
successFormatter.format(response, "SQL executed successfully", format);
|
|
||||||
logger.info("Non-query SQL executed successfully");
|
|
||||||
} else {
|
|
||||||
// Query operation — buffer to temp file, then serve
|
|
||||||
BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(response);
|
|
||||||
try {
|
|
||||||
ResponseFormatter formatter = FormatterFactory.getFormatter(format, configProps);
|
|
||||||
formatter.format(bufferedResponse, result);
|
|
||||||
bufferedResponse.writeToResponse();
|
|
||||||
logger.info("Query results formatted and served successfully");
|
|
||||||
} catch (IOException | java.sql.SQLException | RuntimeException e) {
|
|
||||||
bufferedResponse.cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException | RuntimeException | java.sql.SQLException | ConnectionPoolException e) {
|
|
||||||
logger.log(Level.SEVERE, "Error processing request", e);
|
|
||||||
if (!response.isCommitted()) {
|
|
||||||
errorFormatter.format(
|
|
||||||
response,
|
|
||||||
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
|
||||||
"An internal error occurred while processing your request",
|
|
||||||
format
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} 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.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Strip comments and string literals so they can't hide keywords
|
|
||||||
String stripped = sqlQuery
|
|
||||||
.replaceAll("(?s)/\\*.*?\\*/", " ") // block comments
|
|
||||||
.replaceAll("--[^\n]*", " ") // line comments
|
|
||||||
.replaceAll("'[^']*'", "''"); // string literals
|
|
||||||
|
|
||||||
return !DANGEROUS_SQL.matcher(stripped).find();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
if (dbManager != null) {
|
|
||||||
dbManager.closePool();
|
|
||||||
logger.info("Database connection pool closed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package com.minicircuits;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.sql.Types;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] words = (input.replaceAll("\\s+", " ").trim()).split("[ _]");
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
|
|
||||||
for (String word : words) {
|
|
||||||
if (!word.isEmpty()) {
|
|
||||||
result
|
|
||||||
.append(Character.toUpperCase(word.charAt(0)))
|
|
||||||
.append(word.substring(1).toLowerCase())
|
|
||||||
.append(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 escapeXml(String input) {
|
|
||||||
if (input == null) return "";
|
|
||||||
return input
|
|
||||||
.replace("&", "&")
|
|
||||||
.replace("<", "<")
|
|
||||||
.replace(">", ">")
|
|
||||||
.replace("\"", """)
|
|
||||||
.replace("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String escapeJson(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
StringBuilder sb = new StringBuilder(s.length());
|
|
||||||
for (int i = 0; i < s.length(); i++) {
|
|
||||||
char c = s.charAt(i);
|
|
||||||
switch (c) {
|
|
||||||
case '"': sb.append("\\\""); break;
|
|
||||||
case '\\': sb.append("\\\\"); break;
|
|
||||||
case '\n': sb.append("\\n"); break;
|
|
||||||
case '\r': sb.append("\\r"); break;
|
|
||||||
case '\t': sb.append("\\t"); break;
|
|
||||||
default:
|
|
||||||
if (c < 0x20) {
|
|
||||||
sb.append(String.format("\\u%04x", (int) c));
|
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "CUSTOMER_NAME" -> "customerName"
|
|
||||||
* "Customer Name" -> "customerName"
|
|
||||||
* "CUSTNAME" -> "custname"
|
|
||||||
*/
|
|
||||||
public static String toCamelCase(String input) {
|
|
||||||
if (input == null || input.isBlank())
|
|
||||||
return "";
|
|
||||||
|
|
||||||
String[] parts = input.split("(?<=[a-z])(?=[A-Z])|[_\\s-]+");
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
for (String part : parts) {
|
|
||||||
if (part.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
String lower = part.toLowerCase();
|
|
||||||
if (sb.length() == 0) {
|
|
||||||
sb.append(lower);
|
|
||||||
} else {
|
|
||||||
sb.append(Character.toUpperCase(lower.charAt(0)))
|
|
||||||
.append(lower.substring(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isNumericType(int sqlType, String typeName) {
|
|
||||||
return sqlType == Types.BIGINT
|
|
||||||
|| sqlType == Types.DECIMAL
|
|
||||||
|| sqlType == Types.DOUBLE
|
|
||||||
|| sqlType == Types.FLOAT
|
|
||||||
|| sqlType == Types.INTEGER
|
|
||||||
|| sqlType == Types.NUMERIC
|
|
||||||
|| sqlType == Types.REAL
|
|
||||||
|| sqlType == Types.SMALLINT
|
|
||||||
|| sqlType == Types.TINYINT
|
|
||||||
|| (sqlType == Types.OTHER && typeName != null && typeName.equalsIgnoreCase("DECFLOAT"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isStringType(int sqlType) {
|
|
||||||
return sqlType == Types.CHAR
|
|
||||||
|| sqlType == Types.CLOB
|
|
||||||
|| sqlType == Types.DATALINK
|
|
||||||
|| sqlType == Types.LONGNVARCHAR
|
|
||||||
|| sqlType == Types.LONGVARCHAR
|
|
||||||
|| sqlType == Types.NCHAR
|
|
||||||
|| sqlType == Types.NCLOB
|
|
||||||
|| sqlType == Types.NVARCHAR
|
|
||||||
|| sqlType == Types.VARCHAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isBinaryType(int sqlType) {
|
|
||||||
return sqlType == Types.BINARY
|
|
||||||
|| sqlType == Types.BIT
|
|
||||||
|| sqlType == Types.BLOB
|
|
||||||
|| sqlType == Types.LONGVARBINARY
|
|
||||||
|| sqlType == Types.VARBINARY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isDateTimeType(int sqlType) {
|
|
||||||
return sqlType == Types.DATE
|
|
||||||
|| sqlType == Types.TIME
|
|
||||||
|| sqlType == Types.TIMESTAMP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String sanitizeXmlTag(String tag) {
|
|
||||||
if (tag == null || tag.isEmpty()) {
|
|
||||||
return "_column";
|
|
||||||
}
|
|
||||||
|
|
||||||
char firstChar = tag.charAt(0);
|
|
||||||
if (!Character.isLetter(firstChar) && firstChar != '_') {
|
|
||||||
tag = "_" + tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tag.replaceAll("[^a-zA-Z0-9_.-]", "_");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package com.minicircuits.formatter;
|
|
||||||
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class ErrorFormatter {
|
|
||||||
|
|
||||||
public void format(HttpServletResponse response, int status, String message, String outputFormat) throws IOException {
|
|
||||||
response.setStatus(status);
|
|
||||||
|
|
||||||
switch (outputFormat) {
|
|
||||||
case "xml":
|
|
||||||
formatXml(response, status, message);
|
|
||||||
break;
|
|
||||||
case "html":
|
|
||||||
formatHtml(response, status, message);
|
|
||||||
break;
|
|
||||||
case "csv":
|
|
||||||
formatCsv(response, status, message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
formatJson(response, status, message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatJson(HttpServletResponse response, int status, String message) throws IOException {
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
JSONObject obj = new JSONObject();
|
|
||||||
obj.put("success", false);
|
|
||||||
obj.put("status", status);
|
|
||||||
obj.put("error", message != null ? message : "Unknown error");
|
|
||||||
out.write(obj.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatXml(HttpServletResponse response, int status, String message) throws IOException {
|
|
||||||
response.setContentType("application/xml");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
|
||||||
out.println("<error>");
|
|
||||||
out.println(" <success>false</success>");
|
|
||||||
out.println(" <status>" + status + "</status>");
|
|
||||||
out.println(" <message>" + Utils.escapeXml(message) + "</message>");
|
|
||||||
out.println("</error>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatHtml(HttpServletResponse response, int status, String message) throws IOException {
|
|
||||||
response.setContentType("text/html");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("<h2>Error " + status + ": " + Utils.escapeHtml(message) + "</h2>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatCsv(HttpServletResponse response, int status, String message) throws IOException {
|
|
||||||
response.setContentType("text/csv");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("success,status,error");
|
|
||||||
out.println("false," + status + ",\"" + (message != null ? message.replace("\"", "\"\"") : "") + "\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package com.minicircuits.formatter;
|
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.*;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public class HtmlFormatter implements ResponseFormatter {
|
|
||||||
private static final Logger logger = Logger.getLogger(HtmlFormatter.class.getName());
|
|
||||||
private final 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()) {
|
|
||||||
writeHtmlStyles(out);
|
|
||||||
out.println("<div class='table-container'>");
|
|
||||||
out.println("<table class='data-table'>");
|
|
||||||
writeHtmlHeaders(out, result.headers);
|
|
||||||
writeHtmlBody(out, result);
|
|
||||||
out.println("</tbody></table>");
|
|
||||||
out.println("</div>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeHtmlStyles(PrintWriter out) {
|
|
||||||
out.println("<style>");
|
|
||||||
out.println(
|
|
||||||
".table-container { display: flex; justify-content: center; max-width: 98vw; margin: 20px auto; }"
|
|
||||||
);
|
|
||||||
out.println(
|
|
||||||
".data-table { border-collapse: separate; border-spacing: 0; width: 100%; font-family: Arial, sans-serif; border-radius: 10px; overflow: hidden; }"
|
|
||||||
);
|
|
||||||
out.println(
|
|
||||||
".data-table th { background-color: #4f4cef; color: white; font-weight: bold; padding: 12px 8px; text-align: center; }"
|
|
||||||
);
|
|
||||||
out.println(
|
|
||||||
".data-table td { padding: 8px; border-bottom: 1px solid #eee; }"
|
|
||||||
);
|
|
||||||
out.println(
|
|
||||||
".data-table tr:nth-child(even) { background-color: #f8f8ff; }"
|
|
||||||
);
|
|
||||||
out.println(".data-table tr:nth-child(odd) { background-color: #ffffff; }");
|
|
||||||
out.println(".data-table tr:hover { background-color: #f0f0ff; }");
|
|
||||||
out.println(".data-table tr:last-child td { border-bottom: none; }");
|
|
||||||
out.println(".data-table th:first-child { border-top-left-radius: 10px; }");
|
|
||||||
out.println(".data-table th:last-child { border-top-right-radius: 10px; }");
|
|
||||||
out.println(
|
|
||||||
".data-table tr:last-child td:first-child { border-bottom-left-radius: 10px; }"
|
|
||||||
);
|
|
||||||
out.println(
|
|
||||||
".data-table tr:last-child td:last-child { border-bottom-right-radius: 10px; }"
|
|
||||||
);
|
|
||||||
out.println("</style>");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeHtmlHeaders(PrintWriter out, String[] headers) {
|
|
||||||
out.println("<thead><tr>");
|
|
||||||
for (String header : headers) {
|
|
||||||
out.println("<th>" + Utils.escapeHtml(header) + "</th>");
|
|
||||||
}
|
|
||||||
out.println("</tr></thead><tbody>");
|
|
||||||
}
|
|
||||||
|
|
||||||
private int writeHtmlBody(PrintWriter out, QueryResult result) throws SQLException {
|
|
||||||
int batchSize = Integer.parseInt(
|
|
||||||
configProps.getProperty("html.batch.size", "1000")
|
|
||||||
);
|
|
||||||
int currentBatch = 0;
|
|
||||||
int rowCount = 0;
|
|
||||||
int colCount = result.metadata.getColumnCount();
|
|
||||||
|
|
||||||
while (result.resultSet.next()) {
|
|
||||||
out.println("<tr>");
|
|
||||||
for (int i = 1; i <= colCount; i++) {
|
|
||||||
String value = result.resultSet.getString(i);
|
|
||||||
int columnType = result.metadata.getColumnType(i);
|
|
||||||
String columnTypeName = result.metadata.getColumnTypeName(i);
|
|
||||||
|
|
||||||
if (Utils.isNumericType(columnType, columnTypeName)) {
|
|
||||||
out.print("<td style=\"text-align:right\">");
|
|
||||||
} else {
|
|
||||||
out.print("<td>");
|
|
||||||
}
|
|
||||||
out.print(Utils.escapeHtml(value != null ? Utils.rightTrim(value) : ""));
|
|
||||||
out.println("</td>");
|
|
||||||
}
|
|
||||||
out.println("</tr>");
|
|
||||||
rowCount++;
|
|
||||||
|
|
||||||
if (++currentBatch >= batchSize) {
|
|
||||||
out.flush();
|
|
||||||
currentBatch = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rowCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.minicircuits.formatter;
|
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.Properties;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public class JsonFormatter implements ResponseFormatter {
|
|
||||||
|
|
||||||
public JsonFormatter(Properties configProps) {
|
|
||||||
// configProps not needed by this formatter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
response.setBufferSize(8192);
|
|
||||||
|
|
||||||
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream(), 8192);
|
|
||||||
OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
|
|
||||||
|
|
||||||
int colCount = result.metadata.getColumnCount();
|
|
||||||
osw.write("[");
|
|
||||||
boolean firstRow = true;
|
|
||||||
int rowCount = 0;
|
|
||||||
|
|
||||||
while (result.resultSet.next()) {
|
|
||||||
if (!firstRow) {
|
|
||||||
osw.write(",");
|
|
||||||
}
|
|
||||||
firstRow = false;
|
|
||||||
|
|
||||||
osw.write("{");
|
|
||||||
for (int i = 1; i <= colCount; i++) {
|
|
||||||
if (i > 1) osw.write(",");
|
|
||||||
String columnName = result.metadata.getColumnName(i);
|
|
||||||
String value = result.resultSet.getString(i);
|
|
||||||
|
|
||||||
int columnType = result.metadata.getColumnType(i);
|
|
||||||
String columnTypeName = result.metadata.getColumnTypeName(i);
|
|
||||||
|
|
||||||
osw.write("\"");
|
|
||||||
osw.write(Utils.escapeJson(columnName));
|
|
||||||
osw.write("\":");
|
|
||||||
if (value == null) {
|
|
||||||
osw.write("null");
|
|
||||||
} else if (Utils.isNumericType(columnType, columnTypeName)) {
|
|
||||||
try {
|
|
||||||
String trimmed = value.trim();
|
|
||||||
if (columnType == Types.FLOAT
|
|
||||||
|| columnType == Types.DOUBLE
|
|
||||||
|| columnType == Types.REAL
|
|
||||||
|| result.metadata.getScale(i) > 0) {
|
|
||||||
osw.write(String.valueOf(Double.parseDouble(trimmed)));
|
|
||||||
} else {
|
|
||||||
osw.write(String.valueOf(Long.parseLong(trimmed)));
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
osw.write("\"");
|
|
||||||
osw.write(Utils.escapeJson(Utils.rightTrim(value)));
|
|
||||||
osw.write("\"");
|
|
||||||
}
|
|
||||||
} else if (columnType == Types.BOOLEAN) {
|
|
||||||
osw.write(String.valueOf(result.resultSet.getBoolean(i)));
|
|
||||||
} else {
|
|
||||||
osw.write("\"");
|
|
||||||
osw.write(Utils.escapeJson(Utils.rightTrim(value)));
|
|
||||||
osw.write("\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
osw.write("}");
|
|
||||||
rowCount++;
|
|
||||||
|
|
||||||
if (rowCount % 1000 == 0) {
|
|
||||||
osw.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
osw.write("]");
|
|
||||||
osw.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package com.minicircuits.formatter;
|
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public class RestFormatter implements ResponseFormatter {
|
|
||||||
|
|
||||||
public RestFormatter(Properties configProps) {
|
|
||||||
// configProps not needed by this formatter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
response.setBufferSize(8192);
|
|
||||||
|
|
||||||
// Phase 1: Write data array to a temp file
|
|
||||||
Path dataTempFile = Files.createTempFile("fetchapi_data_", ".tmp");
|
|
||||||
int rowCount = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
try (BufferedOutputStream dataBos = new BufferedOutputStream(new FileOutputStream(dataTempFile.toFile()), 8192);
|
|
||||||
OutputStreamWriter dataWriter = new OutputStreamWriter(dataBos, StandardCharsets.UTF_8)) {
|
|
||||||
|
|
||||||
int colCount = result.metadata.getColumnCount();
|
|
||||||
String[] keys = resolveColumnKeys(result);
|
|
||||||
|
|
||||||
dataWriter.write("[");
|
|
||||||
|
|
||||||
while (result.resultSet.next()) {
|
|
||||||
if (rowCount > 0) {
|
|
||||||
dataWriter.write(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
dataWriter.write("{");
|
|
||||||
for (int i = 1; i <= colCount; i++) {
|
|
||||||
if (i > 1) dataWriter.write(",");
|
|
||||||
|
|
||||||
String value = result.resultSet.getString(i);
|
|
||||||
int columnType = result.metadata.getColumnType(i);
|
|
||||||
String columnTypeName = result.metadata.getColumnTypeName(i);
|
|
||||||
|
|
||||||
dataWriter.write("\"");
|
|
||||||
dataWriter.write(Utils.escapeJson(keys[i - 1]));
|
|
||||||
dataWriter.write("\":");
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
dataWriter.write("null");
|
|
||||||
} else if (Utils.isNumericType(columnType, columnTypeName)) {
|
|
||||||
try {
|
|
||||||
String trimmed = value.trim();
|
|
||||||
if (columnType == Types.FLOAT
|
|
||||||
|| columnType == Types.DOUBLE
|
|
||||||
|| columnType == Types.REAL
|
|
||||||
|| result.metadata.getScale(i) > 0) {
|
|
||||||
dataWriter.write(String.valueOf(Double.parseDouble(trimmed)));
|
|
||||||
} else {
|
|
||||||
dataWriter.write(String.valueOf(Long.parseLong(trimmed)));
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
dataWriter.write("\"");
|
|
||||||
dataWriter.write(Utils.escapeJson(Utils.rightTrim(value)));
|
|
||||||
dataWriter.write("\"");
|
|
||||||
}
|
|
||||||
} else if (columnType == Types.BOOLEAN) {
|
|
||||||
dataWriter.write(String.valueOf(result.resultSet.getBoolean(i)));
|
|
||||||
} else {
|
|
||||||
dataWriter.write("\"");
|
|
||||||
dataWriter.write(Utils.escapeJson(Utils.rightTrim(value)));
|
|
||||||
dataWriter.write("\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataWriter.write("}");
|
|
||||||
rowCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataWriter.write("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Write final envelope with records BEFORE data
|
|
||||||
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream(), 8192);
|
|
||||||
OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
|
|
||||||
|
|
||||||
osw.write("{\"success\":true,\"currentTimestamp\":\"");
|
|
||||||
osw.write(new Timestamp(System.currentTimeMillis()).toString());
|
|
||||||
osw.write("\",\"records\":");
|
|
||||||
osw.write(Integer.toString(rowCount));
|
|
||||||
osw.write(",\"data\":");
|
|
||||||
osw.flush();
|
|
||||||
|
|
||||||
// Copy data temp file to response
|
|
||||||
try (InputStream dataIn = new BufferedInputStream(new FileInputStream(dataTempFile.toFile()), 8192)) {
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = dataIn.read(buffer)) != -1) {
|
|
||||||
bos.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the envelope - write directly to bos since we bypassed osw for the copy
|
|
||||||
bos.write("}".getBytes(StandardCharsets.UTF_8));
|
|
||||||
bos.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
Files.deleteIfExists(dataTempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves all column keys, camelCasing them and appending a numeric
|
|
||||||
* suffix to duplicates: customerName, customerName2, customerName3, etc.
|
|
||||||
*/
|
|
||||||
private String[] resolveColumnKeys(QueryResult result) throws SQLException {
|
|
||||||
int colCount = result.metadata.getColumnCount();
|
|
||||||
String[] keys = new String[colCount];
|
|
||||||
Map<String, Integer> seen = new HashMap<>();
|
|
||||||
|
|
||||||
for (int i = 1; i <= colCount; i++) {
|
|
||||||
String columnName = result.metadata.getColumnName(i);
|
|
||||||
String columnLabel = result.metadata.getColumnLabel(i);
|
|
||||||
|
|
||||||
String key = resolveColumnKey(columnName, columnLabel);
|
|
||||||
int count = seen.getOrDefault(key, 0) + 1;
|
|
||||||
seen.put(key, count);
|
|
||||||
|
|
||||||
keys[i - 1] = count == 1 ? key : key + count;
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If column label exists camelCase it, else column name
|
|
||||||
*/
|
|
||||||
private String resolveColumnKey(String columnName, String columnLabel) {
|
|
||||||
String source;
|
|
||||||
if (columnLabel != null && !columnLabel.trim().isEmpty()) {
|
|
||||||
source = columnLabel.trim();
|
|
||||||
} else {
|
|
||||||
source = columnName.trim();
|
|
||||||
}
|
|
||||||
return Utils.toCamelCase(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.minicircuits.formatter;
|
|
||||||
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class SuccessFormatter {
|
|
||||||
|
|
||||||
public void format(HttpServletResponse response, String message, String outputFormat) throws IOException {
|
|
||||||
switch (outputFormat) {
|
|
||||||
case "xml":
|
|
||||||
formatXml(response, message);
|
|
||||||
break;
|
|
||||||
case "html":
|
|
||||||
formatHtml(response, message);
|
|
||||||
break;
|
|
||||||
case "csv":
|
|
||||||
formatCsv(response, message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
formatJson(response, message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatJson(HttpServletResponse response, String message) throws IOException {
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
JSONObject obj = new JSONObject();
|
|
||||||
obj.put("success", true);
|
|
||||||
obj.put("message", message != null ? message : "SQL executed successfully");
|
|
||||||
out.write(obj.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatXml(HttpServletResponse response, String message) throws IOException {
|
|
||||||
response.setContentType("application/xml");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
|
||||||
out.println("<result>");
|
|
||||||
out.println(" <success>true</success>");
|
|
||||||
out.println(" <message>" + Utils.escapeXml(message) + "</message>");
|
|
||||||
out.println("</result>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatHtml(HttpServletResponse response, String message) throws IOException {
|
|
||||||
response.setContentType("text/html");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("<h2>Success: " + Utils.escapeHtml(message) + "</h2>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatCsv(HttpServletResponse response, String message) throws IOException {
|
|
||||||
response.setContentType("text/csv");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
try (PrintWriter out = response.getWriter()) {
|
|
||||||
out.println("success,message");
|
|
||||||
out.println("true,\"" + (message != null ? message.replace("\"", "\"\"") : "") + "\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-11
@@ -1,19 +1,12 @@
|
|||||||
|
|
||||||
package com.minicircuits;
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import javax.servlet.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.FilterConfig;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.ServletRequest;
|
|
||||||
import javax.servlet.ServletResponse;
|
|
||||||
|
|
||||||
public class CharacterEncodingFilter implements Filter {
|
public class CharacterEncodingFilter implements Filter {
|
||||||
private String encoding;
|
private String encoding;
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) throws ServletException {
|
public void init(FilterConfig config) throws ServletException {
|
||||||
encoding = config.getInitParameter("encoding");
|
encoding = config.getInitParameter("encoding");
|
||||||
if (encoding == null) {
|
if (encoding == null) {
|
||||||
@@ -21,7 +14,6 @@ public class CharacterEncodingFilter implements Filter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response,
|
public void doFilter(ServletRequest request, ServletResponse response,
|
||||||
FilterChain chain) throws IOException, ServletException {
|
FilterChain chain) throws IOException, ServletException {
|
||||||
request.setCharacterEncoding(encoding);
|
request.setCharacterEncoding(encoding);
|
||||||
@@ -29,7 +21,6 @@ public class CharacterEncodingFilter implements Filter {
|
|||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-26
@@ -1,26 +1,19 @@
|
|||||||
package com.minicircuits;
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
|
import com.ibm.as400.access.*;
|
||||||
import java.beans.PropertyVetoException;
|
import java.beans.PropertyVetoException;
|
||||||
import java.sql.Connection;
|
import java.sql.*;
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.ResultSetMetaData;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Statement;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import static dev.alexzaw.fetchapi.Utils.*;
|
||||||
|
|
||||||
import com.ibm.as400.access.AS400JDBCConnectionPool;
|
|
||||||
import com.ibm.as400.access.AS400JDBCConnectionPoolDataSource;
|
|
||||||
import com.ibm.as400.access.ConnectionPoolException;
|
|
||||||
|
|
||||||
public class DatabaseManager {
|
public class DatabaseManager {
|
||||||
private static final String JDBC_DRIVER = "com.ibm.as400.access.AS400JDBCDriver";
|
private static final String JDBC_DRIVER = "com.ibm.as400.access.AS400JDBCDriver";
|
||||||
private static final Logger logger = Logger.getLogger(DatabaseManager.class.getName());
|
private static final Logger logger = Logger.getLogger(DatabaseManager.class.getName());
|
||||||
|
|
||||||
private AS400JDBCConnectionPool pool;
|
private AS400JDBCConnectionPool pool;
|
||||||
private final Properties configProps;
|
private Properties configProps;
|
||||||
|
|
||||||
public DatabaseManager(Properties configProps) throws Exception {
|
public DatabaseManager(Properties configProps) throws Exception {
|
||||||
this.configProps = configProps;
|
this.configProps = configProps;
|
||||||
@@ -31,7 +24,7 @@ public class DatabaseManager {
|
|||||||
private void initializeConnectionPool() throws Exception {
|
private void initializeConnectionPool() throws Exception {
|
||||||
try {
|
try {
|
||||||
String username = configProps.getProperty("db.username");
|
String username = configProps.getProperty("db.username");
|
||||||
String password = Utils.decodeBase64(configProps.getProperty("db.password"));
|
String password = decodeBase64(configProps.getProperty("db.password"));
|
||||||
String serverName = configProps.getProperty("db.server", "localhost");
|
String serverName = configProps.getProperty("db.server", "localhost");
|
||||||
String jdbcString = configProps.getProperty("db.jdbcString");
|
String jdbcString = configProps.getProperty("db.jdbcString");
|
||||||
String libraries = configProps.getProperty("db.libraries", "QGPL,QTEMP");
|
String libraries = configProps.getProperty("db.libraries", "QGPL,QTEMP");
|
||||||
@@ -70,6 +63,7 @@ public class DatabaseManager {
|
|||||||
dataSource.setPassword(password);
|
dataSource.setPassword(password);
|
||||||
dataSource.setProperties(jdbcString);
|
dataSource.setProperties(jdbcString);
|
||||||
dataSource.setLibraries(libraries);
|
dataSource.setLibraries(libraries);
|
||||||
|
dataSource.setExtendedMetaData(true);
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,21 +74,16 @@ public class DatabaseManager {
|
|||||||
|
|
||||||
public QueryResult executeQuery(String sqlQuery) throws SQLException, ConnectionPoolException {
|
public QueryResult executeQuery(String sqlQuery) throws SQLException, ConnectionPoolException {
|
||||||
// Log connection pool status
|
// Log connection pool status
|
||||||
logger.log(Level.INFO, "Connection pool before query - Active connections: {0}, Available connections: {1}",
|
logger.info("Connection pool before query - Active connections: " + pool.getActiveConnectionCount() +
|
||||||
new Object[]{pool.getActiveConnectionCount(), pool.getAvailableConnectionCount()});
|
", Available connections: " + pool.getAvailableConnectionCount());
|
||||||
|
|
||||||
Connection connection = pool.getConnection();
|
Connection connection = pool.getConnection();
|
||||||
logger.log(Level.INFO, "Got new connection from pool: {0}", connection.hashCode());
|
logger.info("Got new connection from pool: " + connection.hashCode());
|
||||||
|
|
||||||
PreparedStatement statement = null;
|
PreparedStatement statement = null;
|
||||||
ResultSet resultSet = null;
|
ResultSet resultSet = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set up library list
|
|
||||||
try (Statement libStmt = connection.createStatement()) {
|
|
||||||
libStmt.execute("Values Amulibq.Set_Library_List()");
|
|
||||||
}
|
|
||||||
|
|
||||||
statement = connection.prepareStatement(
|
statement = connection.prepareStatement(
|
||||||
sqlQuery,
|
sqlQuery,
|
||||||
ResultSet.TYPE_FORWARD_ONLY,
|
ResultSet.TYPE_FORWARD_ONLY,
|
||||||
@@ -113,7 +102,15 @@ public class DatabaseManager {
|
|||||||
// Handle SELECT queries
|
// Handle SELECT queries
|
||||||
resultSet = statement.getResultSet();
|
resultSet = statement.getResultSet();
|
||||||
String[] headers = getQueryHeaders(resultSet);
|
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(
|
return new QueryResult(
|
||||||
connection,
|
connection,
|
||||||
statement,
|
statement,
|
||||||
@@ -135,7 +132,7 @@ public class DatabaseManager {
|
|||||||
// Clean up resources in case of error
|
// Clean up resources in case of error
|
||||||
if (resultSet != null) resultSet.close();
|
if (resultSet != null) resultSet.close();
|
||||||
if (statement != null) statement.close();
|
if (statement != null) statement.close();
|
||||||
connection.close();
|
if (connection != null) connection.close();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,13 +143,17 @@ public class DatabaseManager {
|
|||||||
String[] headers = new String[columnCount];
|
String[] headers = new String[columnCount];
|
||||||
|
|
||||||
for (int i = 1; i <= columnCount; i++) {
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
// Prefer alias/label if defined
|
||||||
String label = rsmd.getColumnLabel(i);
|
String label = rsmd.getColumnLabel(i);
|
||||||
headers[i - 1] = (label != null && !label.trim().isEmpty())
|
if (label == null || label.trim().isEmpty()) {
|
||||||
? Utils.toTitleCase(label.trim())
|
label = rsmd.getColumnName(i);
|
||||||
: Utils.toTitleCase(rsmd.getColumnName(i));
|
}
|
||||||
|
// Title-case only for pretty outputs (HTML/Excel)
|
||||||
|
headers[i - 1] = toTitleCase(label.trim());
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void closePool() {
|
public void closePool() {
|
||||||
if (pool != null) {
|
if (pool != null) {
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -1,10 +1,9 @@
|
|||||||
package com.minicircuits;
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.ResultSetMetaData;
|
import java.sql.ResultSetMetaData;
|
||||||
import java.sql.SQLException;
|
|
||||||
|
|
||||||
public class QueryResult implements AutoCloseable {
|
public class QueryResult implements AutoCloseable {
|
||||||
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.minicircuits;
|
package dev.alexzaw.fetchapi;
|
||||||
|
|
||||||
import java.sql.ResultSetMetaData;
|
import java.sql.ResultSetMetaData;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -10,7 +10,7 @@ import org.apache.poi.ss.usermodel.*;
|
|||||||
|
|
||||||
public class StyleManager {
|
public class StyleManager {
|
||||||
private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();
|
private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();
|
||||||
private final Properties configProps;
|
private Properties configProps;
|
||||||
|
|
||||||
public StyleManager(Properties configProps) {
|
public StyleManager(Properties configProps) {
|
||||||
this.configProps = configProps;
|
this.configProps = configProps;
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-26
@@ -1,7 +1,5 @@
|
|||||||
package com.minicircuits.formatter;
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import com.minicircuits.Utils;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -9,9 +7,11 @@ import java.util.Properties;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
public class CsvFormatter implements ResponseFormatter {
|
public class CsvFormatter implements ResponseFormatter {
|
||||||
private static final Logger logger = Logger.getLogger(CsvFormatter.class.getName());
|
private static final Logger logger = Logger.getLogger(CsvFormatter.class.getName());
|
||||||
private final Properties configProps;
|
private Properties configProps;
|
||||||
private static final char DEFAULT_DELIMITER = ',';
|
private static final char DEFAULT_DELIMITER = ',';
|
||||||
private static final char DEFAULT_QUOTE = '"';
|
private static final char DEFAULT_QUOTE = '"';
|
||||||
|
|
||||||
@@ -34,34 +34,17 @@ public class CsvFormatter implements ResponseFormatter {
|
|||||||
// Write header row
|
// Write header row
|
||||||
writeRow(writer, result.headers, delimiter);
|
writeRow(writer, result.headers, delimiter);
|
||||||
|
|
||||||
// Cache column types to avoid repeated metadata calls
|
|
||||||
int colCount = result.metadata.getColumnCount();
|
|
||||||
int[] columnTypes = new int[colCount];
|
|
||||||
String[] columnTypeNames = new String[colCount];
|
|
||||||
for (int i = 0; i < colCount; i++) {
|
|
||||||
columnTypes[i] = result.metadata.getColumnType(i + 1);
|
|
||||||
columnTypeNames[i] = result.metadata.getColumnTypeName(i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write data rows
|
// Write data rows
|
||||||
int rowCount = 0;
|
|
||||||
while (result.resultSet.next()) {
|
while (result.resultSet.next()) {
|
||||||
String[] rowData = new String[colCount];
|
String[] rowData = new String[result.metadata.getColumnCount()];
|
||||||
for (int i = 1; i <= colCount; i++) {
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
String value = result.resultSet.getString(i);
|
String value = result.resultSet.getString(i);
|
||||||
if (value == null) {
|
rowData[i-1] = value != null ? value.trim() : "";
|
||||||
rowData[i-1] = "";
|
|
||||||
} else if (Utils.isNumericType(columnTypes[i-1], columnTypeNames[i-1])) {
|
|
||||||
rowData[i-1] = value.trim();
|
|
||||||
} else {
|
|
||||||
rowData[i-1] = Utils.rightTrim(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
writeRow(writer, rowData, delimiter);
|
writeRow(writer, rowData, delimiter);
|
||||||
rowCount++;
|
|
||||||
|
|
||||||
// Flush periodically
|
// Flush periodically
|
||||||
if (rowCount % 1000 == 0) {
|
if (result.resultSet.getRow() % 1000 == 0) {
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+5
-4
@@ -1,7 +1,5 @@
|
|||||||
package com.minicircuits.formatter;
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import com.minicircuits.StyleManager;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.ResultSetMetaData;
|
import java.sql.ResultSetMetaData;
|
||||||
@@ -13,9 +11,12 @@ import org.apache.poi.ss.usermodel.*;
|
|||||||
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
||||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
import dev.alexzaw.fetchapi.StyleManager;
|
||||||
|
|
||||||
public class ExcelFormatter implements ResponseFormatter {
|
public class ExcelFormatter implements ResponseFormatter {
|
||||||
private static final Logger logger = Logger.getLogger(ExcelFormatter.class.getName());
|
private static final Logger logger = Logger.getLogger(ExcelFormatter.class.getName());
|
||||||
private final Properties configProps;
|
private Properties configProps;
|
||||||
private StyleManager styleManager;
|
private StyleManager styleManager;
|
||||||
|
|
||||||
public ExcelFormatter(Properties configProps) {
|
public ExcelFormatter(Properties configProps) {
|
||||||
+25
-31
@@ -1,31 +1,25 @@
|
|||||||
package com.minicircuits.formatter;
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
public class FormatterFactory {
|
public class FormatterFactory {
|
||||||
|
|
||||||
public static ResponseFormatter getFormatter(String format, Properties configProps) {
|
public static ResponseFormatter getFormatter(String format, Properties configProps) {
|
||||||
if ("excel".equalsIgnoreCase(format)) {
|
if ("excel".equalsIgnoreCase(format)) {
|
||||||
return new ExcelFormatter(configProps);
|
return new ExcelFormatter(configProps);
|
||||||
} else if ("html".equalsIgnoreCase(format)) {
|
} else if ("html".equalsIgnoreCase(format)) {
|
||||||
return new HtmlFormatter(configProps);
|
return new HtmlFormatter(configProps);
|
||||||
} else if ("csv".equalsIgnoreCase(format)) {
|
} else if ("csv".equalsIgnoreCase(format)) {
|
||||||
return new CsvFormatter(configProps);
|
return new CsvFormatter(configProps);
|
||||||
} else if ("xml".equalsIgnoreCase(format)) {
|
} else if ("xml".equalsIgnoreCase(format)) {
|
||||||
return new XmlFormatter(configProps);
|
return new XmlFormatter(configProps);
|
||||||
} else if ("rest".equalsIgnoreCase(format)) {
|
} else {
|
||||||
return new RestFormatter(configProps);
|
// Default to JSON
|
||||||
} else {
|
return new JsonFormatter(configProps);
|
||||||
// Default to JSON
|
}
|
||||||
return new JsonFormatter(configProps);
|
}
|
||||||
}
|
|
||||||
}
|
public static ErrorFormatter createErrorFormatter() {
|
||||||
|
return new ErrorFormatter();
|
||||||
public static ErrorFormatter createErrorFormatter() {
|
}
|
||||||
return new ErrorFormatter();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static SuccessFormatter createSuccessFormatter() {
|
|
||||||
return new SuccessFormatter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
package com.minicircuits.formatter;
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
public interface ResponseFormatter {
|
public interface ResponseFormatter {
|
||||||
void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException;
|
void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException;
|
||||||
}
|
}
|
||||||
+90
-86
@@ -1,86 +1,90 @@
|
|||||||
package com.minicircuits.formatter;
|
package dev.alexzaw.fetchapi.formatter;
|
||||||
|
|
||||||
import com.minicircuits.QueryResult;
|
import java.io.*;
|
||||||
import com.minicircuits.Utils;
|
import java.sql.SQLException;
|
||||||
import java.io.*;
|
import java.util.Properties;
|
||||||
import java.sql.SQLException;
|
import java.util.logging.Logger;
|
||||||
import java.util.Properties;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.logging.Logger;
|
import javax.xml.stream.XMLOutputFactory;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.xml.stream.XMLStreamException;
|
||||||
import javax.xml.stream.XMLOutputFactory;
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
import javax.xml.stream.XMLStreamException;
|
|
||||||
import javax.xml.stream.XMLStreamWriter;
|
import dev.alexzaw.fetchapi.QueryResult;
|
||||||
|
|
||||||
public class XmlFormatter implements ResponseFormatter {
|
public class XmlFormatter implements ResponseFormatter {
|
||||||
private static final Logger logger = Logger.getLogger(XmlFormatter.class.getName());
|
private static final Logger logger = Logger.getLogger(XmlFormatter.class.getName());
|
||||||
|
private Properties configProps;
|
||||||
public XmlFormatter(Properties configProps) {
|
|
||||||
// configProps not needed by this formatter
|
public XmlFormatter(Properties configProps) {
|
||||||
}
|
this.configProps = configProps;
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
@Override
|
||||||
response.setContentType("application/xml");
|
public void format(HttpServletResponse response, QueryResult result) throws IOException, SQLException {
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setContentType("application/xml");
|
||||||
response.setBufferSize(8192);
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setBufferSize(8192);
|
||||||
try {
|
|
||||||
// Create XML stream writer
|
try {
|
||||||
XMLOutputFactory factory = XMLOutputFactory.newInstance();
|
// Create XML stream writer
|
||||||
XMLStreamWriter writer = factory.createXMLStreamWriter(
|
XMLOutputFactory factory = XMLOutputFactory.newInstance();
|
||||||
response.getOutputStream(), "UTF-8");
|
XMLStreamWriter writer = factory.createXMLStreamWriter(
|
||||||
|
response.getOutputStream(), "UTF-8");
|
||||||
// Start document
|
|
||||||
writer.writeStartDocument("UTF-8", "1.0");
|
// Start document
|
||||||
writer.writeStartElement("data");
|
writer.writeStartDocument("UTF-8", "1.0");
|
||||||
|
writer.writeStartElement("data");
|
||||||
// Write data rows
|
|
||||||
int rowCount = 0;
|
// Write data rows
|
||||||
while (result.resultSet.next()) {
|
int rowCount = 0;
|
||||||
writer.writeStartElement("row");
|
while (result.resultSet.next()) {
|
||||||
|
writer.writeStartElement("row");
|
||||||
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
|
||||||
String columnName = Utils.sanitizeXmlTag(result.metadata.getColumnName(i));
|
for (int i = 1; i <= result.metadata.getColumnCount(); i++) {
|
||||||
String value = result.resultSet.getString(i);
|
String columnName = sanitizeXmlTag(result.metadata.getColumnName(i));
|
||||||
|
String value = result.resultSet.getString(i);
|
||||||
int columnType = result.metadata.getColumnType(i);
|
|
||||||
String columnTypeName = result.metadata.getColumnTypeName(i);
|
writer.writeStartElement(columnName);
|
||||||
|
if (value != null) {
|
||||||
writer.writeStartElement(columnName);
|
writer.writeCharacters(value.trim());
|
||||||
if (Utils.isNumericType(columnType, columnTypeName)) {
|
}
|
||||||
writer.writeAttribute("type", "numeric");
|
writer.writeEndElement(); // column
|
||||||
} else if (Utils.isDateTimeType(columnType)) {
|
}
|
||||||
writer.writeAttribute("type", "datetime");
|
|
||||||
} else if (Utils.isBinaryType(columnType)) {
|
writer.writeEndElement(); // row
|
||||||
writer.writeAttribute("type", "binary");
|
|
||||||
} else if (columnType == java.sql.Types.BOOLEAN) {
|
// Flush periodically
|
||||||
writer.writeAttribute("type", "boolean");
|
if (++rowCount % 1000 == 0) {
|
||||||
} else {
|
writer.flush();
|
||||||
writer.writeAttribute("type", "string");
|
}
|
||||||
}
|
}
|
||||||
if (value != null) {
|
|
||||||
writer.writeCharacters(Utils.rightTrim(value));
|
// End document
|
||||||
}
|
writer.writeEndElement(); // data
|
||||||
writer.writeEndElement(); // column
|
writer.writeEndDocument();
|
||||||
}
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
writer.writeEndElement(); // row
|
|
||||||
|
} catch (XMLStreamException e) {
|
||||||
// Flush periodically
|
logger.severe("Error generating XML: " + e.getMessage());
|
||||||
if (++rowCount % 1000 == 0) {
|
throw new IOException("Error generating XML", e);
|
||||||
writer.flush();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private String sanitizeXmlTag(String tag) {
|
||||||
// End document
|
// XML element names must start with a letter or underscore and can contain
|
||||||
writer.writeEndElement(); // data
|
// letters, digits, hyphens, underscores, and periods
|
||||||
writer.writeEndDocument();
|
if (tag == null || tag.isEmpty()) {
|
||||||
writer.flush();
|
return "_column";
|
||||||
writer.close();
|
}
|
||||||
|
|
||||||
} catch (XMLStreamException e) {
|
// Ensure first character is valid
|
||||||
logger.severe("Error generating XML: " + e.getMessage());
|
char firstChar = tag.charAt(0);
|
||||||
throw new IOException("Error generating XML", e);
|
if (!Character.isLetter(firstChar) && firstChar != '_') {
|
||||||
}
|
tag = "_" + tag;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Replace invalid characters
|
||||||
|
return tag.replaceAll("[^a-zA-Z0-9_.-]", "_");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
version="3.1">
|
version="3.1">
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>EncodingFilter</filter-name>
|
<filter-name>EncodingFilter</filter-name>
|
||||||
<filter-class>com.minicircuits.CharacterEncodingFilter</filter-class>
|
<filter-class>dev.alexzaw.fetchapi.CharacterEncodingFilter</filter-class>
|
||||||
<init-param>
|
<init-param>
|
||||||
<param-name>encoding</param-name>
|
<param-name>encoding</param-name>
|
||||||
<param-value>UTF-8</param-value>
|
<param-value>UTF-8</param-value>
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
<url-pattern>/*</url-pattern>
|
<url-pattern>/*</url-pattern>
|
||||||
</filter-mapping>
|
</filter-mapping>
|
||||||
<servlet>
|
<servlet>
|
||||||
<servlet-name>FetchAPIServlet</servlet-name>
|
<servlet-name>FetchAPI</servlet-name>
|
||||||
<servlet-class>com.minicircuits.FetchAPI</servlet-class>
|
<servlet-class>dev.alexzaw.fetchapi.FetchAPIServlet</servlet-class>
|
||||||
<load-on-startup>1</load-on-startup>
|
<load-on-startup>1</load-on-startup>
|
||||||
</servlet>
|
</servlet>
|
||||||
<servlet-mapping>
|
<servlet-mapping>
|
||||||
<servlet-name>FetchAPIServlet</servlet-name>
|
<servlet-name>FetchAPI</servlet-name>
|
||||||
<url-pattern>/FetchAPIServlet</url-pattern>
|
<url-pattern>/</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
</web-app>
|
</web-app>
|
||||||
|
|||||||
Reference in New Issue
Block a user