diff --git a/cygnus-lib/pom.xml b/cygnus-lib/pom.xml new file mode 100644 index 0000000..a4a37cb --- /dev/null +++ b/cygnus-lib/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.cygnus + cygnus-parent + 1.0.0-SNAPSHOT + ../pom.xml + + cygnus-lib + jar + Cygnus Shared Library + + + org.projectlombok + lombok + ${lombok.version} + provided + true + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/cygnus-lib/src/main/java/lib/constants/ApplicationError.java b/cygnus-lib/src/main/java/lib/constants/ApplicationError.java new file mode 100644 index 0000000..6c1cfac --- /dev/null +++ b/cygnus-lib/src/main/java/lib/constants/ApplicationError.java @@ -0,0 +1,71 @@ +package lib.constants; + +import java.util.Arrays; + +/** Canonical user-facing errors shared by Cygnus applications. */ +public enum ApplicationError { + BAD_REQUEST(400, "HTTP-400", "Invalid request", + "Cygnus could not process the submitted request.", + "Review the supplied information and try again."), + SESSION_REQUIRED(401, "AUTH-401", "Sign in required", + "Your session is missing or has expired.", + "Sign in again to continue securely."), + ACCESS_DENIED(403, "AUTH-403", "Access denied", + "You do not have permission to open this page.", + "Contact your administrator if this feature should be available to you."), + PAGE_NOT_FOUND(404, "HTTP-404", "Page not found", + "The requested page could not be found.", + "Check the address or return to the application dashboard."), + METHOD_NOT_ALLOWED(405, "HTTP-405", "Action not allowed", + "This page does not support the requested action.", + "Return to the previous page and try an available action."), + CONFLICT(409, "HTTP-409", "Request conflict", + "The request conflicts with the current state of the data.", + "Refresh the page and try again."), + PAYLOAD_TOO_LARGE(413, "HTTP-413", "File is too large", + "The submitted content exceeds the permitted size.", + "Reduce the file size and try again."), + TOO_MANY_REQUESTS(429, "HTTP-429", "Too many requests", + "Cygnus has received too many requests in a short period.", + "Wait briefly before trying again."), + INTERNAL_SERVER_ERROR(500, "HTTP-500", "Something went wrong", + "Cygnus could not complete your request.", + "Try again. If the issue continues, share the reference ID with support."), + BAD_GATEWAY(502, "HTTP-502", "Service response unavailable", + "A required service returned an invalid response.", + "Try again shortly. If the issue continues, contact support."), + SERVICE_UNAVAILABLE(503, "HTTP-503", "Service temporarily unavailable", + "A required Cygnus service is currently unavailable.", + "Wait briefly and try again."), + GATEWAY_TIMEOUT(504, "HTTP-504", "Service response timed out", + "A required service took too long to respond.", + "Try the request again shortly."); + + private final int httpStatus; + private final String code; + private final String title; + private final String message; + private final String description; + + ApplicationError( + int httpStatus, String code, String title, String message, String description) { + this.httpStatus = httpStatus; + this.code = code; + this.title = title; + this.message = message; + this.description = description; + } + + public static ApplicationError fromHttpStatus(int status) { + return Arrays.stream(values()) + .filter(error -> error.httpStatus == status) + .findFirst() + .orElse(INTERNAL_SERVER_ERROR); + } + + public int getHttpStatus() { return httpStatus; } + public String getCode() { return code; } + public String getTitle() { return title; } + public String getMessage() { return message; } + public String getDescription() { return description; } +} diff --git a/cygnus-lib/src/main/java/lib/models/CasePunching.java b/cygnus-lib/src/main/java/lib/models/CasePunching.java new file mode 100644 index 0000000..98073ac --- /dev/null +++ b/cygnus-lib/src/main/java/lib/models/CasePunching.java @@ -0,0 +1,22 @@ +package lib.models; + +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CasePunching { + private ErrorDetails errorDetails; + private Map options; + private List visibleSections; + private Integer portfolioId; + private Integer formMode; + private String userId; + private String dynamicFields; + private String dynamicHtml; + private String verificationCaseId; + private String documentCaseId; +} diff --git a/cygnus-lib/src/main/java/lib/models/ErrorDetails.java b/cygnus-lib/src/main/java/lib/models/ErrorDetails.java new file mode 100644 index 0000000..c57584d --- /dev/null +++ b/cygnus-lib/src/main/java/lib/models/ErrorDetails.java @@ -0,0 +1,58 @@ +package lib.models; + +import lib.constants.ApplicationError; +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; +import lombok.Getter; + +/** Dependency-free error information shared across Cygnus modules. */ +@Getter +public final class ErrorDetails implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private final int httpStatus; + private final String errorCode; + private final String title; + private final String message; + private final String description; + private final String referenceId; + private final Instant timestamp; + + public ErrorDetails(ApplicationError error) { + this(error.getHttpStatus(), error.getCode(), error.getTitle(), + error.getMessage(), error.getDescription(), + UUID.randomUUID().toString(), Instant.now()); + } + + private ErrorDetails( + int httpStatus, + String errorCode, + String title, + String message, + String description, + String referenceId, + Instant timestamp) { + if (httpStatus < 400 || httpStatus > 599) { + throw new IllegalArgumentException("HTTP error status must be between 400 and 599"); + } + this.httpStatus = httpStatus; + this.errorCode = required(errorCode, "errorCode"); + this.title = required(title, "title"); + this.message = required(message, "message"); + this.description = description == null ? "" : description.trim(); + this.referenceId = required(referenceId, "referenceId"); + this.timestamp = Objects.requireNonNull(timestamp, "timestamp"); + } + + private static String required(String value, String name) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(name + " is required"); + } + return value.trim(); + } + +} diff --git a/cygnus-lib/src/test/java/lib/models/ErrorDetailsTest.java b/cygnus-lib/src/test/java/lib/models/ErrorDetailsTest.java new file mode 100644 index 0000000..ef9ad9c --- /dev/null +++ b/cygnus-lib/src/test/java/lib/models/ErrorDetailsTest.java @@ -0,0 +1,22 @@ +package lib.models; + +import lib.constants.ApplicationError; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ErrorDetailsTest { + @Test + void createsAReusableErrorContract() { + ErrorDetails details = new ErrorDetails(ApplicationError.ACCESS_DENIED); + assertEquals(403, details.getHttpStatus()); + assertEquals("AUTH-403", details.getErrorCode()); + } + + @Test + void mapsHttpStatusToCanonicalError() { + assertEquals(ApplicationError.PAGE_NOT_FOUND, ApplicationError.fromHttpStatus(404)); + assertEquals(ApplicationError.INTERNAL_SERVER_ERROR, + ApplicationError.fromHttpStatus(599)); + } +} diff --git a/cygnus-lib/target/classes/lib/constants/ApplicationError.class b/cygnus-lib/target/classes/lib/constants/ApplicationError.class new file mode 100644 index 0000000..dec4d78 Binary files /dev/null and b/cygnus-lib/target/classes/lib/constants/ApplicationError.class differ diff --git a/cygnus-lib/target/classes/lib/models/CasePunching.class b/cygnus-lib/target/classes/lib/models/CasePunching.class new file mode 100644 index 0000000..c03a105 Binary files /dev/null and b/cygnus-lib/target/classes/lib/models/CasePunching.class differ diff --git a/cygnus-lib/target/classes/lib/models/ErrorDetails.class b/cygnus-lib/target/classes/lib/models/ErrorDetails.class new file mode 100644 index 0000000..320b593 Binary files /dev/null and b/cygnus-lib/target/classes/lib/models/ErrorDetails.class differ diff --git a/cygnus-lib/target/cygnus-lib-1.0.0-SNAPSHOT.jar b/cygnus-lib/target/cygnus-lib-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..48ce4b9 Binary files /dev/null and b/cygnus-lib/target/cygnus-lib-1.0.0-SNAPSHOT.jar differ diff --git a/cygnus-lib/target/maven-archiver/pom.properties b/cygnus-lib/target/maven-archiver/pom.properties new file mode 100644 index 0000000..ccdce53 --- /dev/null +++ b/cygnus-lib/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=cygnus-lib +groupId=com.cygnus +version=1.0.0-SNAPSHOT diff --git a/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..f7912d0 --- /dev/null +++ b/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,3 @@ +lib/constants/ApplicationError.class +lib/models/ErrorDetails.class +lib/models/CasePunching.class diff --git a/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..9004c25 --- /dev/null +++ b/cygnus-lib/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,3 @@ +/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/constants/ApplicationError.java +/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/CasePunching.java +/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ErrorDetails.java diff --git a/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..f236490 --- /dev/null +++ b/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -0,0 +1 @@ +lib/models/ErrorDetailsTest.class diff --git a/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..15f82d3 --- /dev/null +++ b/cygnus-lib/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -0,0 +1 @@ +/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/test/java/lib/models/ErrorDetailsTest.java diff --git a/cygnus-lib/target/surefire-reports/TEST-lib.models.ErrorDetailsTest.xml b/cygnus-lib/target/surefire-reports/TEST-lib.models.ErrorDetailsTest.xml new file mode 100644 index 0000000..34d6b87 --- /dev/null +++ b/cygnus-lib/target/surefire-reports/TEST-lib.models.ErrorDetailsTest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cygnus-lib/target/surefire-reports/lib.models.ErrorDetailsTest.txt b/cygnus-lib/target/surefire-reports/lib.models.ErrorDetailsTest.txt new file mode 100644 index 0000000..a5a4b55 --- /dev/null +++ b/cygnus-lib/target/surefire-reports/lib.models.ErrorDetailsTest.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: lib.models.ErrorDetailsTest +------------------------------------------------------------------------------- +Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.025 s -- in lib.models.ErrorDetailsTest diff --git a/cygnus-lib/target/test-classes/lib/models/ErrorDetailsTest.class b/cygnus-lib/target/test-classes/lib/models/ErrorDetailsTest.class new file mode 100644 index 0000000..6467965 Binary files /dev/null and b/cygnus-lib/target/test-classes/lib/models/ErrorDetailsTest.class differ diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/app-config.xml b/cygnus-onprem-app/build/WebContent/WEB-INF/app-config.xml index aef4c94..fe631de 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/app-config.xml +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/app-config.xml @@ -10,7 +10,7 @@ - + diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/addcases.jsp b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/addcases.jsp index 8033ea1..c23153a 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/addcases.jsp +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/addcases.jsp @@ -33,7 +33,7 @@ - + @@ -591,7 +591,7 @@
- + Other Details
diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initcase.jsp b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initcase.jsp index fd41b19..9faf01d 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initcase.jsp +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initcase.jsp @@ -3,6 +3,12 @@ <%@page language="java" session="true" %> <%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + + + @@ -14,8 +20,8 @@ - - + + @@ -25,55 +31,66 @@ - - - - - - - - - - - - + + + + + + + + + + + + - +
-<%@ include file="/WEB-INF/app/fragments/app-title.jspf" %> +<%@ include file="/WEB-INF/app/fragments/app-title.jspf" %> - - - + + + -
+
-
- - Common Details - - +
+ + Common Details + + - - - + + + - -
- - - - + + + + + + + + + + + + + +
@@ -116,9 +133,14 @@
@@ -131,9 +153,10 @@
@@ -162,9 +185,10 @@
@@ -177,9 +201,10 @@
@@ -287,9 +312,10 @@
@@ -347,7 +373,7 @@ Office Details
OVR  - RCO  + RCO  Same Office OTV
@@ -404,9 +430,10 @@
@@ -482,8 +509,8 @@ Property Details
PVR  - RCP  - OCP  + RCP  + OCP  Same Property
@@ -530,9 +557,10 @@ @@ -621,12 +649,15 @@
- + Other Details
- ${dynamichtml} + + + +


@@ -650,16 +681,22 @@


- + - + + + + + + + - +
@@ -667,25 +704,40 @@ - +
- - + + + + + +
- - - - + diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initdocs.jsp b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initdocs.jsp index fa1642e..a1eb3c8 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initdocs.jsp +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/app/edp/punching/initdocs.jsp @@ -17,7 +17,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/app/error.jsp b/cygnus-onprem-app/build/WebContent/WEB-INF/app/error.jsp index db03a30..5d6c6b0 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/app/error.jsp +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/app/error.jsp @@ -1,29 +1,67 @@ -<%@ page language="java" contentType="text/html; charset=ISO-8859-1" - pageEncoding="ISO-8859-1"%> - - - - - - - - - - - - -Cygnus 1.0 | 404 Error - - - - -
-
-
404
-

Page unavailable

-

Unauthorized access or the requested page could not be found.

