diff --git a/.gitignore b/.gitignore index 3848554c5..977efb8f4 100644 --- a/.gitignore +++ b/.gitignore @@ -93,5 +93,4 @@ site .jqwik-database # Eclipse - .factorypath diff --git a/TrueFilingEcf4/pom.xml b/TrueFilingEcf4/pom.xml index f141fa070..697a3e275 100644 --- a/TrueFilingEcf4/pom.xml +++ b/TrueFilingEcf4/pom.xml @@ -89,28 +89,15 @@ org.apache.maven.plugins maven-resources-plugin - + org.apache.cxf @@ -128,16 +115,13 @@ ${project.build.directory}/generated-sources/cxf - target/classes/wsdl/WebServices/dev/ECF-4.0-FilingReviewMDEService.wsdl - - - target/classes/wsdl/WebServices/dev/ECF-4.0-CourtRecordMDEService.wsdl + src/main/resources/wsdl/WebServices/dev/ECF-4.0-FilingReviewMDEService.wsdl - target/classes/wsdl/WebServices/dev/ECF-4.0-ServiceMDEService.wsdl + src/main/resources/wsdl/WebServices/dev/ECF-4.0-CourtRecordMDEService.wsdl - target/classes/wsdl/WebServices/dev/ECF-4.0-FilingAssemblyMDEService.wsdl + src/main/resources/wsdl/WebServices/dev/ECF-4.0-FilingAssemblyMDEService.wsdl -Djavax.xml.accessExternalDTD=all diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/Ecf4Helper.java b/TylerEcf4/src/main/java/edu/suffolk/litlab/efsp/tyler/Ecf4Helper.java similarity index 85% rename from proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/Ecf4Helper.java rename to TylerEcf4/src/main/java/edu/suffolk/litlab/efsp/tyler/Ecf4Helper.java index 4671c59f1..fdcd6c42a 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/Ecf4Helper.java +++ b/TylerEcf4/src/main/java/edu/suffolk/litlab/efsp/tyler/Ecf4Helper.java @@ -1,4 +1,21 @@ -package edu.suffolk.litlab.efsp.server.ecf4; +package edu.suffolk.litlab.efsp.tyler; + +import java.io.File; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Optional; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.namespace.QName; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ecf4.latest.gov.niem.niem.domains.jxdm._4.CourtType; import ecf4.latest.gov.niem.niem.fips_10_4._2.CountryCodeType; @@ -11,51 +28,27 @@ import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.CaseFilingType; import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.PersonType; import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.QueryMessageType; -import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.QueryResponseMessageType; -import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; -import edu.suffolk.litlab.efsp.tyler.TylerErrorCodes; -import jakarta.ws.rs.core.Response; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; -import java.io.File; -import java.io.StringWriter; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeConstants; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; -import javax.xml.namespace.QName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -/** - * Helper class that easily converts Java native types to Cumbersome XML Schema types, particularly - * those from Oasis's ECF standard, and the National Information Exchange Model (NIEM). - * - *

Some information about NIEM: Many of the types here all include the - * "SimpleObjectAttributeGroup", which, "provides a collection of attributes which are appropriate - * for definition of object types", including an ID attribute, a Metadata attribute, and a Link - * Metadata attribute that directly reference objects via IDs instead of including the object as an - * element. TBH, I'm not sure what the design decisions that dictate the use of them are, but they - * exist. - * - * @author Bryce Willey - */ public class Ecf4Helper { private static final Logger log = LoggerFactory.getLogger(Ecf4Helper.class); - + static final ecf4.latest.gov.niem.niem.proxy.xsd._2.ObjectFactory niemProxyObjFac; static final ecf4.latest.gov.niem.niem.niem_core._2.ObjectFactory niemCoreObjFac; static final ecf4.latest.gov.niem.niem.domains.jxdm._4.ObjectFactory jxObjFac; static final DatatypeFactory datatypeFac; + /** + * One of the ways that you can communicate over ECF. For more information, see + * https://docs.oasis-open.org/legalxml-courtfiling/specs/ecf/v4.01/ecf-v4.01-spec/errata02/os/ecf-v4.01-spec-errata02-os-complete.html#_Toc425241629 + */ + public static final String MDE_PROFILE_CODE = + "urn:oasis:names:tc:legalxml-courtfiling:schema:xsd:WebServicesMessaging-2.0"; + + static { niemProxyObjFac = new ecf4.latest.gov.niem.niem.proxy.xsd._2.ObjectFactory(); niemCoreObjFac = new ecf4.latest.gov.niem.niem.niem_core._2.ObjectFactory(); @@ -83,6 +76,11 @@ public static DateType convertDate(LocalDate date) { dt.setDateRepresentation(niemCoreObjFac.createDate(d)); return dt; } + + public static LocalDate parseDate(DateType dt) { + var d = ((ecf4.latest.gov.niem.niem.proxy.xsd._2.Date) dt.getDateRepresentation().getValue()).getValue(); + return LocalDate.of(d.getYear(), d.getMonth(), d.getDay()); + } /** Niem's Boolean type: either true or false. */ public static ecf4.latest.gov.niem.niem.proxy.xsd._2.Boolean convertBool(boolean bool) { @@ -293,7 +291,7 @@ public static CountryCodeType strToCountryCode(String country) { return cct; } - public static T prep(T newMsg, String courtId) { + public static T prep(T newMsg, String courtId, String locationId) { EntityType typ = new EntityType(); var commonObjFac = new ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4 @@ -305,16 +303,11 @@ public static T prep(T newMsg, String courtId) { // Example in the ECF4 docs is "https://filingreviewmde.com", which matches best to // EXTERNAL_URL. // Doesn't seem to be used by Tyler however. - newMsg.setSendingMDELocationID(Ecf4Helper.convertId(ServiceHelpers.EXTERNAL_URL)); - newMsg.setSendingMDEProfileCode(ServiceHelpers.MDE_PROFILE_CODE); + newMsg.setSendingMDELocationID(Ecf4Helper.convertId(locationId)); + newMsg.setSendingMDEProfileCode(MDE_PROFILE_CODE); return newMsg; } - public static Response makeResponse( - QueryResponseMessageType resp, Supplier defaultRespFunc) { - return mapTylerCodesToHttp(checkErrors(resp.getError()), defaultRespFunc); - } - public record Error(String code, String text) {} /** @@ -337,23 +330,8 @@ public static List checkErrors( return errors.stream().flatMap(err -> checkError(err).stream()).toList(); } - public static Response mapTylerCodesToHttp(List err, Supplier defaultResp) { - if (!err.isEmpty()) { - var mainErr = err.getFirst(); - if (TylerErrorCodes.tylerToHttp.containsKey(mainErr.code)) { - return Response.status(TylerErrorCodes.tylerToHttp.get(mainErr.code)) - .entity(mainErr.text) - .build(); - } - - // 422 as semantic issues covers most of the error codes - return Response.status(422).entity(mainErr.text).build(); - } - return defaultResp.get(); - } - - public static void setupReplys(CaseFilingType reply) { - reply.setSendingMDELocationID(Ecf4Helper.convertId(ServiceHelpers.SERVICE_URL)); - reply.setSendingMDEProfileCode(ServiceHelpers.MDE_PROFILE_CODE); + public static void setupReplys(CaseFilingType reply, String locationId) { + reply.setSendingMDELocationID(Ecf4Helper.convertId(locationId)); + reply.setSendingMDEProfileCode(MDE_PROFILE_CODE); } } diff --git a/TylerEfmClient/.gitignore b/TylerEfmClient/.gitignore new file mode 100644 index 000000000..00d2ab71d --- /dev/null +++ b/TylerEfmClient/.gitignore @@ -0,0 +1,2 @@ +/.apt_generated/ +/.apt_generated_tests/ diff --git a/TylerEfmClient/pom.xml b/TylerEfmClient/pom.xml index 5f022b1b5..e16e5fa7e 100644 --- a/TylerEfmClient/pom.xml +++ b/TylerEfmClient/pom.xml @@ -7,13 +7,14 @@ efspserver-parent 1.4.0 - edu.suffolk.litlab tyler-efm-client Tyler EFM Services Client code For Tyler's EFMUserService and EFMFirmService. Lets you choose the specific version that you want to interact with. 4.1.6 + + jdt_apt diff --git a/pom.xml b/pom.xml index 934112795..b272cc86f 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ TylerEfmClient TylerEcf4 TylerEcf5 + TrueFilingEcf4 proxyserver @@ -74,6 +75,11 @@ tyler-ecf5 ${project.version} + + edu.suffolk.litlab + truefiling-ecf4 + ${project.version} + org.mapstruct @@ -108,6 +114,7 @@ docs + site enunciate.xml ${project.build.directory}/enunciate-docs diff --git a/proxyserver/pom.xml b/proxyserver/pom.xml index 75fb37f82..94a8abea0 100644 --- a/proxyserver/pom.xml +++ b/proxyserver/pom.xml @@ -53,6 +53,10 @@ edu.suffolk.litlab tyler-ecf5 + + edu.suffolk.litlab + truefiling-ecf4 + org.apache.commons commons-lang3 diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecf4/EcfCaseTypes.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecf4/EcfCaseTypes.java new file mode 100644 index 000000000..393ff046c --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecf4/EcfCaseTypes.java @@ -0,0 +1,17 @@ +package edu.suffolk.litlab.efsp.ecf4; + +/** + * Straight from ECF4 Spec: + * https://docs.oasis-open.org/legalxml-courtfiling/specs/ecf/v4.01/ecf-v4.01-spec/os/ecf-v4.01-spec-os.html#_Toc343512773 + * + *

Enumerated in a few other places, but none of the XML has a straight enum to use. + */ +public enum EcfCaseTypes { + AppellateCase, + BankruptcyCase, + CitationCase, + CivilCase, + CriminalCase, + DomesticCase, + JuvenileCase; +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseAPI.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseAPI.java index 76f038840..41507c8b4 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseAPI.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseAPI.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +42,8 @@ public CodeDatabaseAPI(Connection conn) { * Maps the name of an ECF element to be substituted by a court-specific code list or extension. */ public abstract Map xmlElemToTableName(); + + public abstract Set systemTables(); /** * Gets all court location identifiers (CLI) stored in the database. diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseUtils.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseUtils.java index c79ccb30d..136b77e1b 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseUtils.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDatabaseUtils.java @@ -10,7 +10,9 @@ public class CodeDatabaseUtils { /** This exception is returned when a given table name isn't in the pre-approved list of names. */ public static class UnsupportedTableException extends SQLException { - public UnsupportedTableException(String message) { + private static final long serialVersionUID = 42L; + + public UnsupportedTableException(String message) { super(message); } } diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDocException.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDocException.java index 77ce9888c..ef52857ad 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDocException.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeDocException.java @@ -7,6 +7,8 @@ * (instead of an ops issue). */ public class CodeDocException extends Exception { + private static final long serialVersionUID = 1L; + public CodeDocException(String message, Throwable ex) { super(message, ex); } diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeUpdater.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUpdater.java similarity index 62% rename from proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeUpdater.java rename to proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUpdater.java index f95a8b98c..f0f13581d 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeUpdater.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUpdater.java @@ -1,26 +1,12 @@ -package edu.suffolk.litlab.efsp.tyler.ecfcodes; +package edu.suffolk.litlab.efsp.ecfcodes; -import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyquerymessage_4.CourtPolicyQueryMessageType; -import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyresponsemessage_4.CourtPolicyResponseMessageType; -import ecf4.latest.oasis.names.tc.legalxml_courtfiling.wsdl.webservicesprofile_definitions_4_0.FilingReviewMDEPort; -import ecf4.latest.tyler.efm.wsdl.webservicesprofile_implementation_4_0.FilingReviewMDEService; import edu.suffolk.litlab.efsp.Jurisdiction; import edu.suffolk.litlab.efsp.db.DatabaseCreator; -import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseAPI; import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseUtils.UnsupportedTableException; -import edu.suffolk.litlab.efsp.server.ecf4.Ecf4Helper; import edu.suffolk.litlab.efsp.server.logging.MDCWrappers; import edu.suffolk.litlab.efsp.server.utils.HeaderSigner; -import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; -import edu.suffolk.litlab.efsp.server.utils.SoapX509CallbackHandler; -import edu.suffolk.litlab.efsp.tyler.SoapClientChooser; -import edu.suffolk.litlab.efsp.tyler.TylerClients; -import edu.suffolk.litlab.efsp.tyler.TylerUserClient; -import edu.suffolk.litlab.efsp.tyler.TylerUserFactory; -import edu.suffolk.litlab.efsp.tyler.TylerUserNamePassword; +import edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeDatabase; import jakarta.xml.bind.JAXBException; -import jakarta.xml.ws.BindingProvider; -import jakarta.xml.ws.soap.SOAPFaultException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -33,6 +19,7 @@ import java.sql.Savepoint; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,24 +27,21 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipInputStream; import javax.sql.DataSource; -import org.apache.cxf.headers.Header; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import tyler.efm.latest.services.schema.authenticaterequest.AuthenticateRequestType; -import tyler.efm.latest.services.schema.authenticateresponse.AuthenticateResponseType; /** * Updates the Tyler "Codes" for each court in a jurisdiction. * - *

There are 3 main steps to download a code for a Tyler court: 1. calling `getPolicy` (see - * {@link FilingReviewMDEPort}) for a court to get the URLs that the codes are available at. - this + *

There are 3 main steps to download a code for a Tyler court: 1. calling `FilingReview::getPolicy` for a court to get the URLs that the codes are available at. - this * URL can change, but tbh doesn't change often. It takes approximately 1 for a call to getPolicy to * complete. 2. downloading the codes, which are zip files available at the above URL. - You need * the court itself, the table that you are downloading. Currently, we take the input stream and @@ -119,10 +103,11 @@ public static InputStream getCodesZip(String toRead, String authHeader) * @throws URISyntaxException */ private boolean downloadAndProcessZip( - String toRead, String signedTime, Function process) + Supplier urlStreamMaker, Function process) throws URISyntaxException { + //String toRead, String signedTime, Instant startTable = Instant.now(); - try (InputStream urlStream = getCodesZip(toRead, signedTime)) { + try (InputStream urlStream = urlStreamMaker.get()) { //getCodesZip(toRead, signedTime)) { // Write out the zip file downloadDuration = downloadDuration.plus(Duration.between(startTable, Instant.now())); @@ -137,27 +122,18 @@ private boolean downloadAndProcessZip( } catch (IOException ex) { // Some system codes (everything but "country", "state", "filingstatus", "datafieldconfig", // and "servicetype") are expected to 500. Not really sure why they give us bad URLs. - log.warn("Skipping {}, got exception accessing zip: ", toRead, ex); + log.warn("Skipping, got exception accessing zip: ", ex); return false; } } - private boolean downloadSystemTables(String baseUrl, CodeDatabaseAPI cd, HeaderSigner signer) + private boolean downloadSystemTables(Map> codelistUrls, CodeDatabaseAPI cd, HeaderSigner signer) throws SQLException, IOException, JAXBException, URISyntaxException { MDC.put(MDCWrappers.SESSION_ID, "system"); - Map codeUrls = - Map.of( - "version", - "/CodeService/codes/version/", - "location", - "/CodeService/codes/location/", - // NOTE: the Tyler docs say "error" is available from `GetPolicy'. That is wrong. - "error", - "/CodeService/codes/error"); - - for (Map.Entry urlSuffix : codeUrls.entrySet()) { - MDC.put(MDCWrappers.REQUEST_ID, urlSuffix.getKey()); - cd.createTableIfAbsent(urlSuffix.getKey()); + + for (String tableName: codelistUrls.keySet()) { + MDC.put(MDCWrappers.REQUEST_ID, tableName); + cd.createTableIfAbsent(tableName); } MDC.put(MDCWrappers.REQUEST_ID, "installedversion"); @@ -176,16 +152,18 @@ private boolean downloadSystemTables(String baseUrl, CodeDatabaseAPI cd, HeaderS return false; } - for (Map.Entry urlSuffix : codeUrls.entrySet()) { + for (var tableAndUrl : codelistUrls.entrySet()) { // Let SQL exceptions through here; table names are hard coded, so if they break // we need to know. - cd.deleteFromTable(urlSuffix.getKey()); + var tableName = tableAndUrl.getKey(); + var urlGetter = tableAndUrl.getValue(); + cd.deleteFromTable(tableName); final Function process = (is) -> { - MDC.put(MDCWrappers.REQUEST_ID, urlSuffix.getKey()); + MDC.put(MDCWrappers.REQUEST_ID, tableName); try { // "0" is used as the system court location. - cd.updateTable(urlSuffix.getKey(), "0", is); + cd.updateTable(tableName, "0", is); return true; } catch (final Exception e1) { log.error("Error when updating table", e1); @@ -193,7 +171,7 @@ private boolean downloadSystemTables(String baseUrl, CodeDatabaseAPI cd, HeaderS } }; boolean updateSuccess = - downloadAndProcessZip(baseUrl + urlSuffix.getValue(), signedTime.get(), process); + downloadAndProcessZip(urlGetter, signedTime.get(), process); MDC.remove(MDCWrappers.REQUEST_ID); if (!updateSuccess) { cd.rollback(sp); @@ -209,15 +187,7 @@ private boolean downloadSystemTables(String baseUrl, CodeDatabaseAPI cd, HeaderS * Internal class, meant to pass around information between the "getPolicy URL" stage and the * "download zip" stage. */ - private static class CodeToDownload { - final String tableName; - final String url; - - public CodeToDownload(String tableName, String url) { - this.tableName = tableName; - this.url = url; - } - } + private static record CodeToDownload(String tableName, String url) {} /** * Internal class, meant to pass around information between the "download zip" stage and the @@ -255,26 +225,20 @@ private static Map streamDownload( return codeLists; } - private static Map streamPolicies( - Stream locations, Jurisdiction jurisdiction, FilingReviewMDEPort filingPort) { - var policies = new ConcurrentHashMap(); + private static Map> streamPolicies( + Stream locations, Jurisdiction jurisdiction, Function> courtCodeGetter) { + var policies = new ConcurrentHashMap>(); locations.forEach( - location -> { - var m = Ecf4Helper.prep(new CourtPolicyQueryMessageType(), location); - try { - CourtPolicyResponseMessageType p = filingPort.getPolicy(m); - policies.put(location, p); - } catch (SOAPFaultException ex) { - log.warn( - "Got a SOAP excption getting policy for {} in {}: ", location, jurisdiction, ex); - } + location -> { + policies.put(location, courtCodeGetter.apply(location)); }); return policies; } - private String makeCodeUrl(String baseUrl, String tableName, String location) { - return baseUrl + "CodeService/codes/" + tableName + "/" + location.replace(" ", "%20"); - } + + // An agnostic type for CourtCodelistType + public record CourtCodelistInfo(String ecfElement, String uri, LocalDate effectiveDate) {} + /** * @param tables If empty, all versions will be downloaded @@ -284,8 +248,7 @@ private boolean downloadCourtTables( Optional> tables, CodeDatabaseAPI cd, HeaderSigner signer, - CourtPolicyResponseMessageType policyResp, - String baseUrl) + List policyResp) throws JAXBException, IOException, SQLException { MDC.put(MDCWrappers.SESSION_ID, location); log.info("Doing updates for: {},\ttables: {}", location, tables); @@ -293,16 +256,12 @@ private boolean downloadCourtTables( // TODO(brycew-later): check that the effective date is later than today // JAXBElement obj = ccl.getEffectiveDate().getDateRepresentation(); Map urlMap = - policyResp.getRuntimePolicyParameters().getCourtCodelist().stream() + policyResp.stream() .collect( Collectors.toMap( - (cc1) -> cd.xmlElemToTableName().get(cc1.getECFElementName().getValue()), + (cc1) -> cd.xmlElemToTableName().get(cc1.ecfElement()), // Tyler gives us URLs w/ spaces, which aren't valid. This makes them valid - (cc1) -> - cc1.getCourtCodelistURI() - .getIdentificationID() - .getValue() - .replace(" ", "%20"))); + (cc1) -> cc1.uri().replace(" ", "%20"))); Stream toDownload; if (tables.isEmpty()) { toDownload = @@ -310,11 +269,8 @@ private boolean downloadCourtTables( } else { toDownload = tables.get().stream() - .map( - tableName -> { - String guessUrl = makeCodeUrl(baseUrl, tableName, location); - return new CodeToDownload(tableName, urlMap.getOrDefault(tableName, guessUrl)); - }); + .filter(tableName -> urlMap.containsKey(tableName)) + .map(tableName -> new CodeToDownload(tableName, urlMap.get(tableName))); } Optional signedTime = signer.signedCurrentTime(); if (signedTime.isEmpty()) { @@ -394,11 +350,11 @@ private Map> makeOrRemoveUnsupportedTables( * * @throws URISyntaxException */ - public boolean updateAll(String baseUrl, FilingReviewMDEPort filingPort, CodeDatabaseAPI cd) + public boolean updateAll(Map systemUrls, Function> codeInfoGetter, CodeUrlGetter urlGetter, CodeDatabaseAPI cd) throws SQLException, IOException, JAXBException, URISyntaxException { cd.setAutoCommit(false); HeaderSigner signer = new HeaderSigner(this.pathToKeystore, this.x509Password); - if (!downloadSystemTables(baseUrl, cd, signer)) { + if (!downloadSystemTables(systemUrls, cd, signer)) { log.warn( "System tables didn't update, but we needed them " + " to actually figure out new versions"); @@ -434,9 +390,9 @@ public boolean updateAll(String baseUrl, FilingReviewMDEPort filingPort, CodeDat } log.info("Took {} to remove existing tables", Duration.between(startDel, Instant.now())); Instant startPolicy = Instant.now(); - Map policies = + Map> policies = streamPolicies( - versionsToUpdate.keySet().stream().parallel(), cd.getJurisdiction(), filingPort); + versionsToUpdate.keySet().stream().parallel(), cd.getJurisdiction(), codeInfoGetter); var soapInc = Duration.between(startPolicy, Instant.now()); soapDuration = soapDuration.plus(soapInc); log.info("Soaps took: {} (total: {})", soapInc, soapDuration); @@ -445,7 +401,7 @@ public boolean updateAll(String baseUrl, FilingReviewMDEPort filingPort, CodeDat final String courtLocation = policy.getKey(); final List tables = versionsToUpdate.get(courtLocation); if (!downloadCourtTables( - courtLocation, Optional.of(tables), cd, signer, policy.getValue(), baseUrl)) { + courtLocation, Optional.of(tables), cd, signer, policy.getValue())) { log.warn("Failed updating court {}'s tables {}", courtLocation, tables); cd.rollback(sp); return false; @@ -462,18 +418,18 @@ public boolean updateAll(String baseUrl, FilingReviewMDEPort filingPort, CodeDat * * @throws URISyntaxException */ - public boolean replaceAll(String baseUrl, FilingReviewMDEPort filingPort, CodeDatabaseAPI cd) + public boolean replaceAll(Map systemUrls, Function> codeInfoGetter, CodeDatabaseAPI cd) throws SQLException, IOException, JAXBException, URISyntaxException { - return replaceSome(baseUrl, filingPort, cd, List.of()); + return replaceSome(systemUrls, codeInfoGetter, cd, List.of()); } public boolean replaceSome( - String baseUrl, FilingReviewMDEPort filingPort, CodeDatabaseAPI cd, List locs) + Map systemUrls, Function> codeInfoGetter, CodeDatabaseAPI cd, List locs) throws SQLException, IOException, JAXBException, URISyntaxException { cd.setAutoCommit(false); HeaderSigner signer = new HeaderSigner(this.pathToKeystore, this.x509Password); log.info("Downloading system tables for {}", cd.getJurisdiction()); - boolean success = downloadSystemTables(baseUrl, cd, signer); + boolean success = downloadSystemTables(systemUrls, cd, signer); var tablesToDeleteDomain = cd.xmlElemToTableName().values(); for (String table : tablesToDeleteDomain) { @@ -491,15 +447,14 @@ public boolean replaceSome( // Remove the "0" or top level court, which doesn't usually have individual court tables locs.remove("0"); Instant startPolicy = Instant.now(); - Map policies = - streamPolicies(locs.parallelStream(), cd.getJurisdiction(), filingPort); + var policies = streamPolicies(locs.parallelStream(), cd.getJurisdiction(), codeInfoGetter); soapDuration = soapDuration.plus(Duration.between(startPolicy, Instant.now())); log.info("Soaps: {}", soapDuration); for (var policy : policies.entrySet()) { final String location = policy.getKey(); log.info("Downloading tables for {}", location); success &= - downloadCourtTables(location, Optional.empty(), cd, signer, policy.getValue(), baseUrl); + downloadCourtTables(location, Optional.empty(), cd, signer, policy.getValue()); } log.info( "Downloads took: {}, updates took: {}, soaps took: {}", @@ -512,39 +467,12 @@ public boolean replaceSome( return success; } - /** Sets up the WSDL connection to Tyler, used for `getPolicy` to get the URL. */ - private static FilingReviewMDEPort loginWithTyler( - Jurisdiction jurisdiction, String userEmail, String userPassword) { - Optional userFactory = TylerClients.getEfmUserFactory(jurisdiction); - if (userFactory.isEmpty()) { - throw new RuntimeException("Can't find " + jurisdiction + " in Soap chooser for EFMUser"); - } - log.info("Getting filing factory for {}", jurisdiction); - Optional filingFactory = - SoapClientChooser.getFilingReviewFactory(jurisdiction); - if (filingFactory.isEmpty()) { - throw new RuntimeException( - "Can't find " + jurisdiction + " in Soap Chooser for filing review factory"); - } - TylerUserClient userPort = userFactory.get().makeUserClient(ServiceHelpers::setupServicePort); - AuthenticateRequestType authReq = new AuthenticateRequestType(); - authReq.setEmail(userEmail); - authReq.setPassword(userPassword); - AuthenticateResponseType authRes = userPort.authenticateUser(authReq); - List

headersList = TylerUserNamePassword.makeHeaderList(authRes); - FilingReviewMDEPort filingPort = filingFactory.get().getFilingReviewMDEPort(); - ServiceHelpers.setupServicePort((BindingProvider) filingPort); - Map ctx = ((BindingProvider) filingPort).getRequestContext(); - ctx.put(Header.HEADER_LIST, headersList); - return filingPort; - } - /** * Downloads a single codes zip. For Debugging. * * @throws URISyntaxException */ - public boolean downloadIndiv(List args) throws URISyntaxException { + public boolean downloadIndiv(List args, BiFunction makeUrl) throws URISyntaxException { if (args.size() < 3) { log.error( "Need to pass in args: downloadIndiv args) throws URISyntaxException { return false; } - var jurisdictionArg = Jurisdiction.parse(args.get(1)); - String table = args.get(2); String location = (args.size() == 4) ? args.get(3) : ""; HeaderSigner hs = new HeaderSigner(this.pathToKeystore, this.x509Password); - String endpoint = TylerClients.getTylerServerRootUrl(jurisdictionArg); return downloadAndProcessZip( - makeCodeUrl(endpoint, table, location), + makeUrl.apply(table, location), hs.signedCurrentTime().get(), (in) -> { String newFile = location.replace(':', '_') + "_" + table + "_test.xml"; @@ -573,42 +498,8 @@ public boolean downloadIndiv(List args) throws URISyntaxException { }); } - public static boolean executeCommand( - Supplier cdSupplier, - Jurisdiction jurisdiction, - List args, - String x509Password) { - SoapX509CallbackHandler.setX509Password(x509Password); - String command = args.get(0); - try { - String codesSite = TylerClients.getTylerServerRootUrl(jurisdiction); - FilingReviewMDEPort filingPort = - loginWithTyler( - jurisdiction, - System.getenv("TYLER_USER_EMAIL"), - System.getenv("TYLER_USER_PASSWORD")); - CodeUpdater cu = new CodeUpdater(System.getenv("PATH_TO_KEYSTORE"), x509Password); - if (command.equalsIgnoreCase("replaceall")) { - return cu.replaceAll(codesSite, filingPort, cdSupplier.get()); - } else if (command.equalsIgnoreCase("replacesome")) { - return cu.replaceSome( - codesSite, filingPort, cdSupplier.get(), args.subList(1, args.size())); - } else if (command.equalsIgnoreCase("refresh")) { - return cu.updateAll(codesSite, filingPort, cdSupplier.get()); - } else if (command.equalsIgnoreCase("downloadIndiv")) { - return cu.downloadIndiv(args); - } else { - log.error("Command {} isn't a real command", command); - return false; - } - } catch (SQLException | IOException | JAXBException | URISyntaxException e) { - log.error("Exception when doing code updating! ", e); - return false; - } - } - /** Should just be called from main. */ - private static CodeDatabaseAPI makeCodeDatabase(Jurisdiction jurisdiction) { + public static CodeDatabaseAPI makeCodeDatabase(Function dbMaker) { try { DataSource ds = DatabaseCreator.makeDataSource( @@ -620,45 +511,9 @@ private static CodeDatabaseAPI makeCodeDatabase(Jurisdiction jurisdiction) { 10, 100); - return switch (jurisdiction.getVendor()) { - case Jurisdiction.Vendor.TYLER -> CodeDatabase.fromDS(jurisdiction, ds); - default -> throw new RuntimeException("Other databases not yet supported"); - }; + return dbMaker.apply(ds); } catch (Exception ex) { throw new RuntimeException(ex); } } - - /** - * Run with: - * - *
-   * java -cp efspserver-with-deps.jar edu.suffolk.litlab.efsp.ecfcodes.CodeUpdater refresh
-   * 
- * - *

TODO(#111): use with this System property and class to try to fix parallel unmarshalling - * -Djava.util.concurrent.ForkJoinPool.common.threadFactory=edu.suffolk.litlab.efsp.server.JAXBForkJoinWorkerThreadFactory - * \ https://stackoverflow.com/a/57551188/11416267 - */ - public static void main(String[] args) throws Exception { - if (args.length < 1) { - log.error("Need to pass in a subprogram: downloadIndiv, or refresh"); - System.exit(1); - } - - List jurisdictions = - Stream.of(System.getenv("TYLER_JURISDICTIONS").split(" ")) - .map(Jurisdiction::parse) - .toList(); - for (var jurisdiction : jurisdictions) { - MDC.put(MDCWrappers.USER_ID, jurisdiction.getName()); - // Reusing USER for Jurisdiction, SESSION for the court / location, and REQUEST for the table - // name. - executeCommand( - () -> makeCodeDatabase(jurisdiction), - jurisdiction, - List.of(args), - System.getenv("X509_PASSWORD")); - } - } } diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUrlGetter.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUrlGetter.java new file mode 100644 index 000000000..22277d76b --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/ecfcodes/CodeUrlGetter.java @@ -0,0 +1,10 @@ +package edu.suffolk.litlab.efsp.ecfcodes; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; + +public interface CodeUrlGetter { + public InputStream get(String url) throws IOException, URISyntaxException; + public boolean refresh(); +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/TylerEcf4Helper.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/TylerEcf4Helper.java new file mode 100644 index 000000000..72f7e7bc6 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/ecf4/TylerEcf4Helper.java @@ -0,0 +1,56 @@ +package edu.suffolk.litlab.efsp.server.ecf4; + +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.CaseFilingType; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.QueryMessageType; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.QueryResponseMessageType; +import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; +import edu.suffolk.litlab.efsp.tyler.Ecf4Helper; +//import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; +import edu.suffolk.litlab.efsp.tyler.TylerErrorCodes; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.function.Supplier; + +/** + * Helper class that easily converts Java native types to Cumbersome XML Schema types, particularly + * those from Oasis's ECF standard, and the National Information Exchange Model (NIEM). + * + *

Some information about NIEM: Many of the types here all include the + * "SimpleObjectAttributeGroup", which, "provides a collection of attributes which are appropriate + * for definition of object types", including an ID attribute, a Metadata attribute, and a Link + * Metadata attribute that directly reference objects via IDs instead of including the object as an + * element. TBH, I'm not sure what the design decisions that dictate the use of them are, but they + * exist. + * + * @author Bryce Willey + */ +public class TylerEcf4Helper { + + public static T prep(T newMsg, String courtId) { + return Ecf4Helper.prep(newMsg, courtId, ServiceHelpers.EXTERNAL_URL); + } + + public static Response makeResponse( + QueryResponseMessageType resp, Supplier defaultRespFunc) { + return mapTylerCodesToHttp(Ecf4Helper.checkErrors(resp.getError()), defaultRespFunc); + } + + public static Response mapTylerCodesToHttp(List err, Supplier defaultResp) { + if (!err.isEmpty()) { + var mainErr = err.getFirst(); + if (TylerErrorCodes.tylerToHttp.containsKey(mainErr.code())) { + return Response.status(TylerErrorCodes.tylerToHttp.get(mainErr.code())) + .entity(mainErr.text()) + .build(); + } + + // 422 as semantic issues covers most of the error codes + return Response.status(422).entity(mainErr.text()).build(); + } + return defaultResp.get(); + } + + public static void setupReplys(CaseFilingType reply) { + Ecf4Helper.setupReplys(reply, ServiceHelpers.SERVICE_URL); + } +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/setup/tyler/TylerModuleSetup.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/setup/tyler/TylerModuleSetup.java index 0b493144b..ad2853f0b 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/setup/tyler/TylerModuleSetup.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/setup/tyler/TylerModuleSetup.java @@ -27,8 +27,8 @@ import edu.suffolk.litlab.efsp.server.utils.UpdateCodeVersions; import edu.suffolk.litlab.efsp.stdlib.StdLib; import edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeDatabase; -import edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeUpdater; import edu.suffolk.litlab.efsp.tyler.ecfcodes.DataFieldRow; +import edu.suffolk.litlab.efsp.tyler.ecfcodes.TylerCodeUpdater; import edu.suffolk.litlab.efsp.utils.InterviewToFilingInformationConverter; import java.sql.SQLException; import java.time.LocalTime; @@ -186,11 +186,11 @@ public void preSetup() { "Downloading just codes for {} in {}: please wait a bit", testOnlyLocation, jurisdiction); - CodeUpdater.executeCommand( + TylerCodeUpdater.executeCommand( () -> cd, jurisdiction, List.of("replacesome", testOnlyLocation), this.x509Password); } else { log.info("Downloading all codes for {}: please wait a bit", jurisdiction); - CodeUpdater.executeCommand( + TylerCodeUpdater.executeCommand( () -> cd, jurisdiction, List.of("replaceall"), this.x509Password); } } diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/HeaderSigner.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/HeaderSigner.java index 08f0ce132..280c79d75 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/HeaderSigner.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/HeaderSigner.java @@ -37,6 +37,9 @@ public class HeaderSigner { private final String pathToKeystore; private final String x509Password; + // Can be refreshed + private String signedTime = ""; + /** * The string password to the x509 certificate. This password is given by Tyler, and will also * unlock the .pfx file. @@ -128,9 +131,21 @@ public Optional signedBase64(String content) { * * @return signed bytes of the timestamp string that has been base 64 encoded */ - public Optional signedCurrentTime() { + private Optional signTime() { Instant now = Instant.now(Clock.systemUTC()); String currentTimestamp = now.toString(); return signedBase64(currentTimestamp); } + + public boolean refresh() { + var maybeTime = signTime(); + maybeTime.ifPresent(time -> { + this.signedTime = time; + }); + return maybeTime.isPresent(); + } + + public String signedCurrentTime() { + return this.signedTime; + } } diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/ServiceHelpers.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/ServiceHelpers.java index 8944fef04..14b678d5e 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/ServiceHelpers.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/ServiceHelpers.java @@ -26,13 +26,6 @@ public class ServiceHelpers { private static final Logger log = LoggerFactory.getLogger(ServiceHelpers.class); - /** - * One of the ways that you can communicate over ECF. For more information, see - * https://docs.oasis-open.org/legalxml-courtfiling/specs/ecf/v4.01/ecf-v4.01-spec/errata02/os/ecf-v4.01-spec-errata02-os-complete.html#_Toc425241629 - */ - public static final String MDE_PROFILE_CODE = - "urn:oasis:names:tc:legalxml-courtfiling:schema:xsd:WebServicesMessaging-2.0"; - public static final String MDE_PROFILE_CODE_5 = "urn:oasis:names:tc:legalxml-courtfiling:schema:xsd:WebServicesMessaging-5.0"; diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/UpdateCodeVersions.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/UpdateCodeVersions.java index 65d6ea77f..d59aafe6d 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/UpdateCodeVersions.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/server/utils/UpdateCodeVersions.java @@ -4,7 +4,8 @@ import edu.suffolk.litlab.efsp.db.DatabaseCreator; import edu.suffolk.litlab.efsp.server.logging.MDCWrappers; import edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeDatabase; -import edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeUpdater; +import edu.suffolk.litlab.efsp.tyler.ecfcodes.TylerCodeUpdater; + import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -40,7 +41,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { DatabaseCreator.makeSingleConnection(pgDb, pgFullUrl, pgUser, pgPassword); CodeDatabase cd = new CodeDatabase(jurisdiction, conn)) { success = - CodeUpdater.executeCommand(() -> cd, jurisdiction, List.of("refresh"), x509Password); + TylerCodeUpdater.executeCommand(() -> cd, jurisdiction, List.of("refresh"), x509Password); } catch (SQLException e) { log.error("Couldn't connect to Codes db from Job Executor: ", e); success = false; diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/Ecf4Helper.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/Ecf4Helper.java new file mode 100644 index 000000000..cf019a9fc --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/Ecf4Helper.java @@ -0,0 +1,338 @@ +package edu.suffolk.litlab.efsp.truefiling; + +import java.io.File; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Optional; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.namespace.QName; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gov.niem.niem.domains.jxdm._4.CourtType; +import gov.niem.niem.fips_10_4._2.CountryCodeType; +import gov.niem.niem.iso_4217._2.CurrencyCodeSimpleType; +import gov.niem.niem.niem_core._2.AmountType; +import gov.niem.niem.niem_core._2.DateType; +import gov.niem.niem.niem_core._2.EntityType; +import gov.niem.niem.niem_core._2.MeasureType; +import gov.niem.niem.niem_core._2.TextType; +import gov.niem.niem.proxy.xsd._2.Decimal; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.CaseFilingType; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.PersonType; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.QueryMessageType; + +public class Ecf4Helper { + private static final Logger log = LoggerFactory.getLogger(Ecf4Helper.class); + + static final gov.niem.niem.proxy.xsd._2.ObjectFactory niemProxyObjFac; + static final gov.niem.niem.niem_core._2.ObjectFactory niemCoreObjFac; + static final gov.niem.niem.domains.jxdm._4.ObjectFactory jxObjFac; + static final DatatypeFactory datatypeFac; + + /** + * One of the ways that you can communicate over ECF. For more information, see + * https://docs.oasis-open.org/legalxml-courtfiling/specs/ecf/v4.01/ecf-v4.01-spec/errata02/os/ecf-v4.01-spec-errata02-os-complete.html#_Toc425241629 + */ + public static final String MDE_PROFILE_CODE = + "urn:oasis:names:tc:legalxml-courtfiling:schema:xsd:WebServicesMessaging-2.0"; + + + static { + niemProxyObjFac = new gov.niem.niem.proxy.xsd._2.ObjectFactory(); + niemCoreObjFac = new gov.niem.niem.niem_core._2.ObjectFactory(); + jxObjFac = new gov.niem.niem.domains.jxdm._4.ObjectFactory(); + try { + datatypeFac = DatatypeFactory.newInstance(); + } catch (DatatypeConfigurationException e) { + throw new RuntimeException(e); + } + } + + /** Creates a date from a java date. Doesn't have time associated with it. */ + public static DateType convertDate(LocalDate date) { + GregorianCalendar cal = new GregorianCalendar(); + // TODO(#47): DEFAULT TIMEZONE IS WRONG: how should LocalDate + + // GregorianCalendar operate? + cal.set(date.getYear(), date.getMonthValue() - 1, date.getDayOfMonth(), 0, 0, 0); + + gov.niem.niem.proxy.xsd._2.Date d = niemProxyObjFac.createDate(); + XMLGregorianCalendar x = datatypeFac.newXMLGregorianCalendar(cal); + x.setTimezone(DatatypeConstants.FIELD_UNDEFINED); + d.setValue(x); + + DateType dt = niemCoreObjFac.createDateType(); + dt.setDateRepresentation(niemCoreObjFac.createDate(d)); + return dt; + } + + public static LocalDate parseDate(DateType dt) { + var d = ((gov.niem.niem.proxy.xsd._2.Date) dt.getDateRepresentation().getValue()).getValue(); + return LocalDate.of(d.getYear(), d.getMonth(), d.getDay()); + } + + /** Niem's Boolean type: either true or false. */ + public static gov.niem.niem.proxy.xsd._2.Boolean convertBool(boolean bool) { + gov.niem.niem.proxy.xsd._2.Boolean val = + new gov.niem.niem.proxy.xsd._2.Boolean(); + val.setValue(bool); + return val; + } + + public static Optional getNonEmptyText(TextType text) { + if (text != null && text.getValue() != null && !text.getValue().isBlank()) { + return Optional.of(text.getValue()); + } + return Optional.empty(); + } + + /** + * Converts a Java string to NIEM "Text", a wrapper around the NIEM String. + * + *

"A data type for a character string" + */ + public static TextType convertText(String str) { + TextType tt = niemCoreObjFac.createTextType(); + tt.setValue(str); + return tt; + } + + /** + * Converts a Java string to NIEM's XML String. "A datatype for character strings in XML." + * Practically, just a xsd:string with the extra SimpleObjectAttributeGroup attributes. + */ + public static gov.niem.niem.proxy.xsd._2.String convertString(String str) { + var outStr = niemProxyObjFac.createString(); + outStr.setValue(str); + return outStr; + } + + /** + * @param type will always be in bytes, but this is for backup. + */ + public static long sizeMeasureAsBytes(MeasureType type) { + String measureText = ((TextType) type.getMeasureValue().getValue()).getValue(); + long measureNum = Long.parseLong(measureText); + String measureUnit = type.getMeasureUnitText().getValue(); + if (measureUnit.equalsIgnoreCase("byte")) { + return measureNum; + } else if (measureUnit.equalsIgnoreCase("kilobyte") || measureUnit.equals("kB")) { + return measureNum * 1000; + } else if (measureUnit.equals("KB") || measureUnit.equals("KiB")) { + return measureNum * 1024; + } else if (measureUnit.equals("MB")) { + return measureNum * 1000 * 1000; + } else { + return measureNum; + } + } + + public static gov.niem.niem.niem_core._2.IdentificationType convertId(String idStr) { + var id = niemCoreObjFac.createIdentificationType(); + id.setIdentificationID(convertString(idStr)); + return id; + } + + public static gov.niem.niem.niem_core._2.IdentificationType convertId( + String idStr, String category) { + var id = niemCoreObjFac.createIdentificationType(); + id.setIdentificationID(convertString(idStr)); + id.setIdentificationCategory( + niemCoreObjFac.createIdentificationCategoryText(convertText(category))); + return id; + } + + public static Decimal convertDecimal(BigDecimal deci) { + Decimal xml = new Decimal(); + xml.setValue(deci); + return xml; + } + + /** + * Convert an int to a BigDecimal to an XML Decimal + * + *

Weird conversion, but the multiplier on optional services is a Decimal for some reason? "I + * need 2.3 copies of this sent to my office", lmao. + * + * @param val + * @return + */ + public static Decimal convertDecimal(int val) { + return convertDecimal(new BigDecimal(val)); + } + + /** Converts a Java string to a URI. */ + public static gov.niem.niem.proxy.xsd._2.AnyURI convertUri(String uri) { + var anyUri = niemProxyObjFac.createAnyURI(); + anyUri.setValue(uri); + return anyUri; + } + + public static gov.niem.niem.proxy.xsd._2.Base64Binary convertBase64( + final byte[] rawContent) { + var binaryString = niemProxyObjFac.createBase64Binary(); + // We don't need to encode Base64? For some strange reason, JAXB does it for us. + // See https://stackoverflow.com/a/7224025 + // binaryString.setValue(Base64.getEncoder().encode(rawContent)); + binaryString.setValue(rawContent); + return binaryString; + } + + public static CourtType convertCourtType(String courtId) { + CourtType court = jxObjFac.createCourtType(); + court.setOrganizationIdentification(Ecf4Helper.convertId(courtId)); + return court; + } + + public static String amountToString(AmountType amt) { + if (amt == null) { + return ""; + } + var currencyCode = amt.getCurrencyCode(); + String valStr = amt.getValue().toString(); + if (currencyCode == null) { + return valStr; + } + // TODO(brycew): more internationalization + if (currencyCode.equals(CurrencyCodeSimpleType.USD)) { + return "$" + valStr; + } else { + return valStr + " " + currencyCode.toString(); + } + } + + /** + * Converts any XML annotated object (from CXF) to a string. Useful for debugging. Doesn't throw, + * but does return the string of the exception instead. + * + * @param the type of object being passed in + * @param toXml The object to do things with + * @return the XML string to do what you want with + */ + public static String objectToXmlStrOrError(T toXml, Class toXmlClazz) { + try { + return objectToXmlStr(toXml, toXmlClazz); + } catch (JAXBException | NullPointerException ex) { + return ex.toString() + "(original obj was : " + toXml.toString() + ")"; + } + } + + /** + * Converts any XML annotated object (from CXF) to a string. Useful for testing. Will throw an + * exception if there's a JAXB error. + * + * @param the type of object being passed in + * @param toXml The object to do things with + * @return the XML string to do what you want with + */ + public static String objectToXmlStr(T toXml, Class toXmlClazz) throws JAXBException { + JAXBContext jaxContext = + JAXBContext.newInstance( + toXmlClazz, + gov.niem.niem.niem_core._2.ObjectFactory.class, + gov.niem.niem.structures._2.ObjectFactory.class, + oasis.names.tc.legalxml_courtfiling.schema.xsd.corefilingmessage_4 + .ObjectFactory.class, + oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.ObjectFactory + .class + /* ecfv5.https.docs_oasis_open_org.legalxml_courtfiling.ns.v5_0.ecf.ObjectFactory.class, + ecfv5.https.docs_oasis_open_org.legalxml_courtfiling.ns.v5_0.civil.ObjectFactory.class, + ecfv5.gov.niem.release.niem.niem_core._4.ObjectFactory.class, + ecfv5.gov.niem.release.niem.domains.jxdm._6.ObjectFactory.class*/ ); + Marshaller mar = jaxContext.createMarshaller(); + mar.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + QName qname = new QName("suffolk.test.objectToXml", "objectToXml"); + JAXBElement wrappedRoot = new JAXBElement(qname, toXmlClazz, toXml); + StringWriter sw = new StringWriter(); + mar.marshal(wrappedRoot, sw); + return sw.toString(); + } + + /** + * Converts any XML annotated object (from CXF) to a file. Useful for testing larger XML objects + * that are unwieldly to print. Will throw an exception if there's a JAXB error. + * + * @param the type of object being passed in + * @param toXml The object to do things with + */ + public static void objectToXmlFile(T toXml, Class toXmlClazz, File outfile) + throws JAXBException { + JAXBContext jc = + JAXBContext.newInstance( + gov.niem.niem.niem_core._2.ObjectFactory.class, + gov.niem.niem.structures._2.ObjectFactory.class, + oasis.names.tc.legalxml_courtfiling.schema.xsd.corefilingmessage_4 + .ObjectFactory.class, + oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.ObjectFactory + .class); + Marshaller mar = jc.createMarshaller(); + mar.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + QName qname = new QName("suffolk.test.objectToXml", "objectToXml"); + JAXBElement pp = new JAXBElement(qname, toXmlClazz, toXml); + mar.marshal(pp, outfile); + } + + /** Turn a given string into a country type. */ + public static CountryCodeType strToCountryCode(String country) { + CountryCodeType cct = new CountryCodeType(); + cct.setValue(country); + return cct; + } + + public static T prep(T newMsg, String courtId, String locationId) { + EntityType typ = new EntityType(); + var commonObjFac = + new oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4 + .ObjectFactory(); + JAXBElement elem2 = commonObjFac.createEntityPerson(new PersonType()); + typ.setEntityRepresentation(elem2); + newMsg.setQuerySubmitter(typ); + newMsg.setCaseCourt(Ecf4Helper.convertCourtType(courtId)); + // Example in the ECF4 docs is "https://filingreviewmde.com", which matches best to + // EXTERNAL_URL. + // Doesn't seem to be used by Tyler however. + newMsg.setSendingMDELocationID(Ecf4Helper.convertId(locationId)); + newMsg.setSendingMDEProfileCode(MDE_PROFILE_CODE); + return newMsg; + } + + public record Error(String code, String text) {} + + /** + * Returns the error type on errors from the ECF side of the API. They work the same as the Tyler + * ones. + */ + public static Optional checkError( + oasis.names.tc.legalxml_courtfiling.schema.xsd.commontypes_4.ErrorType error) { + var errCode = error.getErrorCode(); + if (errCode != null && !errCode.getValue().equals("0")) { + log.error("Error!: {}: {}", errCode.getValue(), error.getErrorText().getValue()); + return Optional.of(new Error(errCode.getValue(), error.getErrorText().getValue())); + } + return Optional.empty(); + } + + public static List checkErrors( + List + errors) { + return errors.stream().flatMap(err -> checkError(err).stream()).toList(); + } + + public static void setupReplys(CaseFilingType reply, String locationId) { + reply.setSendingMDELocationID(Ecf4Helper.convertId(locationId)); + reply.setSendingMDEProfileCode(MDE_PROFILE_CODE); + } + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/AllowanceChargeReason.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/AllowanceChargeReason.java new file mode 100644 index 000000000..9d4dc37d8 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/AllowanceChargeReason.java @@ -0,0 +1,6 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +public record AllowanceChargeReason(String code, String description) { + // TODO: finish + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseCategory.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseCategory.java new file mode 100644 index 000000000..f43c8d58a --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseCategory.java @@ -0,0 +1,130 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import edu.suffolk.litlab.efsp.ecf4.EcfCaseTypes; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseUtils.Column; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Heavily based on {@link edu.suffolk.litlab.efsp.tyler.ecfcodes.CaseCategory}, but replacing name + * with description. + */ +public record CaseCategory( + /** Actually a string, different from Tyler's numerical codes. */ + String code, + /* Description to show user*/ + String description, + /* which ECF Case Type to make for this Case Category */ + EcfCaseTypes ecfCaseType) { + + public CaseCategory(String code, String description, String ecfCaseType) { + this(code, description, EcfCaseTypes.valueOf(ecfCaseType)); + } + + public CaseCategory(ResultSet rs) throws SQLException { + this(rs.getString(2), rs.getString(3), EcfCaseTypes.valueOf(rs.getString(4))); + } + + public static final String TABLE_NAME = "truefiling_casecategory"; + public static final List columns = + List.of( + new Column("code", "text"), + new Column("descirption", "text"), + new Column("ecfcasetype", "text")); + + public static final String CREATE_QUERY = + """ + CREATE TABLE truefiling_casecategory ( + "code" text PRIMARY KEY, + "description" text, + "ecfcasetype" text, + "location" varchar(80), + "jurisdiction" varchar(80) + ) + """; + + public static final String INSERT_QUERY = + """ + INSERT INTO truefiling_casecategory ( + "code", "description", "ecfcasetype", "location", "jurisdiction" + ) VALUES ( + ?, ?, ?, ?, ? + ) + """; + + public static final String DELETE_FROM_QUERY = + """ + DELETE FROM truefiling_casecategory WHERE jurisdiction=? AND location=? + """; + + public static final String DELETE_ALL_COURTS_FROM_QUERY = + """ + DELETE FROM truefiling_casecategory WHERE jurisdiction=? + """; + + public static String searchCaseCategories() { + return """ + SELECT DISTINCT description + FROM truefiling_casecategory + WHERE jurisdiction=? AND description ILIKE ? + ORDER BY description + """; + } + + public static String courtCoverageCaseCategories() { + return """ + SELECT DISTINCT location + FROM truefiling_casecategory + WHERE jurisdiction=? AND description ILIKE ? + ORDER BY location + """; + } + + public static String retrieveCaseCategoryForName() { + return """ + SELECT DISTINCT code, location + FROM truefiling_casecategory + WHERE jurisdiction=? AND description=? + ORDER BY location + """; + } + + // TODO(#86): stop filtering out criminal categories + public static String getCaseCategoriesForLoc() { + return """ + SELECT code, description, ecfcasetype + FROM truefiling_casecategory + WHERE jurisdiction=? AND location=? AND ecfcasetype !='CriminalCase' + """; + } + + public static String getFileableCaseCategoryForLoc() { + return """ + SELECT cat.code, cat.description, cat.ecfcasetype + FROM ( + SELECT cate.code, cate.description, cate.ecfcasetype, + type.id AS type_id, + ROW_NUMBER() OVER(PARTITION BY cate.code ORDER BY type.id) AS RN + FROM truefiling_casecategory AS cate + INNER JOIN truefiling_casetype AS type + ON cate.code = type.casecategory AND cate.location = type.location AND cate.jurisdiction = type.jurisdiction + WHERE cate.jurisdiction=? AND cate.location=? AND cate.ecfcasetype != 'CriminalCase' + ) cat + WHERE cat.RN = 1 + """; + } + + public static String getFileableCaseCategoryForTiming() { + // TODO: just ignore the timing param higher up + return getFileableCaseCategoryForLoc(); + } + + public static String getCaseCategoryWithCode() { + return """ + SELECT code, description, ecfcasetype + FROM truefiling_casecategory + WHERE jurisdiction=? AND location=? AND code=? + """; + } +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseType.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseType.java new file mode 100644 index 000000000..46ea6629b --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CaseType.java @@ -0,0 +1,128 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +public record CaseType(UUID id, String code, String description, String caseCategoryCode) { + public CaseType(String id, String code, String description, String caseCategoryCode) { + this(UUID.fromString(id), code, description, caseCategoryCode); + } + + public CaseType(ResultSet rs) throws SQLException { + this((UUID) rs.getObject(1), rs.getString(2), rs.getString(3), rs.getString(4)); + } + + public static final String TABLE_NAME = "truefiling_casetype"; + + public static final String CREATE_QUERY = + """ + CREATE TABLE truefiling_casetype ( + "id" uuid PRIMARY KEY, + "code" text, + "description" text, + "casecategory" text, + "location" varchar(80), + "jurisdiction" varchar(80) + ) + """; + + public static final String INSERT_QUERY = + """ + INSERT INTO truefiling_casetype ( + "id", "code", "description", "casecategory", "location", "jurisdiciton" + ) VALUES ( + ?, ?, ?, ?, ? + ) + """; + + public static final String DELETE_FROM_QUERY = + """ + DELETE FROM truefiling_casetype WHERE jurisdiction=? AND location=? + """; + + public static final String DELETE_ALL_COURTS_FROM_QUERY = + """ + DELETE FROM truefiling_casetype WHERE jurisdiction=? + """; + + public static PreparedStatement prepQuery( + Connection conn, String jurisdiction, String courtLocationId, String caseCategoryCode) + throws SQLException { + String query = + """ + SELECT id, code, description, casecategory + FROM truefiling_casetype + WHERE jurisdiction=? AND location=? AND casecategory=? + """; + PreparedStatement st = conn.prepareStatement(query); + st.setString(1, jurisdiction); + st.setString(2, courtLocationId); + st.setString(3, caseCategoryCode); + return st; + } + + public static PreparedStatement prepQueryWithCode( + Connection conn, String jurisdiction, String courtLocationId, UUID caseTypeId) + throws SQLException { + String withcode = + """ + SELECT id, code, description, casecategory + FROM truefiling_casetype + WHERE jurisdiction=? AND location=? AND id=? + """; + PreparedStatement st = conn.prepareStatement(withcode); + st.setString(1, jurisdiction); + st.setString(2, courtLocationId); + st.setObject(3, caseTypeId); + + return st; + } + + public static PreparedStatement prepSearchQuery( + Connection conn, String jurisdiction, String searchTerm) throws SQLException { + String search = + """ + SELECT DISTINCT description + FROM casetype + WHERE jurisdiction=? AND description ILIKE ? + ORDER BY description + """; + PreparedStatement st = conn.prepareStatement(search); + st.setString(1, jurisdiction); + st.setString(2, searchTerm); + return st; + } + + public static PreparedStatement prepCourtCoverage( + Connection conn, String jurisdiction, String searchTerm) throws SQLException { + String search = + """ + SELECT DISTINCT location + FROM casetype + WHERE jurisdiction=? AND name ILIKE ? + ORDER BY location + """; + PreparedStatement st = conn.prepareStatement(search); + st.setString(1, jurisdiction); + st.setString(2, searchTerm); + return st; + } + + public static PreparedStatement prepRetrieveQuery( + Connection conn, String jurisdiction, String caseTypeName) throws SQLException { + String retrieve = + """ + SELECT DISTINCT code, location + FROM casetype + WHERE jurisdiction=? AND name=? + ORDER BY location + """; + PreparedStatement st = conn.prepareStatement(retrieve); + st.setString(1, jurisdiction); + st.setString(2, caseTypeName); + return st; + } +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CodeTableConstants.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CodeTableConstants.java new file mode 100644 index 000000000..4ddd87951 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/CodeTableConstants.java @@ -0,0 +1,73 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import java.util.List; +import java.util.Map; + +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseUtils.TableColumns; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseUtils.UnsupportedTableException; + +/** + * Court codes in Alaska are very long lived and don't change often. + * Keeping them here for the moment. Marking other places where this + * matters with TODO(ak-no-tables). + */ +public class CodeTableConstants { + + private static final Map tableColumns; + + static { + tableColumns = Map.ofEntries( + Map.entry(CaseCategory.TABLE_NAME, new TableColumns(CaseCategory.columns, List.of(), false)) + ); + } + + public static final List categories = List.of( + new CaseCategory("appellate", "Appellate", "AppellateCase"), + new CaseCategory("bankruptcy", "Bankruptcy", "BankruptcyCase"), + new CaseCategory("citation", "Citation", "CitationCase"), + new CaseCategory("civil", "Civil", "CivilCase"), + new CaseCategory("criminal", "Criminal", "CriminalCase"), + new CaseCategory("domestic", "Domestic", "DomesticCase"), + new CaseCategory("juvenile", "Juvenile", "JuvenileCase") + ); + + public static String getCreateTable(String tableName) throws UnsupportedTableException { + if (createQueries.containsKey(tableName)) { + return createQueries.get(tableName); + } + throw new UnsupportedTableException("No create query for table " + tableName); + } + + private static final Map createQueries = Map.of( + CaseCategory.TABLE_NAME, CaseCategory.CREATE_QUERY, + CaseType.TABLE_NAME, CaseType.CREATE_QUERY + ); + + + private static final Map insertQueries = Map.of( + CaseCategory.TABLE_NAME, CaseCategory.INSERT_QUERY, + CaseType.TABLE_NAME, CaseType.INSERT_QUERY + ); + + private static final Map deleteFromQueries = Map.of( + CaseCategory.TABLE_NAME, CaseCategory.DELETE_FROM_QUERY, + CaseType.TABLE_NAME, CaseType.DELETE_FROM_QUERY + ); + + private static final Map deleteAllCourtsFromQueries = Map.of( + CaseCategory.TABLE_NAME, CaseCategory.DELETE_ALL_COURTS_FROM_QUERY, + CaseType.TABLE_NAME, CaseType.DELETE_ALL_COURTS_FROM_QUERY + ); + + public static String getInsertInto(String tableName) throws UnsupportedTableException { + if (!insertQueries.containsKey(tableName)) { + throw new UnsupportedTableException("No insert query for table " + tableName); + } + return insertQueries.get(tableName); + } + + public static TableColumns getTableColumns(String tableName) { + return tableColumns.getOrDefault(tableName, new TableColumns(List.of(), List.of(), false)); + } + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentStatus.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentStatus.java new file mode 100644 index 000000000..ee9aaebca --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentStatus.java @@ -0,0 +1,9 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +// TODO: what to do with this one? no description or other +// information that isn't present in the code itself? might +// be able to just ignore +public record DocumentStatus(String code) { + // TODO: finish + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentType.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentType.java new file mode 100644 index 000000000..abf1a697b --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/DocumentType.java @@ -0,0 +1,15 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import java.util.UUID; + +// is per court +public record DocumentType( + UUID id, + UUID caseTypeId, + String description, + // TODO: idk what this is used for, not referenced elsewhere in the docs + String filingServiceType, + boolean courtUseOnly) { + + // TODO: finish +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/ErrorCode.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/ErrorCode.java new file mode 100644 index 000000000..418302049 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/ErrorCode.java @@ -0,0 +1,7 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +// System-wide code list +public record ErrorCode(String code, String name) { + // TODO: finish + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/FeeExceptionReason.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/FeeExceptionReason.java new file mode 100644 index 000000000..4abe22e9a --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/FeeExceptionReason.java @@ -0,0 +1,13 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import java.util.UUID; + +// Is per court +public record FeeExceptionReason( + UUID id, + String description, + // TODO: make this a URL? will that be too strict? + String formLink) { + // TODO: finish class + +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabase.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabase.java new file mode 100644 index 000000000..e5e45f55e --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabase.java @@ -0,0 +1,269 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import edu.suffolk.litlab.efsp.Jurisdiction; +import edu.suffolk.litlab.efsp.ecfcodes.CodeAndLocation; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseAPI; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseUtils; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDocException; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDocIterator; +import edu.suffolk.litlab.efsp.ecfcodes.NameAndCode; +import java.io.InputStream; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TFCodeDatabase extends CodeDatabaseAPI { + private static final Logger log = LoggerFactory.getLogger(TFCodeDatabase.class); + + private final Jurisdiction jurisdiction; + + public TFCodeDatabase(Jurisdiction jurisdiction, Connection conn) { + super(conn); + this.jurisdiction = jurisdiction; + } + + public static TFCodeDatabase fromDS(Jurisdiction jurisdiction, DataSource ds) { + try { + TFCodeDatabase cd = new TFCodeDatabase(jurisdiction, ds.getConnection()); + return cd; + } catch (SQLException e) { + log.error("In TFCodeDatabase constructor, can't get connection: ", e); + throw new RuntimeException(e); + } + } + + @Override + public Jurisdiction getJurisdiction() { + return jurisdiction; + } + + // TODO: should this be in the parent class? + private String jurisStr() { + return jurisdiction.getName(); + } + + private static final Map ecf4Map = + Map.of( + "nc:CaseCategoryText", "truefiling_casecategory", + "nc:DocumentCategoryText", "truefiling_documentcategory", + "j:RegisterActionDescriptionText", "truefiling_registeraction", + "nc:BinaryFormatStandardName", "truefiling_binaryformat", + "cbc:AccountingCostCode", "truefiling_accountingcost", + "FeeExceptionReasonCode", "truefiling_feeexception", + "ecf:FilingStatusCode", "truefiling_filingstatus"); + + @Override + public Map xmlElemToTableName() { + return ecf4Map; + } + + @Override + public Set systemTables() { + return Set.of(); + } + + @Override + public List getAllLocations() { + // Only AK Trial Court + return List.of("55da5b11-2bc4-4881-abd1-b0dfdb506bb1"); + } + + @Override + public void updateTable(String tableName, String courtName, InputStream is) + throws SQLException, CodeDocException { + if (conn == null) { + throw new SQLException("Null connection!"); + } + validTable(tableName); + createTableIfAbsent(tableName); + // TODO: also handle indicies + // createIndicesIfAbsent(tableName); + + CodeDocIterator iterator = CodeDocIterator.setup(is); + updateTable(tableName, courtName, iterator.getVersion(), iterator); + } + + @Override + public void updateTable( + String tableName, String courtName, String newVersion, Iterator> rows) + throws SQLException { + // Ignore version here, they don't use that. + String insertQuery = CodeTableConstants.getInsertInto(tableName); + try (PreparedStatement stmt = conn.prepareStatement(insertQuery)) { + while (rows.hasNext()) { + Map rowsVals = rows.next(); + CodeDatabaseUtils.singleInsert( + stmt, CodeTableConstants.getTableColumns(tableName), rowsVals, courtName, jurisStr()); + stmt.addBatch(); + } + stmt.executeBatch(); + } + } + + @Override + public boolean deleteFromTable(String tableName) throws SQLException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteFromTable'"); + } + + @Override + public boolean deleteFromTable(String tableName, String courtLocation) throws SQLException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteFromTable'"); + } + + @Override + public void createTableIfAbsent(String tableName) throws SQLException { + boolean exists = tableExists(tableName); + if (!exists) { + try (Statement createSt = conn.createStatement()) { + createSt.executeUpdate(CodeTableConstants.getCreateTable(tableName)); + } + } + } + + @Override + public Map> getVersionsToUpdate() throws SQLException { + // TODO: just redownload the whole thing + return Map.of(getAllLocations().get(0), + ecf4Map.values().stream().toList()); + } + + @Override + public void vacuumAll() { + try { + String vacuum = "VACUUM ANALYZE"; + try (PreparedStatement vacuumSt = conn.prepareStatement(vacuum)) { + log.info("Full vacuum statement: {}", vacuumSt); + vacuumSt.executeUpdate(); + } + } catch (SQLException ex) { + log.error("Error when vacuuming in {}", this.jurisdiction, ex); + } + } + + @Override + public List searchCaseCategory(String searchTerm) { + return genericSearch( + searchTerm, + (term) -> { + String query = CaseCategory.searchCaseCategories(); + PreparedStatement st = conn.prepareStatement(query); + st.setString(1, jurisStr()); + st.setString(2, term); + return st; + }); + } + + @Override + public List courtCoverageCaseCategory(String searchTerm) { + return genericSearch( + searchTerm, + (term) -> { + String query = CaseCategory.courtCoverageCaseCategories(); + PreparedStatement st = conn.prepareStatement(query); + st.setString(1, jurisStr()); + st.setString(2, term); + return st; + }); + } + + @Override + public List retrieveCaseCategoryByName(String categoryName) { + return safetyWrap( + () -> { + String query = CaseCategory.retrieveCaseCategoryForName(); + List cats = new ArrayList<>(); + try (PreparedStatement st = conn.prepareStatement(query)) { + st.setString(1, jurisStr()); + st.setString(2, categoryName); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + cats.add(new CodeAndLocation(rs.getString(1), rs.getString(2))); + } + } + return cats; + }); + } + + @Override + public List getCaseCategoryNames(String courtLocationId) { + // courtLocation is unused. + return safetyWrap( + () -> { + String query = CaseCategory.getCaseCategoriesForLoc(); + List cats = new ArrayList<>(); + try (PreparedStatement st = conn.prepareStatement(query)) { + st.setString(1, jurisStr()); + st.setString(2, courtLocationId); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + cats.add(new NameAndCode(rs.getString(2), rs.getString(1))); + } + } + return cats; + }); + } + + @Override + public List searchCaseType(String searchTerm) { + return genericSearch(searchTerm, (term) -> CaseType.prepSearchQuery(conn, jurisStr(), term)); + } + + @Override + public List courtCoverageCaseType(String searchTerm) { + return genericSearch(searchTerm, (term) -> CaseType.prepCourtCoverage(conn, jurisStr(), term)); + } + + @Override + public List retrieveCaseTypeByName(String caseTypeName) { + return safetyWrap( + () -> { + try (PreparedStatement st = CaseType.prepRetrieveQuery(conn, jurisStr(), caseTypeName)) { + List types = new ArrayList<>(); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + types.add(new CodeAndLocation(rs.getString(1), rs.getString(2))); + } + return types; + } + }); + } + + @Override + public List getCaseTypeNamesFor( + String courtLocationId, String caseCategoryCode, Optional initial) { + return safetyWrap( + () -> { + // initial is ignored + PreparedStatement st = + CaseType.prepQuery(conn, jurisStr(), courtLocationId, caseCategoryCode); + ResultSet rs = st.executeQuery(); + List nacs = new ArrayList<>(); + // TODO(bryce): can be more efficient, just grabbing the name and code and not all of the + // other data. + while (rs.next()) { + nacs.add(new NameAndCode(rs.getString(2), rs.getString(1))); + } + st.close(); + return nacs; + }); + } + + @Override + public void createTablesIfAbsent() throws SQLException { + createTableIfAbsent(CaseCategory.TABLE_NAME); + } +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TrueFilingCodeUpdater.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TrueFilingCodeUpdater.java new file mode 100644 index 000000000..aa48b41dd --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TrueFilingCodeUpdater.java @@ -0,0 +1,104 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import edu.suffolk.litlab.efsp.Jurisdiction; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseAPI; +import edu.suffolk.litlab.efsp.ecfcodes.CodeUpdater; +import edu.suffolk.litlab.efsp.ecfcodes.CodeUpdater.CourtCodelistInfo; +import edu.suffolk.litlab.efsp.server.logging.MDCWrappers; +import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; +import edu.suffolk.litlab.efsp.truefiling.Ecf4Helper; +import edu.suffolk.litlab.efsp.truefiling.SoapClientChooser; +import imagesoft.ecf.wsdl.webservicesprofile_implementation_4_0.FilingReviewMDEService; +import jakarta.xml.ws.soap.SOAPFaultException; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyquerymessage_4.CourtPolicyQueryMessageType; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyresponsemessage_4.CourtCodelistType; +import oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyresponsemessage_4.CourtPolicyResponseMessageType; + +public class TrueFilingCodeUpdater { + + public static final Logger log = LoggerFactory.getLogger(TrueFilingCodeUpdater.class); + + public static boolean executeCommand( + Supplier cdSupplier, + List args) { + String command = args.get(0); + CodeUpdater cu = new CodeUpdater(); + // DownloadIndiv doesn't connect to the database, just signs the req for the zip + if (command.equalsIgnoreCase("downloadAll")) { + try { + return cu.downloadIndiv(args, (table, loc) -> makeUrl(endpoint, table, loc)); + } catch (URISyntaxException e) { + log.error("Exception when doing code updating! ", e); + return false; + } + } + + try (CodeDatabaseAPI cd = cdSupplier.get()) { + var filingPort = SoapClientChooser.getFilingReviewFactory(Jurisdiction.ALASKA).getFilingReviewMDEPort(); + Function> codelistGetter = (location) -> { + var m = Ecf4Helper.prep(new CourtPolicyQueryMessageType(), location, ServiceHelpers.EXTERNAL_URL); + try { + CourtPolicyResponseMessageType p = filingPort.getPolicy(m); + var the_stream = p.getRuntimePolicyParameters().getCourtCodelist().stream(); + Function ahh = (cc1) -> { + return new CourtCodelistInfo( + cc1.getECFElementName().getValue(), + cc1.getCourtCodelistURI().getIdentificationID().getValue(), Ecf4Helper.parseDate(cc1.getEffectiveDate())); + }; + List the_list = the_stream.map(ahh).toList(); + return the_list; + } catch (SOAPFaultException ex) { + log.warn( + "Got a SOAP excption getting policy for {} in {}: ", location, Jurisdiction.ALASKA, ex); + return List.of(); + } + }; + var systemUrls = cd.systemTables().stream().collect(Collectors.toMap((t) -> t, (t) -> makeUrl(codesSite, t, ""))); + + if (command.equalsIgnoreCase("replaceall")) { + return cu.replaceAll(systemUrls, codelistGetter, cd); + } else if (command.equalsIgnoreCase("replacesome")) { + return cu.replaceSome( + systemUrls, codelistGetter, cd, args.subList(1, args.size())); + } else { + log.error("Command {} isn't a real command", command); + return false; + } + } catch (SQLException | IOException | JAXBException | URISyntaxException e) { + log.error("Exception when doing code updating! ", e); + return false; + } + } + + /** + * Run with: + * + *

+   * java -cp efspserver-with-deps.jar edu.suffolk.litlab.efsp.truefiling.ecfcodes.CodeUpdater refresh
+   * 
+ */ + public static void main(String[] args) throws Exception { + if (args.length < 1) { + log.error("Need to pass in a subprogram: downloadIndiv, or refresh"); + System.exit(1); + } + + MDC.put(MDCWrappers.USER_ID, Jurisdiction.ALASKA.getName()); + // Reusing USER for Jurisdiction, SESSION for the court / location, and REQUEST for the table + // name. + executeCommand( + () -> CodeUpdater.makeCodeDatabase((ds) -> TFCodeDatabase.fromDS(jurisdiction, ds)), + List.of(args)); + } +} diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeDatabase.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeDatabase.java index cf6257270..76549f712 100644 --- a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeDatabase.java +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/CodeDatabase.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; + import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,11 +100,20 @@ public CodeDatabase(Jurisdiction jurisdiction, Connection conn) { Map.entry("tyler:DataFieldConfigCode", "datafieldconfig"), Map.entry("tyler:DocumentOptionalService", "optionalservices")); + // NOTE: the Tyler docs say "error" is available from `GetPolicy'. That is wrong. + private static final Set systemwideTables = Set.of("version", "location", "error"); + @Override public Map xmlElemToTableName() { return ecf4Map; } + @Override + public Set systemTables() { + return systemwideTables; + } + + public static CodeDatabase fromDS(Jurisdiction jurisdiction, DataSource ds) { try { CodeDatabase cd = new CodeDatabase(jurisdiction, ds.getConnection()); diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUpdater.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUpdater.java new file mode 100644 index 000000000..af0c87629 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUpdater.java @@ -0,0 +1,173 @@ +package edu.suffolk.litlab.efsp.tyler.ecfcodes; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.cxf.headers.Header; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import edu.suffolk.litlab.efsp.tyler.TylerClients; +import edu.suffolk.litlab.efsp.tyler.TylerUserClient; +import edu.suffolk.litlab.efsp.tyler.SoapClientChooser; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyquerymessage_4.CourtPolicyQueryMessageType; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyresponsemessage_4.CourtCodelistType; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.schema.xsd.courtpolicyresponsemessage_4.CourtPolicyResponseMessageType; +import ecf4.latest.oasis.names.tc.legalxml_courtfiling.wsdl.webservicesprofile_definitions_4_0.FilingReviewMDEPort; +import ecf4.latest.tyler.efm.wsdl.webservicesprofile_implementation_4_0.FilingReviewMDEService; +import edu.suffolk.litlab.efsp.Jurisdiction; +import edu.suffolk.litlab.efsp.ecfcodes.CodeDatabaseAPI; +import edu.suffolk.litlab.efsp.ecfcodes.CodeUpdater; +import edu.suffolk.litlab.efsp.ecfcodes.CodeUpdater.CourtCodelistInfo; +import edu.suffolk.litlab.efsp.server.ecf4.Ecf4Helper; +import edu.suffolk.litlab.efsp.server.logging.MDCWrappers; +import edu.suffolk.litlab.efsp.server.utils.ServiceHelpers; +import edu.suffolk.litlab.efsp.server.utils.SoapX509CallbackHandler; +import edu.suffolk.litlab.efsp.tyler.TylerUserFactory; +import edu.suffolk.litlab.efsp.tyler.TylerUserNamePassword; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.ws.BindingProvider; +import jakarta.xml.ws.soap.SOAPFaultException; +import tyler.efm.latest.services.schema.authenticaterequest.AuthenticateRequestType; +import tyler.efm.latest.services.schema.authenticateresponse.AuthenticateResponseType; + +public class TylerCodeUpdater { + + private static final Logger log = LoggerFactory.getLogger(TylerCodeUpdater.class); + + /** Sets up the WSDL connection to Tyler, used for `getPolicy` to get the URL. */ + private static FilingReviewMDEPort loginWithTyler( + Jurisdiction jurisdiction, String userEmail, String userPassword) { + Optional userFactory = TylerClients.getEfmUserFactory(jurisdiction); + if (userFactory.isEmpty()) { + throw new RuntimeException("Can't find " + jurisdiction + " in Soap chooser for EFMUser"); + } + log.info("Getting filing factory for {}", jurisdiction); + Optional filingFactory = + SoapClientChooser.getFilingReviewFactory(jurisdiction); + if (filingFactory.isEmpty()) { + throw new RuntimeException( + "Can't find " + jurisdiction + " in Soap Chooser for filing review factory"); + } + TylerUserClient userPort = userFactory.get().makeUserClient(ServiceHelpers::setupServicePort); + AuthenticateRequestType authReq = new AuthenticateRequestType(); + authReq.setEmail(userEmail); + authReq.setPassword(userPassword); + AuthenticateResponseType authRes = userPort.authenticateUser(authReq); + List
headersList = TylerUserNamePassword.makeHeaderList(authRes); + FilingReviewMDEPort filingPort = filingFactory.get().getFilingReviewMDEPort(); + ServiceHelpers.setupServicePort((BindingProvider) filingPort); + Map ctx = ((BindingProvider) filingPort).getRequestContext(); + ctx.put(Header.HEADER_LIST, headersList); + return filingPort; + } + + private static String makeUrl(String endpoint, String tableName, String location) { + return endpoint + "CodeService/codes/" + tableName + "/" + location.replace(" ", "%20"); + } + + public static boolean executeCommand( + Supplier cdSupplier, + Jurisdiction jurisdiction, + List args, + String x509Password) { + SoapX509CallbackHandler.setX509Password(x509Password); + String command = args.get(0); + CodeUpdater cu = new CodeUpdater(System.getenv("PATH_TO_KEYSTORE"), x509Password); + // DownloadIndiv doesn't connect to the database, just signs the req for the zip + if (command.equalsIgnoreCase("downloadIndiv")) { + String endpoint = TylerClients.getTylerServerRootUrl(jurisdiction); + try { + return cu.downloadIndiv(args, (table, loc) -> makeUrl(endpoint, table, loc)); + } catch (URISyntaxException e) { + log.error("Exception when doing code updating! ", e); + return false; + } + } + + try (CodeDatabaseAPI cd = cdSupplier.get()) { + String codesSite = TylerClients.getTylerServerRootUrl(jurisdiction); + + FilingReviewMDEPort filingPort = + loginWithTyler( + jurisdiction, + System.getenv("TYLER_USER_EMAIL"), + System.getenv("TYLER_USER_PASSWORD")); + Function> codelistGetter = (location) -> { + var m = Ecf4Helper.prep(new CourtPolicyQueryMessageType(), location); + try { + CourtPolicyResponseMessageType p = filingPort.getPolicy(m); + var the_stream = p.getRuntimePolicyParameters().getCourtCodelist().stream(); + Function ahh = (cc1) -> { + return new CourtCodelistInfo( + cc1.getECFElementName().getValue(), + cc1.getCourtCodelistURI().getIdentificationID().getValue(), Ecf4Helper.parseDate(cc1.getEffectiveDate())); + }; + List the_list = the_stream.map(ahh).toList(); + return the_list; + } catch (SOAPFaultException ex) { + log.warn( + "Got a SOAP excption getting policy for {} in {}: ", location, jurisdiction, ex); + return List.of(); + } + }; + var systemUrls = cd.systemTables().stream().collect(Collectors.toMap((t) -> t, (t) -> makeUrl(codesSite, t, ""))); + + if (command.equalsIgnoreCase("replaceall")) { + return cu.replaceAll(systemUrls, codelistGetter, cd); + } else if (command.equalsIgnoreCase("replacesome")) { + return cu.replaceSome( + systemUrls, codelistGetter, cd, args.subList(1, args.size())); + } else if (command.equalsIgnoreCase("refresh")) { + return cu.updateAll(systemUrls, codelistGetter, cd); + } else { + log.error("Command {} isn't a real command", command); + return false; + } + } catch (SQLException | IOException | JAXBException | URISyntaxException e) { + log.error("Exception when doing code updating! ", e); + return false; + } + } + /** + * Run with: + * + *
+   * java -cp efspserver-with-deps.jar edu.suffolk.litlab.efsp.tyler.ecfcodes.CodeUpdater refresh
+   * 
+ * + *

TODO(#111): use with this System property and class to try to fix parallel unmarshalling + * -Djava.util.concurrent.ForkJoinPool.common.threadFactory=edu.suffolk.litlab.efsp.server.JAXBForkJoinWorkerThreadFactory + * \ https://stackoverflow.com/a/57551188/11416267 + */ + public static void main(String[] args) throws Exception { + if (args.length < 1) { + log.error("Need to pass in a subprogram: downloadIndiv, or refresh"); + System.exit(1); + } + + List jurisdictions = + Stream.of(System.getenv("TYLER_JURISDICTIONS").split(" ")) + .map(Jurisdiction::parse) + .toList(); + for (var jurisdiction : jurisdictions) { + MDC.put(MDCWrappers.USER_ID, jurisdiction.getName()); + // Reusing USER for Jurisdiction, SESSION for the court / location, and REQUEST for the table + // name. + executeCommand( + () -> CodeUpdater.makeCodeDatabase((ds) -> CodeDatabase.fromDS(jurisdiction, ds)), + jurisdiction, + List.of(args), + System.getenv("X509_PASSWORD")); + } + } +} \ No newline at end of file diff --git a/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUrlGetter.java b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUrlGetter.java new file mode 100644 index 000000000..f2aef3c94 --- /dev/null +++ b/proxyserver/src/main/java/edu/suffolk/litlab/efsp/tyler/ecfcodes/TylerCodeUrlGetter.java @@ -0,0 +1,54 @@ +package edu.suffolk.litlab.efsp.tyler.ecfcodes; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import edu.suffolk.litlab.efsp.ecfcodes.CodeUrlGetter; +import edu.suffolk.litlab.efsp.server.utils.HeaderSigner; + +public class TylerCodeUrlGetter implements CodeUrlGetter { + + + private HeaderSigner signer; + + public TylerCodeUrlGetter(HeaderSigner signer) { + this.signer = signer; + } + + @Override + public InputStream get(String url) throws IOException, URISyntaxException { + return getCodesZip(url, signer.signedCurrentTime()); + } + + @Override + public boolean refresh() { + return signer.refresh(); + } + + /** + * Either downloads the codes file from Tyler, or opens an already downloaded local zip file. + * + *

Code for HttpConnection: https://stackoverflow.com/a/1485730/11416267 + * + * @return InputStream + * @throws IOException + */ + private static InputStream getCodesZip(String toRead, String authHeader) + throws IOException, URISyntaxException { + if (toRead.startsWith("http://") || toRead.startsWith("https://")) { + URL url = (new URI(toRead)).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("tyl-efm-api", authHeader); + return conn.getInputStream(); + } else { + return new FileInputStream(toRead); + } + } + +} diff --git a/proxyserver/src/test/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabaseTest.java b/proxyserver/src/test/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabaseTest.java new file mode 100644 index 000000000..5c3fd2e47 --- /dev/null +++ b/proxyserver/src/test/java/edu/suffolk/litlab/efsp/truefiling/ecfcodes/TFCodeDatabaseTest.java @@ -0,0 +1,65 @@ +package edu.suffolk.litlab.efsp.truefiling.ecfcodes; + +import static org.assertj.core.api.Assertions.assertThat; + +import edu.suffolk.litlab.efsp.Jurisdiction; +import edu.suffolk.litlab.efsp.db.DatabaseCreator; +import edu.suffolk.litlab.efsp.db.DatabaseVersionTest; +import edu.suffolk.litlab.efsp.ecfcodes.NameAndCode; +import java.sql.Connection; +import java.sql.SQLException; +import net.jqwik.api.Tag; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.postgresql.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +@Tag("Docker") +public class TFCodeDatabaseTest { + private static Logger log = LoggerFactory.getLogger(TFCodeDatabaseTest.class); + + private static TFCodeDatabase cd; + + private static String COURT_LOCATION = "55da5b11-2bc4-4881-abd1-b0dfdb506bb1"; + private static String CASE_CATEGORY_TABLE = "truefiling_casecategory"; + + @Container + public static PostgreSQLContainer postgres = + new PostgreSQLContainer(DockerImageName.parse(DatabaseVersionTest.POSTGRES_DOCKER_NAME)); + + @BeforeAll + public static void setUp() throws SQLException { + postgres.start(); + Connection conn = + DatabaseCreator.makeSingleConnection( + postgres.getDatabaseName(), + postgres.getJdbcUrl(), + postgres.getUsername(), + postgres.getPassword()); + cd = new TFCodeDatabase(Jurisdiction.ALASKA, conn); + cd.createTablesIfAbsent(); + } + + @Test + public void testAllLocations() throws SQLException { + assertThat(cd.getAllLocations()).containsOnly(COURT_LOCATION); + } + + @Test + public void testCaseCategory() throws Exception { + loadFromXmls(); + assertThat(cd.getCaseCategoryNames(COURT_LOCATION)) + .contains(new NameAndCode("Bankruptcy", "bankruptcy"), new NameAndCode("Civil", "civil")); + } + + private void loadFromXmls() throws Exception { + cd.createTableIfAbsent(CASE_CATEGORY_TABLE); + String filename = "/genericode_examples/akcourt/case_category_test.gc.xml"; + log.info("Updating from file: {}", filename); + cd.updateTable( + CASE_CATEGORY_TABLE, COURT_LOCATION, this.getClass().getResourceAsStream(filename)); + } +} diff --git a/proxyserver/src/test/resources/genericode_examples/akcourt/case_category_test.gc.xml b/proxyserver/src/test/resources/genericode_examples/akcourt/case_category_test.gc.xml new file mode 100644 index 000000000..2db533e2c --- /dev/null +++ b/proxyserver/src/test/resources/genericode_examples/akcourt/case_category_test.gc.xml @@ -0,0 +1,111 @@ + + + + CaseCategoryCode + CaseCategoryCode + 1.0 + + + + ImageSoft TrueFiling EFM + + + + + + Code + + + + Description + + + + ECFCaseType + + + + CodeKey + + + + + + + appellate + + + Appellate + + + AppellateCase + + + + + bankruptcy + + + Bankruptcy + + + BankruptcyCase + + + + + citation + + + Citation + + + CitationCase + + + + + civil + + + Civil + + + CivilCase + + + + + criminal + + + Criminal + + + CriminalCase + + + + + domestic + + + Domestic + + + DomesticCase + + + + + juvenile + + + Juvenile + + + JuvenileCase + + + +