- -
-
+<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + + + + + + + Cygnus 1.0 | <c:out value="${empty errorDetails.title ? 'Page unavailable' : errorDetails.title}" /> + + + + + <%@ include file="/WEB-INF/app/fragments/app-shell-header.jspf" %> + + + <%@ include file="/WEB-INF/app/fragments/app-title.jspf" %> + + + +
+ +
diff --git a/cygnus-onprem-app/build/WebContent/WEB-INF/web.xml b/cygnus-onprem-app/build/WebContent/WEB-INF/web.xml index 4346b95..6819398 100644 --- a/cygnus-onprem-app/build/WebContent/WEB-INF/web.xml +++ b/cygnus-onprem-app/build/WebContent/WEB-INF/web.xml @@ -37,7 +37,15 @@ 180 - + + + 404 + /ver/error + + + 500 + /ver/error + org.springframework.web.HttpSessionRequiredException /matrix/index.jsp diff --git a/cygnus-onprem-app/build/WebContent/css/add-cases-v4.css b/cygnus-onprem-app/build/WebContent/css/add-cases-v4.css index 46872fe..bbaf9af 100644 --- a/cygnus-onprem-app/build/WebContent/css/add-cases-v4.css +++ b/cygnus-onprem-app/build/WebContent/css/add-cases-v4.css @@ -31,6 +31,12 @@ margin: 0; } +/* Legacy forms separate labels and controls with a BR. In the modern flex + layout that BR becomes an extra flex row and creates an unintended gap. */ +.matrix-add-cases .matrix-form-controls .widget > br { + display: none; +} + .matrix-add-cases .matrix-form-controls .widget[style*="display:none"] { display: none !important; } @@ -89,3 +95,87 @@ .matrix-add-cases .matrix-form-controls .widget:has(#splist), .matrix-add-cases .matrix-form-controls .widget:has(textarea) { grid-column: span 6; } } +.matrix-add-cases .inputcontainer { + position: relative; +} + +.matrix-add-cases .inputcontainer > input.errtxt, +.matrix-add-cases .inputcontainer > textarea.errtxt { + padding-right: 24px !important; +} + +.matrix-add-cases .matrix-validation-error-icon { + display: block; + margin: 0 !important; + object-fit: contain; +} + +.matrix-add-cases #section2 > .title, +.matrix-add-cases #section3 > .title, +.matrix-add-cases #section4 > .title, +.matrix-add-cases #section5 > .title, +.matrix-add-cases #section6 > .title, +.matrix-add-cases #section7 > .title { + box-sizing: border-box; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px 8px; + min-height: 38px; + height: auto !important; + padding: 5px 10px !important; +} + +.matrix-add-cases #section2 > .title > .matrix-title-icon, +.matrix-add-cases #section3 > .title > .matrix-title-icon, +.matrix-add-cases #section4 > .title > .matrix-title-icon, +.matrix-add-cases #section5 > .title > .matrix-title-icon, +.matrix-add-cases #section6 > .title > .matrix-title-icon, +.matrix-add-cases #section7 > .title > .matrix-title-icon { + display: block; + flex: 0 0 16px; + width: 16px; + height: 16px; + margin: 0 !important; + object-fit: contain; + vertical-align: middle; +} + +.matrix-add-cases #section2 > .title > .matrix-title-text, +.matrix-add-cases #section3 > .title > .matrix-title-text, +.matrix-add-cases #section4 > .title > .matrix-title-text, +.matrix-add-cases #section5 > .title > .matrix-title-text, +.matrix-add-cases #section6 > .title > .matrix-title-text, +.matrix-add-cases #section7 > .title > .matrix-title-text { + line-height: 16px; +} + +.matrix-add-cases #section2 > .title > .divchk, +.matrix-add-cases #section3 > .title > .divchk, +.matrix-add-cases #section4 > .title > .divchk, +.matrix-add-cases #section5 > .title > .divchk, +.matrix-add-cases #section7 > .title > .divchk { + position: static !important; + right: auto !important; + float: none !important; + display: inline-flex; + align-items: center; + align-self: center; + gap: 4px; + min-height: 18px; + margin: 0 0 0 auto !important; + padding: 0 !important; + line-height: 18px !important; + white-space: nowrap; +} + +.matrix-add-cases #section2 > .title > .divchk input[type="checkbox"], +.matrix-add-cases #section3 > .title > .divchk input[type="checkbox"], +.matrix-add-cases #section4 > .title > .divchk input[type="checkbox"], +.matrix-add-cases #section5 > .title > .divchk input[type="checkbox"], +.matrix-add-cases #section7 > .title > .divchk input[type="checkbox"] { + flex: 0 0 auto; + margin-top: 0 !important; + margin-bottom: 0 !important; + vertical-align: middle; +} diff --git a/cygnus-onprem-app/build/WebContent/css/matrix-theme-v2.css b/cygnus-onprem-app/build/WebContent/css/matrix-theme-v2.css index e5b6776..c862c45 100644 --- a/cygnus-onprem-app/build/WebContent/css/matrix-theme-v2.css +++ b/cygnus-onprem-app/build/WebContent/css/matrix-theme-v2.css @@ -119,45 +119,103 @@ body .nav-menu { } body.matrix-error-page { - display: grid; min-height: 100vh; margin: 0; - padding: 24px; - place-items: center; color: #263746; background: var(--matrix-theme-canvas) !important; font-family: Arial, sans-serif; } -.matrix-error-card { - width: min(100%, 480px); +.matrix-error-layout { + min-height: calc(100vh - 84px); + display: grid; + place-items: center; + box-sizing: border-box; + padding: clamp(24px, 5vw, 64px) 20px; +} + +.matrix-error-card { + width: min(100%, 760px); + display: grid; + grid-template-columns: 150px minmax(0, 1fr); + overflow: hidden; box-sizing: border-box; - padding: 30px; - text-align: center; border: 1px solid var(--matrix-theme-border); - border-radius: 8px; + border-radius: 10px; background: var(--matrix-theme-panel); box-shadow: 0 8px 24px rgba(31, 52, 72, .12); } -.matrix-error-card__code { - color: #b42318; - font-size: 38px; +.matrix-error-card__status { + min-height: 290px; + display: grid; + place-items: center; + color: #fff; + background: #a4473f; +} + +.matrix-error-page--401 .matrix-error-card__status, +.matrix-error-page--403 .matrix-error-card__status { + background: #a66a19; +} + +.matrix-error-page--404 .matrix-error-card__status, +.matrix-error-page--405 .matrix-error-card__status { + background: #376b94; +} + +.matrix-error-card__status span { + font-size: 42px; font-weight: 800; line-height: 1; } -.matrix-error-card h1 { - margin: 10px 0 6px; - font-size: 21px; +.matrix-error-card__content { + padding: 34px 38px 28px; } -.matrix-error-card p { - margin: 0 0 20px; +.matrix-error-card__eyebrow { + color: #607587; + font-size: 12px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; +} + +.matrix-error-card h1 { + margin: 8px 0 10px; + font-size: 25px; +} + +.matrix-error-card__message { + margin: 0 0 8px; + color: #344b5e; + font-size: 15px; + font-weight: 600; +} + +.matrix-error-card__description { + margin: 0; color: #536779; + line-height: 1.55; +} + +.matrix-error-card__actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 9px; + margin-top: 24px; +} + +.matrix-error-card__actions form { + margin: 0; } .matrix-error-card__action { + display: inline-flex; + align-items: center; + justify-content: center; min-height: 34px; padding: 6px 16px; color: #fff; @@ -166,6 +224,55 @@ body.matrix-error-page { background: #245a91; font-weight: 700; cursor: pointer; + text-decoration: none; +} + +.matrix-error-card__action:hover, +.matrix-error-card__action:focus { + color: #fff; + background: #1d4e7d; +} + +.matrix-error-card__action--secondary { + color: #334b5e; + border-color: #aebdca; + background: transparent; +} + +.matrix-error-card__action--secondary:hover, +.matrix-error-card__action--secondary:focus { + color: #263746; + background: #e4edf4; +} + +.matrix-error-card__reference { + margin: 22px 0 0; + padding-top: 14px; + color: #6b7e8e; + border-top: 1px solid #d5e0e8; + font-size: 11px; +} + +.matrix-error-card__reference code { + color: inherit; +} + +@media (max-width: 620px) { + .matrix-error-card { + grid-template-columns: 1fr; + } + + .matrix-error-card__status { + min-height: 82px; + } + + .matrix-error-card__status span { + font-size: 30px; + } + + .matrix-error-card__content { + padding: 26px 24px; + } } /* Compact toolbars for legacy forms that place controls in their title bar. */ diff --git a/cygnus-onprem-app/build/WebContent/js/appjs/punching.js b/cygnus-onprem-app/build/WebContent/js/appjs/punching.js index 5637afd..55f79d9 100644 --- a/cygnus-onprem-app/build/WebContent/js/appjs/punching.js +++ b/cygnus-onprem-app/build/WebContent/js/appjs/punching.js @@ -12,14 +12,25 @@ AddrElem[1]=["ov","companyname","oaddr1","oaddr2","oaddr3","olandmark","Ocolony" AddrElem[2]=["pv","paddr1","paddr2","paddr3","plandmark","Pcolony","colp","pcity","ppincode"]; AddrElem[3]=["bankcode","bankcode","bank_branch_id","product","loanamount","apptype","contactperson","specialinst"]; -function InitPage(port,viscontents) -{ - centerDivBoth("formcontainer","H"); +function InitPage() +{ + var portField = document.getElementById("portfolio_id"); + var initialPortField = document.getElementById("initial-portfolio-id"); + var visibleContentsField = document.getElementById("visiblecontents"); + var port = initialPortField ? initialPortField.value : (portField ? portField.value : "-1"); + var viscontents = visibleContentsField ? visibleContentsField.value : ""; + if (viscontents && viscontents.slice(-ColDelim.length) !== ColDelim) { + viscontents += ColDelim; + } + + centerDivBoth("formcontainer","H"); HideSections('rtv'); HideSections('otv'); HideSections('section'); UnhideSections(viscontents,ColDelim); - $("#portfolio_id").val(port); + if (portField) { + $(portField).val(port); + } if(port>0) { if(document.getElementById("ajaxmsg") !== null) diff --git a/cygnus-onprem-app/build/WebContent/js/validator.js b/cygnus-onprem-app/build/WebContent/js/validator.js index 76c72f0..2de93e0 100644 --- a/cygnus-onprem-app/build/WebContent/js/validator.js +++ b/cygnus-onprem-app/build/WebContent/js/validator.js @@ -82,39 +82,44 @@ function GetRegExp(el,ValidFormat) if(ValidFormat=='PAN'){return /^(([a-zA-Z]{5})\d{4}([a-zA-Z]{1}))$/;} if(ValidFormat=='CreditCard'){return /^\d{16}$/;} } -function highlightField(el) -{ +function highlightField(el) +{ if (document.getElementById('err'+el.id)) { //nothing to do already exists } - else - { - invalidFields=invalidFields+1; - var offset=null; - var width=null; - if(el.type=='text' || el.type=='textarea' || el.type=='password') - { - el.setAttribute("class", "textbox errtxt"); - offset=$(el).offset(); - width=el.offsetWidth; - } - else if(el.type=='select-one') - { - el.parentNode.setAttribute("class", "divselect errdiv"); - tmpel=el.parentNode; - offset=$(tmpel).offset(); - width=tmpel.offsetWidth; - } - var top=parseInt(offset.top)-5; - var left=parseInt(offset.left)+(width-5); - var img=document.createElement("img"); - img.setAttribute("src","/matrix/images/exclamation.png"); - img.setAttribute("style","position:absolute;margin-left:"+left+"px;margin-top:"+top+"px;z-index:3"); - img.setAttribute("id","err"+el.id); - document.body.appendChild(img); - } -} + else + { + invalidFields=invalidFields+1; + var iconHost=el.parentNode; + var current=el.parentNode; + while(current && current!==document.body) + { + var classes=" "+(current.className || "")+" "; + if(classes.indexOf(" inputcontainer ")!==-1) + { + iconHost=current; + break; + } + current=current.parentNode; + } + if(el.type=='text' || el.type=='textarea' || el.type=='password') + { + el.setAttribute("class", "textbox errtxt"); + } + else if(el.type=='select-one') + { + el.parentNode.setAttribute("class", "divselect errdiv"); + } + iconHost.style.position="relative"; + var img=document.createElement("img"); + img.setAttribute("src","/matrix/images/exclamation.png"); + img.setAttribute("class", "matrix-validation-error-icon"+(el.type=='select-one' ? " matrix-validation-error-icon--select" : "")); + img.setAttribute("style","position:absolute;right:"+(el.type=='select-one' ? "24px" : "5px")+";top:50%;width:14px;height:14px;transform:translateY(-50%);z-index:3;pointer-events:none"); + img.setAttribute("id","err"+el.id); + iconHost.appendChild(img); + } +} // DATE FORMAT VALIDATOR DD/MM/YYYY function addSlashes(el,evt) diff --git a/cygnus-onprem-app/pom.xml b/cygnus-onprem-app/pom.xml index 219aa37..fefbd34 100644 --- a/cygnus-onprem-app/pom.xml +++ b/cygnus-onprem-app/pom.xml @@ -23,6 +23,16 @@ + + com.cygnus + cygnus-lib + ${project.version} + + + com.cygnus + cygnus-onprem-db + ${project.version} + com.cygnus cygnus-cloud-client diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/controller/AbstractAuthenticatedController.java b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/AbstractAuthenticatedController.java new file mode 100644 index 0000000..d522876 --- /dev/null +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/AbstractAuthenticatedController.java @@ -0,0 +1,63 @@ +package matrix.nimble.controller; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lib.constants.ApplicationError; +import lib.models.ErrorDetails; +import matrix.nimble.model.Session; +import matrix.services.commons.CommonErrorService; +import matrix.services.commons.CommonService; +import org.springframework.ui.ModelMap; + +/** Shared session and page-authorization workflow for authenticated controllers. */ +public abstract class AbstractAuthenticatedController { + private final CommonService commonService; + private final CommonErrorService errorService; + + protected AbstractAuthenticatedController( + CommonService commonService, + CommonErrorService errorService) { + this.commonService = commonService; + this.errorService = errorService; + } + + protected final PageAuthorization authorizePage( + String pageRoute, + ModelMap model, + HttpSession httpSession, + HttpServletResponse response) { + Session session = commonService.getSession(httpSession); + if (session == null) { + return denied(model, response, httpSession, ApplicationError.SESSION_REQUIRED); + } + if (!commonService.hasPageAccess(session, pageRoute)) { + return denied(model, response, httpSession, ApplicationError.ACCESS_DENIED); + } + model.addAttribute(CommonService.SESSION_ATTRIBUTE, session); + return PageAuthorization.granted(session); + } + + private PageAuthorization denied( + ModelMap model, + HttpServletResponse response, + HttpSession httpSession, + ApplicationError error) { + String viewName = errorService.render( + model, response, httpSession, new ErrorDetails(error)); + return PageAuthorization.denied(viewName); + } + + protected record PageAuthorization(Session session, String viewName) { + private static PageAuthorization granted(Session session) { + return new PageAuthorization(session, null); + } + + private static PageAuthorization denied(String viewName) { + return new PageAuthorization(null, viewName); + } + + public boolean isGranted() { + return session != null; + } + } +} diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/controller/ApplicationErrorController.java b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/ApplicationErrorController.java new file mode 100644 index 0000000..92e9e86 --- /dev/null +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/ApplicationErrorController.java @@ -0,0 +1,43 @@ +package matrix.nimble.controller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lib.models.ErrorDetails; +import lib.constants.ApplicationError; +import matrix.services.commons.CommonErrorService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class ApplicationErrorController { + private final CommonErrorService errorService; + + public ApplicationErrorController(CommonErrorService errorService) { + this.errorService = errorService; + } + + @RequestMapping("error") + public String error( + ModelMap model, + HttpServletRequest request, + HttpServletResponse response, + HttpSession httpSession) { + int status = status(request); + return errorService.render(model, response, httpSession, details(status)); + } + + private int status(HttpServletRequest request) { + Object value = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + if (value instanceof Integer status && status >= 400 && status <= 599) { + return status; + } + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + + private ErrorDetails details(int status) { + return new ErrorDetails(ApplicationError.fromHttpStatus(status)); + } +} diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/controller/SessionController.java b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/SessionController.java index 0cce47b..82be155 100644 --- a/cygnus-onprem-app/src/main/java/matrix/nimble/controller/SessionController.java +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/controller/SessionController.java @@ -4,6 +4,7 @@ import matrix.nimble.cloud.identity.CloudAuthenticationException; import matrix.nimble.cloud.identity.CloudAuthenticationGateway; import matrix.nimble.model.Login; import matrix.nimble.model.Session; +import matrix.services.commons.CommonService; //servlet libraries import jakarta.servlet.http.HttpServletRequest; @@ -21,9 +22,13 @@ import org.springframework.web.bind.annotation.SessionAttributes; @SessionAttributes("Sessvals") public class SessionController { private final CloudAuthenticationGateway cloudAuthenticationGateway; + private final CommonService commonService; - public SessionController(CloudAuthenticationGateway cloudAuthenticationGateway) { + public SessionController( + CloudAuthenticationGateway cloudAuthenticationGateway, + CommonService commonService) { this.cloudAuthenticationGateway = cloudAuthenticationGateway; + this.commonService = commonService; } @RequestMapping(value="login", method={RequestMethod.GET, RequestMethod.POST}) public String LoginPage(ModelMap model) @@ -42,13 +47,17 @@ public class SessionController { return "login"; } @RequestMapping(value="authenticatelogin",method=RequestMethod.POST ) - public String Authenticate(ModelMap model,@ModelAttribute(value="login") Login login) + public String Authenticate( + ModelMap model, + @ModelAttribute(value="login") Login login, + HttpSession httpSession) { try { Session cloudSession = cloudAuthenticationGateway.authenticate( login.getLoginid(), login.getPassword()); model.remove("login"); model.addAttribute("Sessvals", cloudSession); + commonService.storeSession(httpSession, cloudSession); return "home"; } catch (CloudAuthenticationException exception) { login.setErrMsg(exception.reason() diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/edp/punching/controller/CasePucnhing.java b/cygnus-onprem-app/src/main/java/matrix/nimble/edp/punching/controller/CasePucnhing.java deleted file mode 100644 index f88f45f..0000000 --- a/cygnus-onprem-app/src/main/java/matrix/nimble/edp/punching/controller/CasePucnhing.java +++ /dev/null @@ -1,161 +0,0 @@ -package matrix.nimble.edp.punching.controller; - -//servlet libraries -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; - -//spring libraries -import matrix.nimble.edp.punching.model.CaseDetails; -import matrix.nimble.model.Session; -import matrix.nimble.utilities.ModuleFunctions; -import matrix.nimble.utilities.GlobalClass; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.SessionAttributes; - -@Controller -@SessionAttributes({"Sessvals"}) -public class CasePucnhing { - @RequestMapping(value="caseadd1",method=RequestMethod.POST ) - public String OpenAddCases(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals) - { - CaseDetails caseDetails=new CaseDetails(); - caseDetails.setFormMode(1); - caseDetails.setErrMsg(""); - model.addAttribute("visibleContents",""); - model.addAttribute("caseDetails",caseDetails); - model.addAttribute("branchlist",FillBranchList("0")); - model.addAttribute("prodlist",FillProductList("0")); - model.addAttribute("citylist",FillCityList("0")); - model.addAttribute("typelist",FillApptypeList("0")); - model.addAttribute("doclist",FillDocList("0")); - model.addAttribute("catlist",FillCatList("0")); - model.addAttribute("Sessvals",Sessvals); - model.addAttribute("portlist",FillPortList(Sessvals.getBranchID())); - return "edp/punching/addcases"; - } - @RequestMapping(value="casesave",method=RequestMethod.POST ) - public String SaveCase(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="caseDetails") CaseDetails caseDet,HttpSession session) - { - String DynamicNsessVals=""; - CaseDetails caseDetails=new CaseDetails(); - caseDet.setErrCode("1001"); - if(caseDet.getDynamicFields()!="") - { - String [] DynamicFields=caseDet.getDynamicFields().split(GlobalClass.ColDelim); - for(int indx=0;indx CURRENT = new AtomicReference<>(); + + private ModernDbExecutors() {} + + public static void install(CygnusDbExecutor executor) { + CURRENT.set(java.util.Objects.requireNonNull(executor, "executor")); + } + + public static CygnusDbExecutor current() { + CygnusDbExecutor executor = CURRENT.get(); + if (executor == null) throw new IllegalStateException("Modern database executor is not initialized"); + return executor; + } + + public static void clear(CygnusDbExecutor executor) { + CURRENT.compareAndSet(executor, null); + } +} diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/query/RedisCachingQueryProvider.java b/cygnus-onprem-app/src/main/java/matrix/nimble/query/RedisCachingQueryProvider.java index d1479ec..f792bf5 100644 --- a/cygnus-onprem-app/src/main/java/matrix/nimble/query/RedisCachingQueryProvider.java +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/query/RedisCachingQueryProvider.java @@ -7,7 +7,7 @@ import java.util.logging.Logger; public final class RedisCachingQueryProvider implements QueryProvider { - public static final Duration QUERY_TTL = Duration.ofHours(12); + public static final Duration QUERY_TTL = Duration.ofHours(3); private static final Logger LOGGER = Logger.getLogger(RedisCachingQueryProvider.class.getName()); private static final int LOCK_COUNT = 64; diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DBFunctions.java b/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DBFunctions.java index 0709d7d..54c3c0f 100644 --- a/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DBFunctions.java +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DBFunctions.java @@ -1,6 +1,14 @@ package matrix.nimble.utilities; import java.sql.*; import java.util.Hashtable; +import java.util.List; +import java.util.Optional; +import com.cygnus.db.DbResult; +import com.cygnus.db.RowMapper; +import com.cygnus.db.RowView; +import com.cygnus.db.RowConsumer; +import com.cygnus.db.TransactionCallback; +import matrix.nimble.query.ModernDbExecutors; public class DBFunctions { private Connection con; @@ -294,7 +302,7 @@ public class DBFunctions { } return DataSet; } - public String FetchRunQuery(int QueryCode,String Vals[]) + public String FetchRunQuery(int QueryCode,String Vals[]) { InitConfiguration(); setErrMsg(""); @@ -367,7 +375,60 @@ public class DBFunctions { { return getErrMsg(); } - } + } + + public DbResult execute(int queryId, Object[] parameters) { + return ModernDbExecutors.current().execute(queryId, parameters); + } + + public DbResult execute(int queryId, Object[] parameters, Class responseType) { + return ModernDbExecutors.current().execute(queryId, parameters, responseType); + } + + public List query(int queryId, Object[] parameters, Class responseType) { + return ModernDbExecutors.current().query(queryId, parameters, responseType); + } + + public List query(int queryId, Object[] parameters, RowMapper mapper) { + return ModernDbExecutors.current().query(queryId, parameters, mapper); + } + + public Optional queryOne(int queryId, Object[] parameters, Class responseType) { + return ModernDbExecutors.current().queryOne(queryId, parameters, responseType); + } + + public int insert(int queryId, Object[] parameters) { + return ModernDbExecutors.current().insert(queryId, parameters); + } + + public K insert(int queryId, Object[] parameters, Class generatedKeyType) { + return ModernDbExecutors.current().insert(queryId, parameters, generatedKeyType); + } + + public int update(int queryId, Object[] parameters) { + return ModernDbExecutors.current().update(queryId, parameters); + } + + public int delete(int queryId, Object[] parameters) { + return ModernDbExecutors.current().delete(queryId, parameters); + } + + public T procedure(int queryId, Object[] parameters, Class responseType) { + return ModernDbExecutors.current().procedure(queryId, parameters, responseType); + } + + public List procedureRows(int queryId, Object[] parameters, Class responseType) { + return ModernDbExecutors.current().procedureRows(queryId, parameters, responseType); + } + + public void stream( + int queryId, Object[] parameters, RowMapper mapper, RowConsumer consumer) { + ModernDbExecutors.current().stream(queryId, parameters, mapper, consumer); + } + + public T transaction(TransactionCallback callback) { + return ModernDbExecutors.current().transaction(callback); + } @SuppressWarnings("finally") public String QueryBuilder(String Query,String[] Val) { diff --git a/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DatabaseConnectionPool.java b/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DatabaseConnectionPool.java index 468a78a..6b92a22 100644 --- a/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DatabaseConnectionPool.java +++ b/cygnus-onprem-app/src/main/java/matrix/nimble/utilities/DatabaseConnectionPool.java @@ -5,6 +5,7 @@ import java.sql.SQLException; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import javax.sql.DataSource; /** Shared, bounded JDBC connection pool for the legacy database layer. */ public final class DatabaseConnectionPool { @@ -20,6 +21,10 @@ public final class DatabaseConnectionPool { return getDataSource().getConnection(); } + public static DataSource dataSource() { + return getDataSource(); + } + private static HikariDataSource getDataSource() { HikariDataSource current = dataSource; if (current != null) { diff --git a/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonErrorService.java b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonErrorService.java new file mode 100644 index 0000000..94bcde7 --- /dev/null +++ b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonErrorService.java @@ -0,0 +1,33 @@ +package matrix.services.commons; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lib.models.ErrorDetails; +import matrix.nimble.model.Session; +import org.springframework.stereotype.Service; +import org.springframework.ui.ModelMap; + +@Service +public class CommonErrorService { + public static final String ERROR_ATTRIBUTE = "errorDetails"; + private final CommonService commonService; + + public CommonErrorService(CommonService commonService) { + this.commonService = commonService; + } + + public String render( + ModelMap model, + HttpServletResponse response, + HttpSession httpSession, + ErrorDetails errorDetails) { + response.setStatus(errorDetails.getHttpStatus()); + model.addAttribute(ERROR_ATTRIBUTE, errorDetails); + + Session session = commonService.getSession(httpSession); + if (session != null) { + model.addAttribute(CommonService.SESSION_ATTRIBUTE, session); + } + return "error"; + } +} diff --git a/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java new file mode 100644 index 0000000..d7bb7ad --- /dev/null +++ b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java @@ -0,0 +1,73 @@ +package matrix.services.commons; + +import jakarta.servlet.http.HttpSession; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import matrix.nimble.model.Session; +import org.springframework.stereotype.Service; + +@Service +public class CommonService { + public static final String SESSION_ATTRIBUTE = "Sessvals"; + + private static final Pattern MENU_COMMAND = Pattern.compile( + "SubmitMenuCommand\\(\\s*'([^']+)'\\s*,", + Pattern.CASE_INSENSITIVE); + + public Session getSession(HttpSession httpSession) { + if (httpSession == null) { + return null; + } + Object value = httpSession.getAttribute(SESSION_ATTRIBUTE); + return value instanceof Session session ? session : null; + } + + public void storeSession(HttpSession httpSession, Session session) { + Objects.requireNonNull(httpSession, "httpSession"); + httpSession.setAttribute(SESSION_ATTRIBUTE, Objects.requireNonNull(session, "session")); + } + + public boolean hasPageAccess(HttpSession httpSession, String pageRoute) { + return hasPageAccess(getSession(httpSession), pageRoute); + } + + public boolean hasPageAccess(Session session, String pageRoute) { + if (session == null || session.getMenuHtml() == null) { + return false; + } + + String requiredRoute = normalizeRoute(pageRoute); + if (requiredRoute.isEmpty()) { + return false; + } + + Matcher matcher = MENU_COMMAND.matcher(session.getMenuHtml()); + while (matcher.find()) { + if (normalizeRoute(matcher.group(1)).equals(requiredRoute)) { + return true; + } + } + return false; + } + + private String normalizeRoute(String route) { + if (route == null) { + return ""; + } + String normalized = route.trim().replace('\\', '/'); + int query = normalized.indexOf('?'); + if (query >= 0) { + normalized = normalized.substring(0, query); + } + int fragment = normalized.indexOf('#'); + if (fragment >= 0) { + normalized = normalized.substring(0, fragment); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + int slash = normalized.lastIndexOf('/'); + return slash >= 0 ? normalized.substring(slash + 1) : normalized; + } +} diff --git a/cygnus-onprem-app/src/main/java/matrix/services/edp/PunchingService.java b/cygnus-onprem-app/src/main/java/matrix/services/edp/PunchingService.java new file mode 100644 index 0000000..ddf7a9d --- /dev/null +++ b/cygnus-onprem-app/src/main/java/matrix/services/edp/PunchingService.java @@ -0,0 +1,166 @@ +package matrix.services.edp; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.springframework.stereotype.Service; + +import lib.models.CasePunching; +import lib.models.ErrorDetails; +import lib.constants.ApplicationError; +import matrix.nimble.edp.punching.model.PunchingHandler; +import matrix.nimble.utilities.CommonFunctions; +import matrix.nimble.utilities.GlobalClass; + +@Service +public class PunchingService { + + private static final String PORTFOLIO = "portfolio."; + private static final String BRANCH = "branch."; + private static final String PRODUCT = "product."; + private static final String CITY = "city."; + private static final String APPLICATION_TYPE = "applicationType."; + private static final String CATEGORY = "category."; + + public CasePunching init( + String branchIds, + String userId, + String verificationCaseId, + String documentCaseId) { + PunchingHandler punchingHandler = new PunchingHandler(); + punchingHandler.setErrCode("1001"); + punchingHandler.GetDDValues(3, branchIds.split(GlobalClass.ColDelim)); + + CasePunching casePunching = new CasePunching(); + Map options = new LinkedHashMap<>(); + addOptions(options, PORTFOLIO, punchingHandler.getOptionvals()); + casePunching.setOptions(Collections.unmodifiableMap(options)); + casePunching.setVisibleSections(List.of()); + casePunching.setPortfolioId(-1); + casePunching.setFormMode(0); + casePunching.setUserId(userId); + casePunching.setDynamicFields(""); + casePunching.setDynamicHtml(""); + casePunching.setVerificationCaseId(verificationCaseId); + casePunching.setDocumentCaseId(documentCaseId); + return casePunching; + } + + public CasePunching addCase( + CasePunching submitted, + String branchIds, + String userId) { + CasePunching result = new CasePunching(); + result.setPortfolioId(submitted.getPortfolioId()); + result.setFormMode(1); + result.setUserId(userId); + result.setVerificationCaseId(submitted.getVerificationCaseId()); + result.setDocumentCaseId("xxx"); + + if (submitted.getPortfolioId() == null || submitted.getPortfolioId() <= 0) { + result.setOptions(Collections.emptyMap()); + result.setVisibleSections(List.of()); + result.setDynamicFields(""); + result.setDynamicHtml(""); + result.setErrorDetails(new ErrorDetails(ApplicationError.BAD_REQUEST)); + return result; + } + + String portfolioId = submitted.getPortfolioId().toString(); + PunchingHandler punchingHandler = new PunchingHandler(); + CommonFunctions commonFunctions = new CommonFunctions(); + punchingHandler.setErrCode("1001"); + punchingHandler.setPortfolioid(portfolioId); + punchingHandler.setProcessFlag(true); + punchingHandler.DynamicHtml("10"); + + if (punchingHandler.isProcessFlag()) { + result.setDynamicFields(defaultString(punchingHandler.getDynamicfields())); + result.setDynamicHtml(defaultString(punchingHandler.getDynamicpanel())); + String sections = defaultString(punchingHandler.GetVisibleSections()) + + defaultString(punchingHandler.getDynamicsection()); + result.setVisibleSections(toVisibleSections(sections)); + } else { + result.setDynamicFields(""); + result.setDynamicHtml(""); + result.setVisibleSections(List.of()); + result.setErrorDetails(new ErrorDetails(ApplicationError.INTERNAL_SERVER_ERROR)); + } + + Map options = new LinkedHashMap<>(); + punchingHandler.GetDDValues(3, branchIds.split(GlobalClass.ColDelim)); + addOptions(options, PORTFOLIO, punchingHandler.getOptionvals()); + + punchingHandler.GetDDValues(4, new String[] { portfolioId }); + addOptions(options, BRANCH, punchingHandler.getOptionvals()); + + punchingHandler.GetDDValues(5, + (branchIds + GlobalClass.ColDelim + + "op.description='CITY' or op.description='APPTYPE'" + + GlobalClass.ColDelim).split(GlobalClass.ColDelim)); + String[][] generalOptions = punchingHandler.getOptionvals(); + if (generalOptions != null) { + addOptions(options, CITY, + commonFunctions.GetSubArray("CITY", 2, generalOptions)); + addOptions(options, APPLICATION_TYPE, + commonFunctions.GetSubArray("APPTYPE", 2, generalOptions)); + } + + punchingHandler.GetDDValues(70, + (portfolioId + GlobalClass.ColDelim + + "op.description='CATEGORY' or op.description='PRODUCT'" + + GlobalClass.ColDelim).split(GlobalClass.ColDelim)); + String[][] portfolioOptions = punchingHandler.getOptionvals(); + if (portfolioOptions != null) { + addOptions(options, PRODUCT, + commonFunctions.GetSubArray("PRODUCT", 2, portfolioOptions)); + addOptions(options, CATEGORY, + commonFunctions.GetSubArray("CATEGORY", 2, portfolioOptions)); + } + result.setOptions(Collections.unmodifiableMap(options)); + return result; + } + + public CasePunching editCase( + Integer portfolioId, + String documentCaseId, + String branchIds, + String userId) { + CasePunching submitted = new CasePunching(); + submitted.setPortfolioId(portfolioId); + submitted.setDocumentCaseId(documentCaseId); + CasePunching result = addCase(submitted, branchIds, userId); + result.setFormMode(2); + result.setDocumentCaseId(documentCaseId); + return result; + } + + private void addOptions(Map options, String prefix, String[][] values) { + if (values == null) { + return; + } + for (String[] value : values) { + if (value != null && value.length >= 2 && value[0] != null) { + options.put(prefix + value[0], value[1] == null ? "" : value[1]); + } + } + } + + private List toVisibleSections(String sections) { + if (sections == null || sections.isBlank()) { + return List.of(); + } + return List.of(sections.split(Pattern.quote(GlobalClass.ColDelim))).stream() + .map(String::trim) + .filter(section -> !section.isEmpty() && !"null".equalsIgnoreCase(section)) + .distinct() + .toList(); + } + + private String defaultString(String value) { + return value == null ? "" : value; + } +} diff --git a/cygnus-onprem-app/src/test/java/matrix/nimble/controller/AbstractAuthenticatedControllerTest.java b/cygnus-onprem-app/src/test/java/matrix/nimble/controller/AbstractAuthenticatedControllerTest.java new file mode 100644 index 0000000..35a1cae --- /dev/null +++ b/cygnus-onprem-app/src/test/java/matrix/nimble/controller/AbstractAuthenticatedControllerTest.java @@ -0,0 +1,70 @@ +package matrix.nimble.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import matrix.nimble.model.Session; +import matrix.services.commons.CommonErrorService; +import matrix.services.commons.CommonService; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.ui.ModelMap; + +class AbstractAuthenticatedControllerTest { + private final CommonService commonService = new CommonService(); + private final TestController controller = new TestController( + commonService, new CommonErrorService(commonService)); + + @Test + void grantsAConfiguredMenuRoute() { + MockHttpSession httpSession = new MockHttpSession(); + Session session = sessionWithRoute("initview"); + commonService.storeSession(httpSession, session); + + AbstractAuthenticatedController.PageAuthorization result = controller.authorize( + "initview", new ModelMap(), httpSession, new MockHttpServletResponse()); + + assertTrue(result.isGranted()); + assertSame(session, result.session()); + } + + @Test + void returns401ForMissingSessionAnd403ForMissingPage() { + MockHttpServletResponse noSessionResponse = new MockHttpServletResponse(); + AbstractAuthenticatedController.PageAuthorization noSession = controller.authorize( + "initview", new ModelMap(), new MockHttpSession(), noSessionResponse); + assertEquals("error", noSession.viewName()); + assertEquals(401, noSessionResponse.getStatus()); + + MockHttpSession httpSession = new MockHttpSession(); + commonService.storeSession(httpSession, sessionWithRoute("dashboard")); + MockHttpServletResponse deniedResponse = new MockHttpServletResponse(); + AbstractAuthenticatedController.PageAuthorization denied = controller.authorize( + "initview", new ModelMap(), httpSession, deniedResponse); + assertEquals("error", denied.viewName()); + assertEquals(403, deniedResponse.getStatus()); + } + + private Session sessionWithRoute(String route) { + Session session = new Session(); + session.setMenuHtml("Page"); + return session; + } + + private static final class TestController extends AbstractAuthenticatedController { + private TestController(CommonService commonService, CommonErrorService errorService) { + super(commonService, errorService); + } + + private PageAuthorization authorize( + String route, + ModelMap model, + MockHttpSession session, + MockHttpServletResponse response) { + return authorizePage(route, model, session, response); + } + } +} diff --git a/cygnus-onprem-app/src/test/java/matrix/nimble/query/CloudQueryDefinitionProviderTest.java b/cygnus-onprem-app/src/test/java/matrix/nimble/query/CloudQueryDefinitionProviderTest.java new file mode 100644 index 0000000..f078313 --- /dev/null +++ b/cygnus-onprem-app/src/test/java/matrix/nimble/query/CloudQueryDefinitionProviderTest.java @@ -0,0 +1,28 @@ +package matrix.nimble.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.cygnus.db.QueryDefinitionException; +import com.cygnus.db.QueryType; +import org.junit.jupiter.api.Test; + +class CloudQueryDefinitionProviderTest { + @Test + void adaptsEncryptedCloudQueryProviderValueToDbDefinition() { + CloudQueryDefinitionProvider provider = new CloudQueryDefinitionProvider( + queryId -> "select!C0L!select operation_id from operation where type = ?"); + + var definition = provider.get(513); + assertEquals(513, definition.queryId()); + assertEquals(QueryType.SELECT, definition.type()); + assertEquals("select operation_id from operation where type = ?", definition.sql()); + } + + @Test + void preventsLegacyPlaceholderExecution() { + CloudQueryDefinitionProvider provider = new CloudQueryDefinitionProvider( + queryId -> "select!C0L!select * from operation where type = 'val0'"); + assertThrows(QueryDefinitionException.class, () -> provider.get(513)); + } +} diff --git a/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonErrorServiceTest.java b/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonErrorServiceTest.java new file mode 100644 index 0000000..0c2649a --- /dev/null +++ b/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonErrorServiceTest.java @@ -0,0 +1,35 @@ +package matrix.services.commons; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import lib.models.ErrorDetails; +import lib.constants.ApplicationError; +import matrix.nimble.model.Session; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.ui.ModelMap; + +class CommonErrorServiceTest { + private final CommonService commonService = new CommonService(); + private final CommonErrorService errorService = new CommonErrorService(commonService); + + @Test + void appliesHttpStatusAndPreservesAnActiveSessionForTheMenu() { + MockHttpSession httpSession = new MockHttpSession(); + Session session = new Session(); + session.setMenuHtml(""); + commonService.storeSession(httpSession, session); + ErrorDetails details = new ErrorDetails(ApplicationError.ACCESS_DENIED); + MockHttpServletResponse response = new MockHttpServletResponse(); + ModelMap model = new ModelMap(); + + String view = errorService.render(model, response, httpSession, details); + + assertEquals("error", view); + assertEquals(403, response.getStatus()); + assertSame(details, model.get(CommonErrorService.ERROR_ATTRIBUTE)); + assertSame(session, model.get(CommonService.SESSION_ATTRIBUTE)); + } +} diff --git a/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonServiceTest.java b/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonServiceTest.java new file mode 100644 index 0000000..6ceb025 --- /dev/null +++ b/cygnus-onprem-app/src/test/java/matrix/services/commons/CommonServiceTest.java @@ -0,0 +1,45 @@ +package matrix.services.commons; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import matrix.nimble.model.Session; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpSession; + +class CommonServiceTest { + private final CommonService service = new CommonService(); + + @Test + void storesAndReadsTheAuthenticatedSession() { + MockHttpSession httpSession = new MockHttpSession(); + Session session = sessionWithMenu("initview"); + + service.storeSession(httpSession, session); + + assertSame(session, service.getSession(httpSession)); + } + + @Test + void grantsOnlyAnExactMenuCommandRoute() { + Session session = sessionWithMenu("/matrix/ver/initview?source=menu"); + + assertTrue(service.hasPageAccess(session, "initview")); + assertFalse(service.hasPageAccess(session, "view")); + assertFalse(service.hasPageAccess(session, "initview-admin")); + } + + @Test + void deniesMissingSessionAndMissingMenu() { + assertFalse(service.hasPageAccess((Session) null, "initview")); + assertFalse(service.hasPageAccess(new Session(), "initview")); + } + + private Session sessionWithMenu(String route) { + Session session = new Session(); + session.setMenuHtml("Punching"); + return session; + } +} diff --git a/cygnus-onprem-db/README.md b/cygnus-onprem-db/README.md new file mode 100644 index 0000000..a730d4f --- /dev/null +++ b/cygnus-onprem-db/README.md @@ -0,0 +1,39 @@ +# Cygnus On-Premises Database Core + +This module executes cloud-supplied, parameterized query definitions against the +on-premises JDBC `DataSource` without creating delimiter strings or `String[][]` +result buffers. + +## Query format + +The query catalog value retains the operation prefix during incremental migration: + +```text +select!C0L!select id, name from operation where operation_type = ? and operated_by = ? +``` + +Legacy `val0`, `val1`, ... placeholders are rejected by the modern executor. They +remain available through the existing `DBFunctions.FetchRunQuery` path until each +query is migrated. + +## Usage + +```java +List rows = dbFunctions.query( + 600, + new Object[] { operationType, operatedBy }, + Operation.class); +``` + +For maximum throughput, use an explicit mapper: + +```java +List rows = dbFunctions.query( + 600, + parameters, + result -> new Operation(result.getLong("id"), result.getString("name"))); +``` + +Large reports can use `stream(...)` so rows are consumed without retaining the +whole result in memory. PostgreSQL cursor fetching is enabled by running reads with +auto-commit disabled and the configured fetch size. diff --git a/cygnus-onprem-db/pom.xml b/cygnus-onprem-db/pom.xml new file mode 100644 index 0000000..6056ab2 --- /dev/null +++ b/cygnus-onprem-db/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + com.cygnus + cygnus-parent + 1.0.0-SNAPSHOT + ../pom.xml + + cygnus-onprem-db + jar + Cygnus On-Premises Database Core + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + com.h2database + h2 + 2.3.232 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/CygnusDbExecutor.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/CygnusDbExecutor.java new file mode 100644 index 0000000..f3998e4 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/CygnusDbExecutor.java @@ -0,0 +1,21 @@ +package com.cygnus.db; + +import java.util.List; +import java.util.Optional; + +public interface CygnusDbExecutor { + DbResult execute(int queryId, Object[] parameters, Class responseType); + DbResult execute(int queryId, Object[] parameters); + List query(int queryId, Object[] parameters, Class responseType); + List query(int queryId, Object[] parameters, RowMapper mapper); + List query(int queryId, Object[] parameters); + Optional queryOne(int queryId, Object[] parameters, Class responseType); + int insert(int queryId, Object[] parameters); + K insert(int queryId, Object[] parameters, Class generatedKeyType); + int update(int queryId, Object[] parameters); + int delete(int queryId, Object[] parameters); + T procedure(int queryId, Object[] parameters, Class responseType); + List procedureRows(int queryId, Object[] parameters, Class responseType); + void stream(int queryId, Object[] parameters, RowMapper mapper, RowConsumer consumer); + T transaction(TransactionCallback callback); +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/DatabaseExecutionException.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/DatabaseExecutionException.java new file mode 100644 index 0000000..869ec15 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/DatabaseExecutionException.java @@ -0,0 +1,12 @@ +package com.cygnus.db; + +public final class DatabaseExecutionException extends RuntimeException { + private final int queryId; + + public DatabaseExecutionException(int queryId, String message, Throwable cause) { + super(message, cause); + this.queryId = queryId; + } + + public int queryId() { return queryId; } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/DbResult.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/DbResult.java new file mode 100644 index 0000000..f686b22 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/DbResult.java @@ -0,0 +1,20 @@ +package com.cygnus.db; + +import java.util.List; +import java.util.Optional; + +public record DbResult(QueryType type, List rows, int affectedRows, T value) { + public DbResult { + rows = rows == null ? List.of() : List.copyOf(rows); + } + + public Optional optionalValue() { return Optional.ofNullable(value); } + + public static DbResult rows(QueryType type, List rows) { + return new DbResult<>(type, rows, 0, rows.isEmpty() ? null : rows.getFirst()); + } + + public static DbResult affected(QueryType type, int count) { + return new DbResult<>(type, List.of(), count, null); + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/ExecutorOptions.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/ExecutorOptions.java new file mode 100644 index 0000000..c204234 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/ExecutorOptions.java @@ -0,0 +1,10 @@ +package com.cygnus.db; + +public record ExecutorOptions(int fetchSize, int queryTimeoutSeconds) { + public static final ExecutorOptions DEFAULT = new ExecutorOptions(250, 60); + + public ExecutorOptions { + if (fetchSize < 0) throw new IllegalArgumentException("fetchSize cannot be negative"); + if (queryTimeoutSeconds < 0) throw new IllegalArgumentException("queryTimeoutSeconds cannot be negative"); + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/ImmutableRowView.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/ImmutableRowView.java new file mode 100644 index 0000000..8e1f749 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/ImmutableRowView.java @@ -0,0 +1,33 @@ +package com.cygnus.db; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +final class ImmutableRowView implements RowView { + private final Map values; + private final Map normalized; + + ImmutableRowView(Map values) { + this.values = Collections.unmodifiableMap(new LinkedHashMap<>(values)); + Map index = new LinkedHashMap<>(); + values.forEach((name, value) -> index.put(name.toLowerCase(Locale.ROOT), value)); + this.normalized = Map.copyOf(index); + } + + @Override + public Object get(String column) { + Object value = values.get(column); + return value != null || values.containsKey(column) + ? value : normalized.get(column.toLowerCase(Locale.ROOT)); + } + + @Override + public T get(String column, Class type) { + return ValueConverter.convert(get(column), type); + } + + @Override + public Map asMap() { return values; } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/JdbcCygnusDbExecutor.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/JdbcCygnusDbExecutor.java new file mode 100644 index 0000000..774ba60 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/JdbcCygnusDbExecutor.java @@ -0,0 +1,316 @@ +package com.cygnus.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.sql.DataSource; + +public final class JdbcCygnusDbExecutor implements CygnusDbExecutor { + private final DataSource dataSource; + private final QueryDefinitionProvider queryProvider; + private final ExecutorOptions options; + private final ParameterBinder binder = new ParameterBinder(); + private final ObjectRowMapperFactory mapperFactory = new ObjectRowMapperFactory(); + + public JdbcCygnusDbExecutor(DataSource dataSource, QueryDefinitionProvider queryProvider) { + this(dataSource, queryProvider, ExecutorOptions.DEFAULT); + } + + public JdbcCygnusDbExecutor( + DataSource dataSource, QueryDefinitionProvider queryProvider, ExecutorOptions options) { + this.dataSource = java.util.Objects.requireNonNull(dataSource, "dataSource"); + this.queryProvider = java.util.Objects.requireNonNull(queryProvider, "queryProvider"); + this.options = java.util.Objects.requireNonNull(options, "options"); + } + + @Override + public DbResult execute(int queryId, Object[] parameters, Class responseType) { + QueryDefinition definition = queryProvider.get(queryId); + if (definition.type() == QueryType.SELECT) { + return withReadConnection(queryId, connection -> + executeRowsOrCount(connection, definition, parameters, responseType)); + } + if (definition.type() == QueryType.PROCEDURE) { + return executeRowsOrCount(definition, parameters, responseType); + } + return DbResult.affected(definition.type(), executeUpdate(definition, parameters)); + } + + @Override + public DbResult execute(int queryId, Object[] parameters) { + return execute(queryId, parameters, RowView.class); + } + + @Override + public List query(int queryId, Object[] parameters, Class responseType) { + return withReadConnection(queryId, connection -> query( + connection, require(queryId, QueryType.SELECT), parameters, responseType)); + } + + @Override + public List query(int queryId, Object[] parameters, RowMapper mapper) { + return withReadConnection(queryId, connection -> query( + connection, require(queryId, QueryType.SELECT), parameters, mapper)); + } + + @Override + public List query(int queryId, Object[] parameters) { + return query(queryId, parameters, RowView.class); + } + + @Override + public Optional queryOne(int queryId, Object[] parameters, Class responseType) { + List rows = query(queryId, parameters, responseType); + if (rows.size() > 1) throw new DatabaseExecutionException( + queryId, "Expected one row but received " + rows.size(), null); + return rows.stream().findFirst(); + } + + @Override + public int insert(int queryId, Object[] parameters) { + return executeUpdate(require(queryId, QueryType.INSERT), parameters); + } + + @Override + public K insert(int queryId, Object[] parameters, Class generatedKeyType) { + QueryDefinition definition = require(queryId, QueryType.INSERT); + return withConnection(queryId, connection -> { + try (PreparedStatement statement = prepare( + connection, definition, Statement.RETURN_GENERATED_KEYS)) { + binder.bind(statement, parameters); + statement.executeUpdate(); + try (ResultSet keys = statement.getGeneratedKeys()) { + if (!keys.next()) throw new DatabaseExecutionException( + queryId, "Insert did not return a generated key", null); + return ValueConverter.convert(keys.getObject(1), generatedKeyType); + } + } + }); + } + + @Override + public int update(int queryId, Object[] parameters) { + return executeUpdate(require(queryId, QueryType.UPDATE), parameters); + } + + @Override + public int delete(int queryId, Object[] parameters) { + return executeUpdate(require(queryId, QueryType.DELETE), parameters); + } + + @Override + public T procedure(int queryId, Object[] parameters, Class responseType) { + List rows = procedureRows(queryId, parameters, responseType); + return rows.isEmpty() ? null : rows.getFirst(); + } + + @Override + public List procedureRows(int queryId, Object[] parameters, Class responseType) { + DbResult result = executeRowsOrCount( + require(queryId, QueryType.PROCEDURE), parameters, responseType); + return result.rows(); + } + + @Override + public void stream( + int queryId, Object[] parameters, RowMapper mapper, RowConsumer consumer) { + QueryDefinition definition = require(queryId, QueryType.SELECT); + withReadConnection(queryId, connection -> { + try (PreparedStatement statement = prepare(connection, definition); + ResultSet result = executeQuery(statement, parameters)) { + while (result.next()) consumer.accept(mapper.map(result)); + return null; + } + }); + } + + @Override + public T transaction(TransactionCallback callback) { + try (Connection connection = dataSource.getConnection()) { + boolean autoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + try { + T result = callback.execute(new TransactionOperationsImpl(connection)); + connection.commit(); + return result; + } catch (Exception exception) { + try { connection.rollback(); } catch (SQLException rollback) { exception.addSuppressed(rollback); } + if (exception instanceof RuntimeException runtime) throw runtime; + throw new DatabaseExecutionException(0, "Transaction failed", exception); + } finally { + connection.setAutoCommit(autoCommit); + } + } catch (SQLException exception) { + throw new DatabaseExecutionException(0, "Unable to manage transaction", exception); + } + } + + private DbResult executeRowsOrCount( + QueryDefinition definition, Object[] parameters, Class responseType) { + return withConnection(definition.queryId(), connection -> + executeRowsOrCount(connection, definition, parameters, responseType)); + } + + private DbResult executeRowsOrCount( + Connection connection, + QueryDefinition definition, + Object[] parameters, + Class responseType) throws SQLException { + try (PreparedStatement statement = prepare(connection, definition)) { + binder.bind(statement, parameters); + boolean rows = statement.execute(); + if (!rows) return DbResult.affected(definition.type(), statement.getUpdateCount()); + try (ResultSet result = statement.getResultSet()) { + return DbResult.rows(definition.type(), map(result, responseType)); + } + } + } + + private int executeUpdate(QueryDefinition definition, Object[] parameters) { + return withConnection(definition.queryId(), connection -> + executeUpdate(connection, definition, parameters)); + } + + private int executeUpdate( + Connection connection, QueryDefinition definition, Object[] parameters) throws SQLException { + try (PreparedStatement statement = prepare(connection, definition)) { + binder.bind(statement, parameters); + return statement.executeUpdate(); + } + } + + private List query( + Connection connection, + QueryDefinition definition, + Object[] parameters, + Class responseType) throws SQLException { + try (PreparedStatement statement = prepare(connection, definition); + ResultSet result = executeQuery(statement, parameters)) { + return map(result, responseType); + } + } + + private List query( + Connection connection, + QueryDefinition definition, + Object[] parameters, + RowMapper mapper) throws SQLException { + try (PreparedStatement statement = prepare(connection, definition); + ResultSet result = executeQuery(statement, parameters)) { + List rows = new ArrayList<>(); + while (result.next()) rows.add(mapper.map(result)); + return List.copyOf(rows); + } + } + + private List map(ResultSet result, Class responseType) throws SQLException { + RowMapper mapper = mapperFactory.create(responseType, result.getMetaData()); + List rows = new ArrayList<>(); + while (result.next()) rows.add(mapper.map(result)); + return List.copyOf(rows); + } + + private ResultSet executeQuery(PreparedStatement statement, Object[] parameters) throws SQLException { + binder.bind(statement, parameters); + return statement.executeQuery(); + } + + private PreparedStatement prepare(Connection connection, QueryDefinition definition) throws SQLException { + PreparedStatement statement = connection.prepareStatement( + definition.sql(), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + configure(statement); + return statement; + } + + private PreparedStatement prepare( + Connection connection, QueryDefinition definition, int generatedKeys) throws SQLException { + PreparedStatement statement = connection.prepareStatement(definition.sql(), generatedKeys); + configure(statement); + return statement; + } + + private void configure(PreparedStatement statement) throws SQLException { + statement.setFetchSize(options.fetchSize()); + statement.setQueryTimeout(options.queryTimeoutSeconds()); + } + + private QueryDefinition require(int queryId, QueryType expected) { + QueryDefinition definition = queryProvider.get(queryId); + if (definition.type() != expected) throw new QueryDefinitionException( + "Query " + queryId + " is " + definition.type() + ", not " + expected); + return definition; + } + + private T withConnection(int queryId, SqlWork work) { + try (Connection connection = dataSource.getConnection()) { + return work.execute(connection); + } catch (DatabaseExecutionException exception) { + throw exception; + } catch (Exception exception) { + throw new DatabaseExecutionException(queryId, "Database operation failed for query " + queryId, exception); + } + } + + private T withReadConnection(int queryId, SqlWork work) { + try (Connection connection = dataSource.getConnection()) { + boolean autoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); // PostgreSQL requires this for cursor-based fetchSize. + try { + T value = work.execute(connection); + connection.commit(); + return value; + } catch (Exception exception) { + try { connection.rollback(); } catch (SQLException rollback) { exception.addSuppressed(rollback); } + if (exception instanceof RuntimeException runtime) throw runtime; + throw new DatabaseExecutionException(queryId, + "Database read failed for query " + queryId, exception); + } finally { + connection.setAutoCommit(autoCommit); + } + } catch (SQLException exception) { + throw new DatabaseExecutionException(queryId, + "Unable to manage database read for query " + queryId, exception); + } + } + + @FunctionalInterface + private interface SqlWork { T execute(Connection connection) throws Exception; } + + private final class TransactionOperationsImpl implements TransactionOperations { + private final Connection connection; + private TransactionOperationsImpl(Connection connection) { this.connection = connection; } + + @Override + public List query(int queryId, Object[] parameters, Class type) { + try { return JdbcCygnusDbExecutor.this.query( + connection, require(queryId, QueryType.SELECT), parameters, type); } + catch (SQLException exception) { throw failure(queryId, exception); } + } + + @Override + public Optional queryOne(int queryId, Object[] parameters, Class type) { + List rows = query(queryId, parameters, type); + if (rows.size() > 1) throw new DatabaseExecutionException( + queryId, "Expected one row but received " + rows.size(), null); + return rows.stream().findFirst(); + } + + @Override public int insert(int id, Object[] values) { return updateType(id, values, QueryType.INSERT); } + @Override public int update(int id, Object[] values) { return updateType(id, values, QueryType.UPDATE); } + @Override public int delete(int id, Object[] values) { return updateType(id, values, QueryType.DELETE); } + + private int updateType(int id, Object[] values, QueryType type) { + try { return executeUpdate(connection, require(id, type), values); } + catch (SQLException exception) { throw failure(id, exception); } + } + + private DatabaseExecutionException failure(int id, SQLException exception) { + return new DatabaseExecutionException(id, "Transaction operation failed for query " + id, exception); + } + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/ObjectRowMapperFactory.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/ObjectRowMapperFactory.java new file mode 100644 index 0000000..0a8aa30 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/ObjectRowMapperFactory.java @@ -0,0 +1,141 @@ +package com.cygnus.db; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.RecordComponent; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class ObjectRowMapperFactory { + private final ConcurrentMap, TypeMetadata> metadataCache = new ConcurrentHashMap<>(); + + RowMapper create(Class type, ResultSetMetaData resultMetadata) throws SQLException { + if (type == RowView.class) return result -> type.cast(rowView(result, resultMetadata)); + if (isScalar(type)) return result -> ValueConverter.convert(result.getObject(1), type); + TypeMetadata typeMetadata = metadataCache.computeIfAbsent(type, this::inspect); + Map columns = columns(resultMetadata); + return typeMetadata.mapper(type, columns); + } + + private RowView rowView(java.sql.ResultSet result, ResultSetMetaData metadata) throws SQLException { + Map values = new LinkedHashMap<>(); + for (int column = 1; column <= metadata.getColumnCount(); column++) { + values.put(metadata.getColumnLabel(column), result.getObject(column)); + } + return new ImmutableRowView(values); + } + + private TypeMetadata inspect(Class type) { + try { + if (type.isRecord()) { + RecordComponent[] components = type.getRecordComponents(); + Class[] parameterTypes = new Class[components.length]; + String[] names = new String[components.length]; + for (int index = 0; index < components.length; index++) { + parameterTypes[index] = components[index].getType(); + names[index] = normalize(components[index].getName()); + } + Constructor constructor = type.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + return new RecordMetadata(constructor, parameterTypes, names); + } + Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + List fields = new ArrayList<>(); + for (Class current = type; current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + fields.add(field); + } + } + } + return new BeanMetadata(constructor, List.copyOf(fields)); + } catch (ReflectiveOperationException exception) { + throw new IllegalArgumentException( + "Response type must be a record or have a no-argument constructor: " + type.getName(), exception); + } + } + + private Map columns(ResultSetMetaData metadata) throws SQLException { + Map result = new LinkedHashMap<>(); + for (int column = 1; column <= metadata.getColumnCount(); column++) { + result.putIfAbsent(normalize(metadata.getColumnLabel(column)), column); + } + return result; + } + + private static String normalize(String value) { + return value.replace("_", "").toLowerCase(Locale.ROOT); + } + + private static boolean isScalar(Class type) { + return type.isPrimitive() || type.isEnum() + || Number.class.isAssignableFrom(type) + || type == String.class || type == Boolean.class || type == Character.class + || type == java.util.UUID.class + || type.getPackageName().equals("java.time"); + } + + private sealed interface TypeMetadata permits RecordMetadata, BeanMetadata { + RowMapper mapper(Class type, Map columns); + } + + private record RecordMetadata( + Constructor constructor, Class[] parameterTypes, String[] names) implements TypeMetadata { + @Override + public RowMapper mapper(Class type, Map columns) { + int[] indexes = new int[names.length]; + for (int index = 0; index < names.length; index++) { + Integer column = columns.get(names[index]); + if (column == null) throw new IllegalArgumentException( + "Result does not contain record component: " + names[index]); + indexes[index] = column; + } + return result -> { + try { + Object[] values = new Object[indexes.length]; + for (int index = 0; index < indexes.length; index++) { + values[index] = ValueConverter.convert( + result.getObject(indexes[index]), parameterTypes[index]); + } + return type.cast(constructor.newInstance(values)); + } catch (ReflectiveOperationException exception) { + throw new DatabaseExecutionException(0, "Unable to map record " + type.getName(), exception); + } + }; + } + } + + private record BeanMetadata(Constructor constructor, List fields) implements TypeMetadata { + @Override + public RowMapper mapper(Class type, Map columns) { + List bindings = fields.stream() + .map(field -> new FieldBinding(field, columns.get(normalize(field.getName())))) + .filter(binding -> binding.column() != null) + .toList(); + return result -> { + try { + T instance = type.cast(constructor.newInstance()); + for (FieldBinding binding : bindings) { + binding.field().set(instance, ValueConverter.convert( + result.getObject(binding.column()), binding.field().getType())); + } + return instance; + } catch (ReflectiveOperationException exception) { + throw new DatabaseExecutionException(0, "Unable to map bean " + type.getName(), exception); + } + }; + } + } + + private record FieldBinding(Field field, Integer column) {} +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/ParameterBinder.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/ParameterBinder.java new file mode 100644 index 0000000..ab9fd53 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/ParameterBinder.java @@ -0,0 +1,42 @@ +package com.cygnus.db; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.UUID; + +final class ParameterBinder { + void bind(PreparedStatement statement, Object[] parameters) throws SQLException { + Object[] values = parameters == null ? new Object[0] : parameters; + for (int index = 0; index < values.length; index++) { + bind(statement, index + 1, values[index]); + } + } + + private void bind(PreparedStatement statement, int index, Object value) throws SQLException { + if (value instanceof SqlParameter parameter) { + if (parameter.value() == null) statement.setNull(index, parameter.sqlType()); + else statement.setObject(index, parameter.value(), parameter.sqlType()); + } else if (value == null) { + statement.setObject(index, null); + } else if (value instanceof Instant instant) { + statement.setTimestamp(index, Timestamp.from(instant)); + } else if (value instanceof LocalDate date) { + statement.setObject(index, date); + } else if (value instanceof LocalDateTime dateTime) { + statement.setObject(index, dateTime); + } else if (value instanceof OffsetDateTime dateTime) { + statement.setObject(index, dateTime); + } else if (value instanceof UUID uuid) { + statement.setObject(index, uuid); + } else if (value instanceof Enum enumValue) { + statement.setString(index, enumValue.name()); + } else { + statement.setObject(index, value); + } + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinition.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinition.java new file mode 100644 index 0000000..0841193 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinition.java @@ -0,0 +1,29 @@ +package com.cygnus.db; + +import java.util.Objects; +import java.util.regex.Pattern; + +public record QueryDefinition(int queryId, QueryType type, String sql) { + private static final String DELIMITER = "!C0L!"; + private static final Pattern LEGACY_PARAMETER = Pattern.compile("\\bval\\d+\\b"); + + public QueryDefinition { + if (queryId <= 0) throw new QueryDefinitionException("Query ID must be positive"); + Objects.requireNonNull(type, "type"); + if (sql == null || sql.isBlank()) throw new QueryDefinitionException("Query SQL is empty: " + queryId); + if (LEGACY_PARAMETER.matcher(sql).find()) { + throw new QueryDefinitionException( + "Query " + queryId + " has not been migrated to parameterized SQL"); + } + } + + public static QueryDefinition parse(int queryId, String storedValue) { + if (storedValue == null) throw new QueryDefinitionException("Query was not found: " + queryId); + int delimiter = storedValue.indexOf(DELIMITER); + if (delimiter <= 0) throw new QueryDefinitionException("Invalid query definition: " + queryId); + return new QueryDefinition( + queryId, + QueryType.parse(storedValue.substring(0, delimiter)), + storedValue.substring(delimiter + DELIMITER.length()).trim()); + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionException.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionException.java new file mode 100644 index 0000000..7ff1235 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionException.java @@ -0,0 +1,6 @@ +package com.cygnus.db; + +public final class QueryDefinitionException extends RuntimeException { + public QueryDefinitionException(String message) { super(message); } + public QueryDefinitionException(String message, Throwable cause) { super(message, cause); } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionProvider.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionProvider.java new file mode 100644 index 0000000..e8b137f --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionProvider.java @@ -0,0 +1,6 @@ +package com.cygnus.db; + +@FunctionalInterface +public interface QueryDefinitionProvider { + QueryDefinition get(int queryId); +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryType.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryType.java new file mode 100644 index 0000000..6e81ad7 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryType.java @@ -0,0 +1,15 @@ +package com.cygnus.db; + +import java.util.Locale; + +public enum QueryType { + SELECT, INSERT, UPDATE, DELETE, PROCEDURE; + + public static QueryType parse(String value) { + try { + return valueOf(value.trim().toUpperCase(Locale.ROOT)); + } catch (RuntimeException exception) { + throw new QueryDefinitionException("Unsupported query type: " + value, exception); + } + } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/RowConsumer.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowConsumer.java new file mode 100644 index 0000000..f504271 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowConsumer.java @@ -0,0 +1,6 @@ +package com.cygnus.db; + +@FunctionalInterface +public interface RowConsumer { + void accept(T row) throws Exception; +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/RowMapper.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowMapper.java new file mode 100644 index 0000000..45b2aa2 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowMapper.java @@ -0,0 +1,9 @@ +package com.cygnus.db; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + T map(ResultSet resultSet) throws SQLException; +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/RowView.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowView.java new file mode 100644 index 0000000..93ca9fd --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/RowView.java @@ -0,0 +1,9 @@ +package com.cygnus.db; + +import java.util.Map; + +public interface RowView { + Object get(String column); + T get(String column, Class type); + Map asMap(); +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/SqlParameter.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/SqlParameter.java new file mode 100644 index 0000000..284472b --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/SqlParameter.java @@ -0,0 +1,6 @@ +package com.cygnus.db; + +public record SqlParameter(int sqlType, Object value) { + public static SqlParameter of(int sqlType, Object value) { return new SqlParameter(sqlType, value); } + public static SqlParameter nullValue(int sqlType) { return new SqlParameter(sqlType, null); } +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionCallback.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionCallback.java new file mode 100644 index 0000000..3518b7b --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionCallback.java @@ -0,0 +1,6 @@ +package com.cygnus.db; + +@FunctionalInterface +public interface TransactionCallback { + T execute(TransactionOperations operations) throws Exception; +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionOperations.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionOperations.java new file mode 100644 index 0000000..64a1590 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionOperations.java @@ -0,0 +1,12 @@ +package com.cygnus.db; + +import java.util.List; +import java.util.Optional; + +public interface TransactionOperations { + List query(int queryId, Object[] parameters, Class responseType); + Optional queryOne(int queryId, Object[] parameters, Class responseType); + int insert(int queryId, Object[] parameters); + int update(int queryId, Object[] parameters); + int delete(int queryId, Object[] parameters); +} diff --git a/cygnus-onprem-db/src/main/java/com/cygnus/db/ValueConverter.java b/cygnus-onprem-db/src/main/java/com/cygnus/db/ValueConverter.java new file mode 100644 index 0000000..581a2d2 --- /dev/null +++ b/cygnus-onprem-db/src/main/java/com/cygnus/db/ValueConverter.java @@ -0,0 +1,79 @@ +package com.cygnus.db; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.UUID; + +final class ValueConverter { + private ValueConverter() {} + + @SuppressWarnings({"unchecked", "rawtypes"}) + static T convert(Object value, Class target) { + if (value == null) return target.isPrimitive() ? (T) primitiveDefault(target) : null; + Class boxed = box(target); + if (boxed.isInstance(value)) return (T) value; + if (boxed == String.class) return (T) value.toString(); + if (Number.class.isAssignableFrom(boxed) && value instanceof Number number) { + return (T) number(number, boxed); + } + if (boxed == Boolean.class) { + if (value instanceof Number number) return (T) Boolean.valueOf(number.intValue() != 0); + return (T) Boolean.valueOf(value.toString()); + } + if (boxed == Character.class) { + String text = value.toString(); + if (text.isEmpty()) throw new IllegalArgumentException("Cannot convert an empty value to char"); + return (T) Character.valueOf(text.charAt(0)); + } + if (boxed == UUID.class) return (T) UUID.fromString(value.toString()); + if (boxed == LocalDate.class && value instanceof Date date) return (T) date.toLocalDate(); + if (boxed == LocalDateTime.class && value instanceof Timestamp timestamp) return (T) timestamp.toLocalDateTime(); + if (boxed == Instant.class && value instanceof Timestamp timestamp) return (T) timestamp.toInstant(); + if (boxed == OffsetDateTime.class && value instanceof OffsetDateTime dateTime) return (T) dateTime; + if (boxed.isEnum()) return (T) Enum.valueOf((Class) boxed, value.toString()); + throw new DatabaseExecutionException(0, + "Cannot convert " + value.getClass().getName() + " to " + target.getName(), null); + } + + private static Object number(Number value, Class target) { + if (target == Integer.class) return value.intValue(); + if (target == Long.class) return value.longValue(); + if (target == Double.class) return value.doubleValue(); + if (target == Float.class) return value.floatValue(); + if (target == Short.class) return value.shortValue(); + if (target == Byte.class) return value.byteValue(); + if (target == BigDecimal.class) return value instanceof BigDecimal decimal + ? decimal : new BigDecimal(value.toString()); + return value; + } + + private static Class box(Class type) { + if (!type.isPrimitive()) return type; + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == double.class) return Double.class; + if (type == float.class) return Float.class; + if (type == short.class) return Short.class; + if (type == byte.class) return Byte.class; + if (type == boolean.class) return Boolean.class; + if (type == char.class) return Character.class; + return type; + } + + private static Object primitiveDefault(Class type) { + if (type == boolean.class) return false; + if (type == char.class) return '\0'; + if (type == byte.class) return (byte) 0; + if (type == short.class) return (short) 0; + if (type == int.class) return 0; + if (type == long.class) return 0L; + if (type == float.class) return 0F; + if (type == double.class) return 0D; + return null; + } +} diff --git a/cygnus-onprem-db/src/test/java/com/cygnus/db/JdbcCygnusDbExecutorTest.java b/cygnus-onprem-db/src/test/java/com/cygnus/db/JdbcCygnusDbExecutorTest.java new file mode 100644 index 0000000..fa651b6 --- /dev/null +++ b/cygnus-onprem-db/src/test/java/com/cygnus/db/JdbcCygnusDbExecutorTest.java @@ -0,0 +1,86 @@ +package com.cygnus.db; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JdbcCygnusDbExecutorTest { + private JdbcCygnusDbExecutor executor; + + @BeforeEach + void setUp() throws Exception { + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:cygnus;DB_CLOSE_DELAY=-1"); + try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS person"); + statement.execute("CREATE TABLE person (id bigint generated by default as identity primary key, name varchar(100), active boolean)"); + statement.execute("INSERT INTO person(name, active) VALUES ('Asha', true), ('Ravi', false)"); + } + Map queries = Map.of( + 1, new QueryDefinition(1, QueryType.SELECT, + "select id, name from person where active = ? order by id"), + 2, new QueryDefinition(2, QueryType.INSERT, + "insert into person(name, active) values (?, ?)"), + 3, new QueryDefinition(3, QueryType.UPDATE, + "update person set active = ? where name = ?"), + 4, new QueryDefinition(4, QueryType.DELETE, + "delete from person where name = ?"), + 5, new QueryDefinition(5, QueryType.PROCEDURE, "call abs(?)")); + executor = new JdbcCygnusDbExecutor(dataSource, queries::get, new ExecutorOptions(10, 5)); + } + + @Test + void mapsSelectDirectlyToRecordsAndDynamicRows() { + var people = executor.query(1, new Object[]{true}, Person.class); + assertEquals(java.util.List.of(new Person(1L, "Asha")), people); + + RowView row = executor.query(1, new Object[]{true}).getFirst(); + assertEquals("Asha", row.get("NAME", String.class)); + assertEquals(1L, row.get("id", Long.class)); + } + + @Test + void executesInsertUpdateDeleteAndGeneratedKeys() { + Long id = executor.insert(2, new Object[]{"Mira", true}, Long.class); + assertTrue(id > 0); + assertEquals(1, executor.update(3, new Object[]{false, "Mira"})); + assertEquals(1, executor.delete(4, new Object[]{"Mira"})); + } + + @Test + void executesProcedureAndStreamsWithoutMaterializingRows() { + assertEquals(7, executor.procedure(5, new Object[]{-7}, Integer.class)); + AtomicInteger count = new AtomicInteger(); + executor.stream(1, new Object[]{true}, + result -> new Person(result.getLong("id"), result.getString("name")), + row -> count.incrementAndGet()); + assertEquals(1, count.get()); + } + + @Test + void rollsBackFailedTransactions() { + assertThrows(IllegalStateException.class, () -> executor.transaction(tx -> { + tx.insert(2, new Object[]{"Rollback", true}); + throw new IllegalStateException("stop"); + })); + assertFalse(executor.query(1, new Object[]{true}, Person.class).stream() + .anyMatch(person -> person.name().equals("Rollback"))); + } + + @Test + void rejectsLegacyStringReplacementQueries() { + assertThrows(QueryDefinitionException.class, () -> + QueryDefinition.parse(513, "select!C0L!select * from person where id=val0")); + } + + record Person(long id, String name) {} +} diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/CygnusDbExecutor.class b/cygnus-onprem-db/target/classes/com/cygnus/db/CygnusDbExecutor.class new file mode 100644 index 0000000..62e9476 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/CygnusDbExecutor.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/DatabaseExecutionException.class b/cygnus-onprem-db/target/classes/com/cygnus/db/DatabaseExecutionException.class new file mode 100644 index 0000000..feb6663 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/DatabaseExecutionException.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/DbResult.class b/cygnus-onprem-db/target/classes/com/cygnus/db/DbResult.class new file mode 100644 index 0000000..c4bcd95 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/DbResult.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ExecutorOptions.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ExecutorOptions.class new file mode 100644 index 0000000..e0833fe Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ExecutorOptions.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ImmutableRowView.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ImmutableRowView.class new file mode 100644 index 0000000..188facc Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ImmutableRowView.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$SqlWork.class b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$SqlWork.class new file mode 100644 index 0000000..aa5e0f9 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$SqlWork.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$TransactionOperationsImpl.class b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$TransactionOperationsImpl.class new file mode 100644 index 0000000..d0cb4c1 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor$TransactionOperationsImpl.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor.class b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor.class new file mode 100644 index 0000000..3db5089 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/JdbcCygnusDbExecutor.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$BeanMetadata.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$BeanMetadata.class new file mode 100644 index 0000000..ba4dbcb Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$BeanMetadata.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$FieldBinding.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$FieldBinding.class new file mode 100644 index 0000000..0fa7c6d Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$FieldBinding.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$RecordMetadata.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$RecordMetadata.class new file mode 100644 index 0000000..8958a9b Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$RecordMetadata.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$TypeMetadata.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$TypeMetadata.class new file mode 100644 index 0000000..dc82ade Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory$TypeMetadata.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory.class new file mode 100644 index 0000000..b18dfbf Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ObjectRowMapperFactory.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ParameterBinder.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ParameterBinder.class new file mode 100644 index 0000000..282b2ec Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ParameterBinder.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinition.class b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinition.class new file mode 100644 index 0000000..6afbe78 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinition.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionException.class b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionException.class new file mode 100644 index 0000000..6f9111d Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionException.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionProvider.class b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionProvider.class new file mode 100644 index 0000000..188443c Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryDefinitionProvider.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/QueryType.class b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryType.class new file mode 100644 index 0000000..546bfc2 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/QueryType.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/RowConsumer.class b/cygnus-onprem-db/target/classes/com/cygnus/db/RowConsumer.class new file mode 100644 index 0000000..874565e Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/RowConsumer.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/RowMapper.class b/cygnus-onprem-db/target/classes/com/cygnus/db/RowMapper.class new file mode 100644 index 0000000..6bcc114 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/RowMapper.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/RowView.class b/cygnus-onprem-db/target/classes/com/cygnus/db/RowView.class new file mode 100644 index 0000000..efce593 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/RowView.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/SqlParameter.class b/cygnus-onprem-db/target/classes/com/cygnus/db/SqlParameter.class new file mode 100644 index 0000000..8babb28 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/SqlParameter.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionCallback.class b/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionCallback.class new file mode 100644 index 0000000..bbf88c4 Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionCallback.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionOperations.class b/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionOperations.class new file mode 100644 index 0000000..83bbc0b Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/TransactionOperations.class differ diff --git a/cygnus-onprem-db/target/classes/com/cygnus/db/ValueConverter.class b/cygnus-onprem-db/target/classes/com/cygnus/db/ValueConverter.class new file mode 100644 index 0000000..a961e6b Binary files /dev/null and b/cygnus-onprem-db/target/classes/com/cygnus/db/ValueConverter.class differ diff --git a/cygnus-onprem-db/target/cygnus-onprem-db-1.0.0-SNAPSHOT.jar b/cygnus-onprem-db/target/cygnus-onprem-db-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..e33c7ea Binary files /dev/null and b/cygnus-onprem-db/target/cygnus-onprem-db-1.0.0-SNAPSHOT.jar differ diff --git a/cygnus-onprem-db/target/maven-archiver/pom.properties b/cygnus-onprem-db/target/maven-archiver/pom.properties new file mode 100644 index 0000000..c08b578 --- /dev/null +++ b/cygnus-onprem-db/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=cygnus-onprem-db +groupId=com.cygnus +version=1.0.0-SNAPSHOT diff --git a/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..dc391ef --- /dev/null +++ b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,25 @@ +com/cygnus/db/RowConsumer.class +com/cygnus/db/RowView.class +com/cygnus/db/QueryDefinitionException.class +com/cygnus/db/QueryDefinitionProvider.class +com/cygnus/db/CygnusDbExecutor.class +com/cygnus/db/SqlParameter.class +com/cygnus/db/ObjectRowMapperFactory$RecordMetadata.class +com/cygnus/db/JdbcCygnusDbExecutor.class +com/cygnus/db/JdbcCygnusDbExecutor$SqlWork.class +com/cygnus/db/QueryDefinition.class +com/cygnus/db/QueryType.class +com/cygnus/db/ObjectRowMapperFactory$TypeMetadata.class +com/cygnus/db/TransactionCallback.class +com/cygnus/db/RowMapper.class +com/cygnus/db/ObjectRowMapperFactory.class +com/cygnus/db/DatabaseExecutionException.class +com/cygnus/db/ObjectRowMapperFactory$FieldBinding.class +com/cygnus/db/TransactionOperations.class +com/cygnus/db/ValueConverter.class +com/cygnus/db/ExecutorOptions.class +com/cygnus/db/DbResult.class +com/cygnus/db/ParameterBinder.class +com/cygnus/db/ObjectRowMapperFactory$BeanMetadata.class +com/cygnus/db/JdbcCygnusDbExecutor$TransactionOperationsImpl.class +com/cygnus/db/ImmutableRowView.class diff --git a/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..aefbb7e --- /dev/null +++ b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,19 @@ +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/CygnusDbExecutor.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/DatabaseExecutionException.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/DbResult.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ExecutorOptions.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ImmutableRowView.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/JdbcCygnusDbExecutor.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ObjectRowMapperFactory.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ParameterBinder.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinition.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionException.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionProvider.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryType.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowConsumer.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowMapper.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowView.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/SqlParameter.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionCallback.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionOperations.java +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ValueConverter.java diff --git a/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..9712c41 --- /dev/null +++ b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -0,0 +1,2 @@ +com/cygnus/db/JdbcCygnusDbExecutorTest.class +com/cygnus/db/JdbcCygnusDbExecutorTest$Person.class diff --git a/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..316832b --- /dev/null +++ b/cygnus-onprem-db/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -0,0 +1 @@ +/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/test/java/com/cygnus/db/JdbcCygnusDbExecutorTest.java diff --git a/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest$Person.class b/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest$Person.class new file mode 100644 index 0000000..db7baa8 Binary files /dev/null and b/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest$Person.class differ diff --git a/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest.class b/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest.class new file mode 100644 index 0000000..a44de3a Binary files /dev/null and b/cygnus-onprem-db/target/test-classes/com/cygnus/db/JdbcCygnusDbExecutorTest.class differ diff --git a/index.sql b/index.sql new file mode 100644 index 0000000..f98b065 --- /dev/null +++ b/index.sql @@ -0,0 +1,2 @@ +CREATE INDEX idx_portfolio_branch_active_name +ON portfolio (branch_id, isactive, portname); \ No newline at end of file diff --git a/latest_queries.qry b/latest_queries.qry index 0fcfd30..1daf6b1 100644 --- a/latest_queries.qry +++ b/latest_queries.qry @@ -491,7 +491,7 @@ Query509!C0L!procedure!C0L!select changeuserstatus(val0,val1) Query510!C0L!select!C0L!select u.user_id,u.loginid,u.displayname,u.group_id,u.emailid from app_user u where u.user_id=val0 Query511!C0L!select!C0L!select group_id,name from user_group where isactive = 1 and group_id in (select c.child_id from user_group_childs c where c.group_id = val0) Query512!C0L!procedure!C0L!select updateuser(val0,'val1','val2',val3,'val4',val5,val6,val7,val8) -Query513!C0L!procedure!C0L!select startmaintenance('val0',val1,'val2') +Query513!C0L!procedure!C0L!select startmaintenance(?,?,?) Query514!C0L!select!C0L!select m.uuid,mc.portname,mc.mvcode,mc.applno,mc.customername,mc.address,mc.visit,mc.uid from mnimble_cases mc,main m where mc.submittedon is not null and mc.case_id = m.case_id and (lower(mc.mvcode) = lower('val0') or lower(mc.applno) = lower('val0')) Query515!C0L!procedure!C0L!select pendingmnimblecase(val0,'val1',val2) Query516!C0L!select!C0L!select uid,emailid from emailids @@ -515,4 +515,4 @@ Query533!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(rec Query534!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(receivedate,'DD/MM/YYYY') as recdate,applno,upper(customername) cname,bank_branch_id,upper(name) as pname1,'RESIDENCE VERIFICATION REPORT' as label1,'Agency' as label2,upper(maincompany) as agencyname, 'Received Date' as label4,to_char(receivedate,'DD/MM/YYYY') as recdate,'Application No' as label5,upper(applno) as applno2,'Product' as label6,upper(product) as product1,'Landmark' as label7,upper(landmark) as loanamount,'Visit Date' as label8,visitdate,'Visit Time' as label9,visittime,'Report Date' as label10,to_char(sdoneon,'DD/MM/YYYY') as sdoneon,'Applicant''s Name' as label11,upper(customername) as custname1,'Address' as label13,upper(raddr) as raddr,'Met Person' as label14,upper(metperson) as metperson,'Relation' as label15,(case when upper(metrela)='OTHERS' then upper(metrealoth) else upper(metrela) end) as metrela,'Ownership' as label16,(case when upper(rowner)='OTHERS' then upper(owneroth) else upper(rowner) end) as rowner,'Stay at Curr Address' as label17,coalesce(((case when (yrsadd<>'0' and yrsadd<>'') then yrsadd||' YEARS(S) ' else '' end)||(case when (mnthadd <> '0' and mnthadd <> '') then mnthadd||' MONTH(S)' else '' end)),'NA') as rstay,'Marital Status' as label18,upper(maritial) as marital,'Total Family Members' as label19,totmem,'Total Earning Members' as label20,earning,'Spouse Working' as label21,upper(spswork) as spswork,'Spouse Employment Details' as label22,upper(spsempdet) as spsempdet,'Residence Proof' as label23,upper(proofdet) as resproof,'Ease of Location' as label24,upper(raccess) as raccess,'Locality' as label25,(case when upper(locality)='OTHERS' then upper(locaoth) else upper(locality) end) as locality,'Residence Type' as lblrtype,(case when upper(restype)='OTHERS' then upper(typeoth) else upper(restype) end) as restype,'Name Plate' as label26,(case when upper(nameplate)='YES' then upper(platedet) else upper(nameplate) end) nameplate,'Neighbour Reference' as label27,(case when upper(field30_9)='-1' then 'NA' else upper(field30_9) end) as neref,'Neighbour Feedback' as label28,upper(fieldtext_1) as nerem,'Any Political Links' as label29,upper(political) as political,'If Yes, Details' as label30,upper(politicdet) as politicdet,'Verifier Remarks' as label31,upper(verremarks) as verrem,'Final Status' as label33,upper(status) as status,'Remarks' as label34,upper(remarks) as remarks,'Scanned By' as label35,upper(reportdoneby),'Verifier Name' as label35,upper(rvername),'Agency Seal','images/signs/'||sdoneby||'.png' as singature,(case when portfolio_id=19 OR portfolio_id=43 then 'Photographs' else '{SK!PTH15D@TA}' end) as lblphotographs,coalesce((case when portfolio_id=19 OR portfolio_id=43 then sublist('select docpath as field from tagged_docs where isdeleted=0 and doctype=''REP_ATTACH'' and vistype=''R'' and uuid='''||r.uuid||'''',',') else '{SK!PTH15D@TA}' end),'NA') as photographs from banklist_main_residence r where (val0) val1 Query535!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(receivedate,'DD/MM/YYYY') as recdate,applno,upper(customername) cname,bank_branch_id,upper(name) as pname1,'OFFICE VERIFICATION REPORT' as label1,'Agency' as label2,upper(maincompany) as agenname,'Received Date' as label4,to_char(receivedate,'DD/MM/YYYY') as receivedate,'Application No' as label5,upper(applno) as applno1,'Product' as label6,upper(product) as product1,'Landmark' as label7,'' as landmark,'Visit Date' as label8,visitdate,'Visit time' as label9,visittime,'Report Date' as label10,to_char(sdoneon,'DD/MM/YYYY') as sdoneon,'Applicants Name' as label11,upper(customername) as customername2,'Occupation' as label12,upper(category) as category1,'Address' as label13,upper(oaddr) as oaddr,'Met Person' as label14,upper(metperson) as metperson,'Relation' as label15,(case when upper(metrela)='OTHERS' then upper(metrealoth) else upper(metrela) end) as metrela,'Ownership' as label16,(case when upper(ownership)='OTHERS' then upper(owneroth) else upper(ownership) end) as ownership,'Job/Biz at Curr Add' as label17,((case when (yrsjob<>'0' and yrsjob<>'') then yrsjob||' YEARS(S) ' else '' end)||(case when (mnthjob<> '0' and mnthjob <> '') then mnthjob||' MONTH(S)' else '' end)) as yrsatjob,'Designation'as label18,(case when upper(appdesign)='OTHERS' then upper(appdesignoth) else upper(appdesign) end) as appdesign,'Department' as label19,upper(appdept) as appdept,'Reporting To' as label20,upper(reportingto) as reportingto,'Salary' as label21,upper(saldetail) as saldetail,'NOB' as label22,(case when upper(compprof)='OTHERS' then upper(compprofoth) else upper(compprof) end) as compprof,'Board Seen' as lblboardseen, (case when bizboard='-1' then 'NA' else upper(bizboard) end) as bizboard,'Ease of Access' as label27,upper(easeloc) as easeloc,'Locality' as label28,(case when upper(locality)='OTHERS' then upper(locaoth) else upper(locality) end) as locality,'Organisation Type' as label29,upper(busitype) as orgtype,'Neighbour Reference' as label30,(case when upper(field30_9)='-1' then 'NA' else upper(field30_9) end) as neref,'Neighbour Feedback' as label31,upper(fieldtext_1) as negrem,'Final Status' as label36,upper(status) as status,'Remarks' as label37,upper(remarks) as remarks,'Scanned By' as label38,upper(reportdoneby) as doneby,'Verifier Name' as lblvername, upper(overname) as overname, 'Agency Seal'as lbl,'images/signs/'||sdoneby||'.png' as singature,(case when portfolio_id=19 OR portfolio_id=43 then 'Photographs' else '{SK!PTH15D@TA}' end) as lblphotographs,coalesce((case when portfolio_id=19 OR portfolio_id=43 then sublist('select docpath as field from tagged_docs where isdeleted=0 and doctype=''REP_ATTACH'' and vistype=''O'' and uuid='''||o.uuid||'''',',') else '{SK!PTH15D@TA}' end),'NA') as photographs from banklist_main_office o where (val0) val1 Query536!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(receivedate,'DD/MM/YYYY') as recdate,applno,upper(customername) as cname,bank_branch_id,'MATRIX - Residence Report','Client Name:- '||upper(name),'Product:- '||upper(product),'Application ID:- '||upper(applno) as applno1,'Applicant Name:- '||upper(customername) as cname1,'Verification Type:-','Residence:','images/tick.gif' as chkresidence,'RCO:',(case when upper(Field15_1)='YES' then 'images/tick.gif' else 'images/untick.jpg' end) as chkrco,'Office:','images/untick.jpg' as chkoffice,'Date & Time of Visit: - '||coalesce(visitdate||' ','')||coalesce(visittime,''),'Address of Visit:','Residence Address: -',upper(raddr) as raddr,'Office Address: -','','Remarks & Observations:',upper(remarks) as remarks,'Status:',upper(status),'Check List:','Residence:','1.','Stay Confirm:','Yes',(case when upper(Field30_3)='YES' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_3)='NO' then 'images/tick.gif' else 'images/untick.jpg' end),'2.','Location Type:','Slum',(case when upper(locality)='SLUM' then 'images/tick.gif' else 'images/untick.jpg' end),'Chawl',(case when upper(locality)='CHAWL TYPE' then 'images/tick.gif' else 'images/untick.jpg' end),'UMC',(case when upper(locality)='UPPER MIDDLE CLASS' then 'images/tick.gif' else 'images/untick.jpg' end),'LMC',(case when upper(locality)='LOWER MIDDLE CLASS' then 'images/tick.gif' else 'images/untick.jpg' end),'','Com-Dom',(case when upper(locality)='COM-DOM' then 'images/tick.gif' else 'images/untick.jpg' end),'Poor',(case when upper(locality)='POOR' then 'images/tick.gif' else 'images/untick.jpg' end),'3.','TPC Confirm:','Yes',(case when upper(Field30_9)='POSITIVE' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_9)='NEGATIVE' then 'images/tick.gif' else 'images/untick.jpg' end),'4.','Name Board:',upper(platedet),'','Business:','1.','Working Confirm:','Yes','images/untick.jpg','No','images/untick.jpg','2.','Name board Match with the application details','Yes','images/untick.jpg','No','images/untick.jpg','3.','Nature of Business:','','','Field Executive Name: - '||upper(rvername) as vername,'Reports Prepared By: - '||upper(sdonebyname),'Manager/Authority Name & Sign:','images/signs/'||supervisor||'.png' as singature,(case portfolio_id when 194 then 'Images:' else '{SK!PTH15D@TA}' end) as lblphoto,(case portfolio_id when 194 then sublist('select docpath as field from tagged_docs where isdeleted=0 and doctype=''REP_ATTACH'' and vistype=''R'' and uuid='''||r.uuid||'''',',') else '{SK!PTH15D@TA}' end) as photographs from banklist_main_residence r where (val0) val1 -Query537!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(receivedate,'DD/MM/YYYY') as recdate,applno,upper(customername) as cname,bank_branch_id,'MATRIX - Office Report','Client Name:- '||upper(name),'Product:- '||upper(product),'Application ID:- '||upper(applno) as applno1,'Applicant Name:- '||upper(customername) as cname1,'Verification Type:-','Residence:','images/untick.jpg' as chkresidence,'RCO:',(case when upper(Field15_1)='YES' then 'images/tick.gif' else 'images/untick.jpg' end) as chkrco,'Office:','images/tick.gif' as chkoffice,'Date & Time of Visit: - '||coalesce(visitdate||' ','')||coalesce(visittime,''),'Address of Visit:','Residence Address: -','' as raddr,'Office Address: -',upper(oaddr) as oaddr,'Remarks & Observations:',upper(remarks) as remarks,'Status:',upper(status),'Check List:','Residence:','1.','Stay Confirm:','Yes','images/untick.jpg','No','images/untick.jpg','2.','Location Type:','Slum','images/untick.jpg','Chawl','images/untick.jpg','UMC','images/untick.jpg','LMC','images/untick.jpg','','Com-Dom','images/untick.jpg','Poor','images/untick.jpg','3.','TPC Confirm:','Yes',(case when upper(Field30_9)='POSITIVE' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_9) <> 'POSITIVE' AND upper(Field30_9) <> 'NA' then 'images/tick.gif' else 'images/untick.jpg' end),'4.','Name Board:','','','Business:','1.','Working Confirm:','Yes',(case when upper(Field30_22)='YES' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_22) <> 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'2.','Name board Match with the application details','Yes',(case when upper(Field30_2) = 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_2) <> 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'3.','Nature of Business:',upper(proddealt),'','Field Executive Name: - '||upper(overname) as vername,'Reports Prepared By: - '||upper(sdonebyname),'Manager/Authority Name & Sign:','images/signs/'||supervisor||'.png' as singature,(case portfolio_id when 194 then 'Images:' else '{SK!PTH15D@TA}' end) as lblphoto,(case portfolio_id when 194 then sublist('select docpath as field from tagged_docs where isdeleted=0 and doctype=''REP_ATTACH'' and vistype=''O'' and uuid='''||o.uuid||'''',',') else '{SK!PTH15D@TA}' end) as photographs from banklist_main_office o where (val0) val1 \ No newline at end of file +Query537!C0L!select!C0L!select uuid,product,maincompany,bkbranchname,to_char(receivedate,'DD/MM/YYYY') as recdate,applno,upper(customername) as cname,bank_branch_id,'MATRIX - Office Report','Client Name:- '||upper(name),'Product:- '||upper(product),'Application ID:- '||upper(applno) as applno1,'Applicant Name:- '||upper(customername) as cname1,'Verification Type:-','Residence:','images/untick.jpg' as chkresidence,'RCO:',(case when upper(Field15_1)='YES' then 'images/tick.gif' else 'images/untick.jpg' end) as chkrco,'Office:','images/tick.gif' as chkoffice,'Date & Time of Visit: - '||coalesce(visitdate||' ','')||coalesce(visittime,''),'Address of Visit:','Residence Address: -','' as raddr,'Office Address: -',upper(oaddr) as oaddr,'Remarks & Observations:',upper(remarks) as remarks,'Status:',upper(status),'Check List:','Residence:','1.','Stay Confirm:','Yes','images/untick.jpg','No','images/untick.jpg','2.','Location Type:','Slum','images/untick.jpg','Chawl','images/untick.jpg','UMC','images/untick.jpg','LMC','images/untick.jpg','','Com-Dom','images/untick.jpg','Poor','images/untick.jpg','3.','TPC Confirm:','Yes',(case when upper(Field30_9)='POSITIVE' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_9) <> 'POSITIVE' AND upper(Field30_9) <> 'NA' then 'images/tick.gif' else 'images/untick.jpg' end),'4.','Name Board:','','','Business:','1.','Working Confirm:','Yes',(case when upper(Field30_22)='YES' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_22) <> 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'2.','Name board Match with the application details','Yes',(case when upper(Field30_2) = 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'No',(case when upper(Field30_2) <> 'YES' then 'images/tick.gif' else 'images/untick.jpg' end),'3.','Nature of Business:',upper(proddealt),'','Field Executive Name: - '||upper(overname) as vername,'Reports Prepared By: - '||upper(sdonebyname),'Manager/Authority Name & Sign:','images/signs/'||supervisor||'.png' as singature,(case portfolio_id when 194 then 'Images:' else '{SK!PTH15D@TA}' end) as lblphoto,(case portfolio_id when 194 then sublist('select docpath as field from tagged_docs where isdeleted=0 and doctype=''REP_ATTACH'' and vistype=''O'' and uuid='''||o.uuid||'''',',') else '{SK!PTH15D@TA}' end) as photographs from banklist_main_office o where (val0) val1 diff --git a/pom.xml b/pom.xml index 9e22f2d..93c75a4 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,8 @@ Cygnus Platform + cygnus-lib + cygnus-onprem-db cygnus-cloud-service cygnus-cloud-client cygnus-installer @@ -37,5 +39,6 @@ 5.1.5 1.2.8 10.4 + 1.18.46