From 1f9fcacf26c3ec37170968aa3c6ef96ac7d789ff Mon Sep 17 00:00:00 2001 From: Cloud IX Team Date: Fri, 19 Jun 2026 03:29:53 -0700 Subject: [PATCH] Add new skill to help migrate weblogic monoliths to GCP. PiperOrigin-RevId: 934837142 --- .../EVAL.txtpb | 339 ++++++++ .../google-cloud-weblogic-migration/SKILL.md | 196 +++++ .../assets/example_advanced_jms_migration.md | 177 ++++ .../assets/example_caching_and_clustering.md | 158 ++++ .../assets/example_containerization.md | 135 +++ .../assets/example_ejb_migration.md | 194 +++++ .../assets/example_jms_migration.md | 183 ++++ .../assets/example_observability_migration.md | 149 ++++ .../assets/example_scheduling_and_timers.md | 146 ++++ .../assets/example_security_migration.md | 232 ++++++ .../example_servlet_filter_migration.md | 168 ++++ .../example_soap_webservices_migration.md | 162 ++++ .../references/analysis_guide.md | 153 ++++ .../references/classloading_and_packaging.md | 134 +++ .../references/decomposition_guide.md | 205 +++++ .../references/deployment_guide.md | 56 ++ .../references/distributed_transactions.md | 198 +++++ .../references/gcp_mapping.md | 375 +++++++++ .../references/legacy_remoting_and_jca.md | 204 +++++ .../references/migration_plan_guide.md | 263 ++++++ .../references/refactoring_guide.md | 206 +++++ .../references/refactoring_quarkus.md | 161 ++++ .../references/refactoring_spring.md | 192 +++++ .../references/sql_dialect_migration.md | 178 ++++ .../references/verification_guide.md | 59 ++ .../references/web_modernization.md | 193 +++++ .../references/weblogic_specific_apis.md | 264 ++++++ .../scripts/__init__.py | 2 + .../scripts/ast_parser/pom.xml | 27 + .../src/main/java/OpenRewriteAstAnalyzer.java | 700 ++++++++++++++++ .../scripts/build_parser.py | 263 ++++++ .../scripts/cli.py | 786 ++++++++++++++++++ .../scripts/clusterer.py | 644 ++++++++++++++ .../scripts/config_parser.py | 332 ++++++++ .../scripts/db_mapper.py | 348 ++++++++ .../scripts/domain_parser.py | 294 +++++++ .../scripts/generate_temp_pom.py | 196 +++++ .../scripts/import_analyzer.py | 254 ++++++ .../scripts/jar_analyzer.py | 186 +++++ .../scripts/lexical_normalizer.py | 218 +++++ .../scripts/requirements.txt | 1 + .../scripts/run_openrewrite.sh | 79 ++ .../scripts/static_analyzer.py | 698 ++++++++++++++++ 43 files changed, 10108 insertions(+) create mode 100644 skills/cloud/google-cloud-weblogic-migration/EVAL.txtpb create mode 100644 skills/cloud/google-cloud-weblogic-migration/SKILL.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_advanced_jms_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_caching_and_clustering.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_containerization.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_ejb_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_jms_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_observability_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_scheduling_and_timers.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_security_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_servlet_filter_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/assets/example_soap_webservices_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/analysis_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/classloading_and_packaging.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/decomposition_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/deployment_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/distributed_transactions.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/gcp_mapping.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/legacy_remoting_and_jca.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/migration_plan_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/refactoring_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/refactoring_quarkus.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/refactoring_spring.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/sql_dialect_migration.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/verification_guide.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/web_modernization.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/references/weblogic_specific_apis.md create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/__init__.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/pom.xml create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/src/main/java/OpenRewriteAstAnalyzer.java create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/build_parser.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/cli.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/clusterer.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/config_parser.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/db_mapper.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/domain_parser.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/generate_temp_pom.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/import_analyzer.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/jar_analyzer.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/lexical_normalizer.py create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/requirements.txt create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/run_openrewrite.sh create mode 100644 skills/cloud/google-cloud-weblogic-migration/scripts/static_analyzer.py diff --git a/skills/cloud/google-cloud-weblogic-migration/EVAL.txtpb b/skills/cloud/google-cloud-weblogic-migration/EVAL.txtpb new file mode 100644 index 0000000000..57e6acefee --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/EVAL.txtpb @@ -0,0 +1,339 @@ +# proto-file: learning/gemini/agents/evaluation/evalin/proto/eval_suite.proto +# proto-message: EvalSuite +# txtpbfmt: wrap_strings_after_newlines + +suite_name: "google-cloud-weblogic-migration" +timeout_seconds: 300 + +cases { + name: "ejb_cmp_to_spring_data_jpa" + prompt: + "Migrate the following WebLogic EJB 2.0 CMP Entity Bean representing a Patient to Spring Boot:\n" + "```java\n" + "package com.acme.medimed.entities;\n" + "import javax.ejb.EntityBean;\n" + "import javax.ejb.EntityContext;\n" + "public abstract class PatientEJB implements EntityBean {\n" + " public abstract Integer getPatientId();\n" + " public abstract void setPatientId(Integer id);\n" + " public abstract String getEmail();\n" + " public abstract void setEmail(String email);\n" + " public abstract String getFirstName();\n" + " public abstract void setFirstName(String name);\n" + " public abstract String getLastName();\n" + " public abstract void setLastName(String name);\n" + " \n" + " public Integer ejbCreate(String email, String firstName, String lastName) {\n" + " setEmail(email);\n" + " setFirstName(firstName);\n" + " setLastName(lastName);\n" + " return null;\n" + " }\n" + " public void ejbPostCreate(String email, String firstName, String lastName) {}\n" + " public void setEntityContext(EntityContext ctx) {}\n" + " public void unsetEntityContext() {}\n" + " public void ejbRemove() {}\n" + " public void ejbActivate() {}\n" + " public void ejbPassivate() {}\n" + " public void ejbLoad() {}\n" + " public void ejbStore() {}\n" + "}\n" + "```\n" + "We are migrating to Spring Data JPA on Google Cloud SQL for PostgreSQL.\n" + expectations: "The agent MUST convert the EJB CMP entity bean into a standard JPA `@Entity` class named `Patient`." + expectations: "The agent MUST map the primary key using `@Id` and configure it for PostgreSQL (e.g. `@GeneratedValue(strategy = GenerationType.IDENTITY)`)." + expectations: "The agent MUST remove all legacy EJB lifecycle methods (ejbCreate, ejbLoad, ejbStore, setEntityContext, etc.)." + expectations: "The agent MUST create a corresponding `PatientRepository` interface extending `org.springframework.data.jpa.repository.JpaRepository`." +} + +cases { + name: "jax_rpc_to_spring_rest_modernization" + prompt: + "Migrate this legacy WebLogic JAX-RPC SOAP web service to a modern Spring Boot REST Controller:\n" + "```java\n" + "package com.acme.medimed.webservices;\n" + "import java.rmi.RemoteException;\n" + "import com.acme.medimed.value.PatientVO;\n" + "\n" + "public class PatientWS {\n" + " private PatientService patientService; // Assume injected\n" + " \n" + " public PatientVO getPatientInfo(Integer id) throws RemoteException {\n" + " try {\n" + " return patientService.getPatient(id);\n" + " } catch (Exception e) {\n" + " throw new RemoteException(\"Failed to get patient\", e);\n" + " }\n" + " }\n" + "}\n" + "```\n" + "We also need to import a new utility library for JSON validation (`com.networknt.schema`) in the modernized service.\n" + expectations: "The agent MUST call the `scan_dependencies` tool to validate `com.networknt.schema` before generating code with the import." + expectations: "The agent converts the JAX-RPC service to a Spring `@RestController` mapped to `/api/patients`." + expectations: "The agent refactors `getPatientInfo` to a `@GetMapping` returning a `ResponseEntity` or standard DTO." + expectations: "The agent removes `RemoteException` and uses Spring's status codes (e.g. returning 404 if patient not found, or 500 with proper error mapping)." +} + +cases { + name: "struts_action_to_spring_rest_and_spa_decoupling" + prompt: + "Migrate this legacy Struts 1.1 Action class that handles patient profile updates:\n" + "```java\n" + "package com.acme.medimed.actions;\n" + "import org.apache.struts.action.Action;\n" + "import org.apache.struts.action.ActionForm;\n" + "import org.apache.struts.action.ActionForward;\n" + "import org.apache.struts.action.ActionMapping;\n" + "import javax.servlet.http.HttpServletRequest;\n" + "import javax.servlet.http.HttpServletResponse;\n" + "import com.acme.medimed.beans.PatientForm;\n" + "\n" + "public class UpdateProfileAction extends Action {\n" + " public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {\n" + " PatientForm pForm = (PatientForm) form;\n" + " PatientService patientService = (PatientService) ServiceLocator.getService(\"PatientService\");\n" + " patientService.updateProfile(pForm.getPatientId(), pForm.getEmail(), pForm.getPhone());\n" + " return mapping.findForward(\"success_profile_view\");\n" + " }\n" + "}\n" + "```\n" + expectations: "The agent refactors the Struts Action into a Spring `@RestController` endpoint mapped to HTTP PUT or POST." + expectations: "The agent replaces the Struts `ActionForm` parameter with a standard Java POJO/DTO mapping using `@RequestBody`." + expectations: "The agent replaces legacy JNDI `ServiceLocator` lookup with Spring Dependency Injection (`@Autowired` or constructor injection)." + expectations: "The agent returns JSON data (e.g., a success message or updated profile) instead of a Struts `ActionForward` (JSP)." + expectations: "The agent explicitly recommends decoupling the UI into a standalone Angular or React SPA calling this REST API." +} + +cases { + name: "louvain_boundary_consolidation_plan" + prompt: + "We ran the Louvain community detection on `medimed` and it proposed 11 separate microservices, including standalone services for `AddressEJB`, `GroupEJB`, `PrescriptionEJB`, and `VitalSignsEJB`.\n" + "How should we structure the final migration plan in Phase 2?\n" + expectations: "The agent identifies the 11-service decomposition as an 'entity service' anti-pattern leading to a distributed monolith." + expectations: "The agent recommends consolidating these into logical domain-driven services (e.g., combining Patient, Address, and User into `patient-service`; combining Record, VitalSigns, and Prescription into `record-service`)." + expectations: "The agent documents this consolidation in the `wls-migration-plan.md` artifact." +} + +cases { + name: "jms_mdb_to_pubsub_spring_boot" + prompt: + "Migrate this legacy WebLogic EJB Message-Driven Bean (MDB) that processes incoming medical records from a JMS queue:\n" + "```java\n" + "package com.acme.medimed.mail;\n" + "import javax.ejb.MessageDriven;\n" + "import javax.jms.Message;\n" + "import javax.jms.MessageListener;\n" + "import javax.jms.TextMessage;\n" + "\n" + "@MessageDriven(name = \"MailMDB\")\n" + "public class MailMDB implements MessageListener {\n" + " public void onMessage(Message message) {\n" + " try {\n" + " TextMessage msg = (TextMessage) message;\n" + " String text = msg.getText();\n" + " // send email logic...\n" + " } catch (Exception e) {\n" + " e.printStackTrace();\n" + " }\n" + " }\n" + "}\n" + "```\n" + "We are migrating to Spring Boot on GCP Cloud Run and want to use Google Cloud Pub/Sub.\n" + expectations: "The agent replaces the legacy `@MessageDriven` EJB with a Spring `@Service` or `@Component`." + expectations: "The agent implements a GCP Pub/Sub subscriber (e.g. using `PubSubTemplate` or Spring Cloud Stream) to process incoming messages." + expectations: "The agent refactors the JMS `TextMessage` handling to extract payload from Pub/Sub `PubsubMessage`." +} + +cases { + name: "cloud_unfriendly_pattern_start_browser" + prompt: + "During Phase 1, we found this class in the `startupEar` module of `medimed`:\n" + "```java\n" + "package com.acme.medimed.startup;\n" + "import java.net.Socket;\n" + "public class StartBrowser implements Runnable {\n" + " public void run() {\n" + " try {\n" + " Thread.sleep(5000);\n" + " Socket s = new Socket(\"localhost\", 7001);\n" + " Runtime.getRuntime().exec(\"rundll32 url.dll,FileProtocolHandler http://localhost:7001/medimed\");\n" + " } catch (Exception e) {}\n" + " }\n" + "}\n" + "```\n" + "How should this be handled in the modernized Cloud Run architecture?\n" + expectations: "The agent identifies raw Thread usage (`Runnable`/`sleep`), direct socket connection (`new Socket`), and native OS execution (`exec`) as cloud-unfriendly patterns." + expectations: "The agent determines that this startup browser helper is obsolete for cloud deployment." + expectations: "The agent recommends discarding or disabling this entire startup module in the modernization plan." +} + +cases { + name: "generative_testing_cactus_retirement" + prompt: + "We are migrating a legacy Struts Action to a Spring Boot REST Controller in `medimed`. The legacy application had a Cactus integration test named `PatientActionCactusTest.java` that runs inside the WebLogic container.\n" + "Provide the modernized test strategy.\n" + expectations: "The agent generatively authors a new Spring Boot `@WebMvcTest` or `@SpringBootTest` to test the new REST endpoint." + expectations: "The agent explicitly states that the legacy Cactus test is container-bound, un-portable, and should be retired (disabled/removed)." + expectations: "The agent documents the retired test in the testing strategy section of the migration plan." +} + +cases { + name: "sql_dialect_oracle_to_postgres" + prompt: + "Migrate this legacy DAO query in `medimed` containing Oracle-specific syntax to Google Cloud SQL for PostgreSQL:\n" + "`SELECT p.pat_id, r.rec_id FROM patient p, record r WHERE p.pat_id = r.pat_id(+) AND NVL(p.status, 'INACTIVE') != 'DELETED'`\n" + expectations: "The agent translates the Oracle outer join syntax `r.pat_id(+)` to a standard SQL `LEFT OUTER JOIN`." + expectations: "The agent translates the Oracle `NVL` function to standard SQL `COALESCE`." +} + +cases { + name: "security_jaas_to_spring_security" + prompt: + "We have a custom WebLogic JAAS LoginModule for authentication in `medimed`:\n" + "```java\n" + "package com.acme.medimed.security;\n" + "import javax.security.auth.spi.LoginModule;\n" + "// ... custom authentication logic checking DB ...\n" + "```\n" + "How do we modernize this for Spring Boot on Cloud Run?\n" + expectations: "The agent recommends replacing the custom JAAS `LoginModule` with Spring Security." + expectations: "The agent configures Spring Security to use stateless JWT authentication." + expectations: "The agent explains how the client will authenticate (e.g. via a login REST endpoint returning JWT) and how subsequent requests will be validated." +} + +cases { + name: "infrastructure_as_code_generation_medimed" + prompt: + "We have completed Phase 3 and 4 of our WebLogic migration for `medimed`. The target architecture is Spring Boot on Cloud Run.\n" + "Please proceed with Phase 5 for the 'patient-service'.\n" + expectations: "The agent generates a multi-stage Dockerfile optimized for Spring Boot." + expectations: "The agent generatively authors Terraform manifests (main.tf, variables.tf) for deploying the service to Cloud Run and connecting it to Cloud SQL." +} + +cases { + name: "security_constraint_preservation_spring" + prompt: + "Migrate this legacy web.xml security snippet to Spring Boot for `medimed` application:\n" + "```xml\n" + "\n" + " \n" + " /admin/*\n" + " \n" + " Admin\n" + "\n" + "```\n" + "And we also have a public `/health` endpoint that had no constraints.\n" + expectations: "The agent applies Spring Security configurations (e.g., in a `SecurityFilterChain` bean) to secure `/admin/**` requiring `Admin` role." + expectations: "The agent explicitly ensures `/health` is permitted to all (`permitAll()`)." +} + +cases { + name: "cloud_unfriendly_file_io_medimed" + prompt: + "In `medimed`, we have a legacy class that saves patient reports to local disk:\n" + "```java\n" + "package com.acme.medimed.utils;\n" + "import java.io.File;\n" + "import java.io.FileOutputStream;\n" + "public class ReportSaver {\n" + " public void saveReport(String patientId, byte[] pdfBytes) throws Exception {\n" + " File file = new File(\"/var/medimed/reports/\" + patientId + \".pdf\");\n" + " FileOutputStream fos = new FileOutputStream(file);\n" + " fos.write(pdfBytes);\n" + " fos.close();\n" + " }\n" + "}\n" + "```\n" + "Migrate this to a cloud-native pattern.\n" + expectations: "The agent identifies local File I/O as a cloud-unfriendly pattern." + expectations: "The agent refactors the code to use Google Cloud Storage (GCS) client library to upload the PDF bytes." +} + +cases { + name: "target_stack_alignment_medimed" + prompt: "We want to migrate our monolithic `medimed` application to GCP. We are already using Maven. What are the next steps for architecture alignment?\n" + expectations: "The agent explicitly asks the user technical architecture questions using `ask_question` tool." + expectations: "The agent skips asking about the Build System since Maven was specified." + expectations: "The agent groups multiple alignment questions (Framework, GCP Service, UI, DB, JMS) into a single tool call to minimize interactions." +} + +cases { + name: "louvain_knob_tuning_and_resolutions" + prompt: + "We are analyzing the `acme-medimed` billing and inventory modules. The package structure is:\n" + "- `com.acme.medimed.billing`: Contains `BillingSessionEJB.java`, `BillingEJB.ejb`, `InvoiceEJB.ejb`.\n" + "- `com.acme.medimed.inventory`: Contains `InventorySessionEJB.java`, `ItemEJB.ejb`, `WarehouseEJB.ejb`.\n" + "- `com.acme.medimed.common`: Contains `PaymentService.java` (a utility class imported by both `BillingSessionEJB` and `InventorySessionEJB` with high fan-in).\n" + "\n" + "We ran the mathematical decomposition. \n" + "- At resolution 0.1, the Louvain algorithm groups everything into a single giant service.\n" + "- At resolution 1.0, the algorithm splits `BillingSessionEJB` and `BillingEJB` into separate services, and also splits `InventorySessionEJB` and `ItemEJB`. It also creates separate services for `InvoiceEJB` and `WarehouseEJB`.\n" + "\n" + "How should we tune the CLI clustering knobs and resolve this topology?\n" + expectations: "The agent MUST identify `com.acme.medimed.common.PaymentService` as a shared utility ('God Glue') that causes over-consolidation at low resolutions." + expectations: "The agent MUST recommend adding `com.acme.medimed.common.*` to `--exclude-patterns` or `--infrastructure-package-pattern` to sever this artificial link." + expectations: "The agent MUST identify the splitting of Session EJBs from their corresponding Entity EJBs (e.g. `BillingSessionEJB` split from `BillingEJB`) as an anchoring failure." + expectations: "The agent MUST recommend increasing `--weight-domain-anchor` (e.g., to 10.0 or 15.0) to bind Session facades to their underlying domain entities." + expectations: "The agent recommends consolidating the resulting components into two logical backend microservices: `billing-service` and `inventory-service`." +} + +cases { + name: "multi_file_ejb_and_jms_refactoring" + prompt: + "Modernize the following two interacting classes from the `acme-medimed` application.\n" + "\n" + "File 1: `com/acme/medimed/billing/BillingSessionEJB.java`\n" + "```java\n" + "package com.acme.medimed.billing;\n" + "import javax.ejb.SessionBean;\n" + "import javax.naming.InitialContext;\n" + "import javax.jms.QueueConnectionFactory;\n" + "import javax.jms.QueueConnection;\n" + "import javax.jms.QueueSession;\n" + "import javax.jms.QueueSender;\n" + "import javax.jms.Queue;\n" + "import javax.jms.TextMessage;\n" + "\n" + "public class BillingSessionEJB implements SessionBean {\n" + " public void processPayment(String patientId, double amount) throws Exception {\n" + " // Lookup and call another EJB\n" + " InitialContext ctx = new InitialContext();\n" + " PatientSessionHome patientHome = (PatientSessionHome) ctx.lookup(\"PatientSessionEJB.Home\");\n" + " PatientSession patient = patientHome.create();\n" + " String email = patient.getPatientEmail(patientId);\n" + " \n" + " // Send message to JMS Queue\n" + " QueueConnectionFactory qcf = (QueueConnectionFactory) ctx.lookup(\"com.acme.medimed.QueueConnectionFactory\");\n" + " QueueConnection qc = qcf.createQueueConnection();\n" + " QueueSession qsession = qc.createQueueSession(false, QueueSession.AUTO_ACKNOWLEDGE);\n" + " Queue q = (Queue) ctx.lookup(\"com.acme.medimed.mail.MailQueue\");\n" + " QueueSender sender = qsession.createSender(q);\n" + " TextMessage msg = qsession.createTextMessage(\"Send billing email to: \" + email + \" for amount: \" + amount);\n" + " sender.send(msg);\n" + " qc.close();\n" + " }\n" + " // EJB hooks...\n" + " public void ejbCreate() {}\n" + " public void setSessionContext(javax.ejb.SessionContext ctx) {}\n" + " public void ejbRemove() {}\n" + " public void ejbActivate() {}\n" + " public void ejbPassivate() {}\n" + "}\n" + "```\n" + "\n" + "File 2: `com/acme/medimed/billing/PatientSession.java` (Interface)\n" + "```java\n" + "package com.acme.medimed.billing;\n" + "import javax.ejb.EJBObject;\n" + "import java.rmi.RemoteException;\n" + "public interface PatientSession extends EJBObject {\n" + " public String getPatientEmail(String patientId) throws RemoteException;\n" + "}\n" + "```\n" + "We are migrating to Spring Boot on GCP.\n" + expectations: "The agent MUST convert `BillingSessionEJB` into a Spring `@Service` class named `BillingService`." + expectations: "The agent MUST replace the JNDI lookup of `PatientSession` with Spring Dependency Injection (e.g., injecting a `PatientServiceClient` or `PatientService` bean)." + expectations: "The agent MUST replace the legacy JMS Queue Connection/Sender code with `PubSubTemplate` or Spring `PubSubTemplate` to publish messages to a GCP Pub/Sub topic." + expectations: "The agent MUST remove all legacy EJB lifecycle methods (ejbCreate, setSessionContext, etc.)." + expectations: "The agent MUST remove `RemoteException` handling from the signature of `getPatientEmail` in the modernized interface/client." +} diff --git a/skills/cloud/google-cloud-weblogic-migration/SKILL.md b/skills/cloud/google-cloud-weblogic-migration/SKILL.md new file mode 100644 index 0000000000..db73075740 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/SKILL.md @@ -0,0 +1,196 @@ +--- +name: google-cloud-weblogic-migration +description: | + Migrates legacy WebLogic applications to cloud-native serverless microservices on GCP (Spring Boot or Quarkus on Cloud Run or Cloud Functions). + Use when analyzing, decomposing, or refactoring legacy Java EE/WebLogic monoliths (EJBs, JMS, JNDI, Struts, CMP/BMP entity beans) into modern cloud microservices. + Don't use for general-purpose Java bug fixing, standard Spring Boot feature development, or non-WebLogic/non-Java EE application migrations. +--- + +# WebLogic Monolith to Cloud-Native Migration Skill + +This skill guides the agent through migrating a legacy WebLogic monolith +application to a modern, cloud-native serverless microservice architecture on +Google Cloud Platform (GCP). It supports migrating to either Spring Boot or +Quarkus, running on GKE, Cloud Run or Cloud Functions. + +## Workflow Overview + +The migration process is divided into seven phases: + +1. **Environment & Prerequisites Check**: Verify JDK compatibility (LTS JDK 11, + 17, or 21 is required for AST parsing) and resolve environmental blockers. +2. **Analysis & Discovery**: Analyze the codebase to identify WebLogic-specific + APIs, EJB, JMS, JNDI, and configs, and discover clusters mathematically to + propose microservices boundaries. +3. **Technical Stack Alignment & Decomposition Review**: Align with the user on + target cloud modernization choices and present the customized architecture + plan as an interactive UI Artifact or markdown file for review and approval. +4. **Incremental Refactoring**: Refactor the code to remove WebLogic + dependencies and implement modern patterns. +5. **Configuration & Infrastructure Mapping**: Map WebLogic resources + (datasources, JMS, caching, file storage) to GCP services (Cloud SQL, + Pub/Sub, Memorystore, Cloud Storage). +6. **Containerization & Deployment**: Generate Dockerfiles and deployment + configurations. +7. **Verification, Audit, & Traceability**: Verify that the migration was + performed correctly, validate security gates, and establish complete + endpoint traceability in a walkthrough audit report. + +-------------------------------------------------------------------------------- + +## Phase 0: Environment & Prerequisites Check + +Before initiating the codebase analysis, verify that the environment meets the +technical prerequisites to ensure accurate static analysis. + +> [!IMPORTANT] You **MUST** run a pre-flight check of the environment: 1. Verify +> that Maven is installed and accessible. 2. The AST parsing engine requires a +> compatible LTS JDK version (JDK 11, 17, or 21). The tools will automatically +> attempt to locate a compatible JDK under `/usr/lib/jvm/` or +> `/usr/local/buildtools/java/` and override `JAVA_HOME` if the default JDK is +> incompatible (e.g. JDK 26+). 3. Verify that Python dependencies are installed +> by executing: `pip install -r scripts/requirements.txt` 4. **Catastrophic +> Failure Halt**: If a compatible JDK is not found, if Maven compilation of the +> `ast_parser` project fails, or if more than 10% of the Java files yield +> `ParseError` during analysis, you **MUST STOP** and align with the user. Do +> **NOT** proceed to Phase 1 with disabled or failing AST parsing. Present the +> error to the user and ask them to resolve the environment issue (e.g., +> installing a compatible JDK 11/17/21 or fixing Maven settings). + +-------------------------------------------------------------------------------- + +## Phase 1: Analysis & Discovery + +Scan the legacy monolith codebase iteratively to understand its structure, +identify migration blockers, and calculate optimal microservice boundaries +mathematically using the Louvain community detection engine. + +> [!IMPORTANT] You **MUST** read and follow +> [decomposition_guide.md](./references/decomposition_guide.md) for instructions +> on executing multi-resolution exploration, utilizing generalization knobs, +> filtering "God Glue" shared utilities, and iterating internally until +> achieving a stable mathematical optimum. + +-------------------------------------------------------------------------------- + +## Phase 2: Technical Stack Alignment & Decomposition Review + +Align with the user on target cloud modernization choices, then present the +customized architecture plan as an interactive UI Artifact (if supported by the +environment) or a standard markdown file for their review and approval. + +> [!IMPORTANT] You **MUST** read and follow +> [migration_plan_guide.md](./references/migration_plan_guide.md) for: 1. +> Conducting dynamic, context-relevant technical stack alignment with the user +> (adding custom questions for unique discovered blockers and omitting +> irrelevant ones). 2. Generating the customized `wls-migration-plan.md` +> artifact once and only once after all discovery and alignment are complete. 3. +> Managing the interactive human feedback and refinement loop (overwriting +> `wls-migration-plan.md` and stopping tool execution upon reviewer comments +> until explicitly approved). + +-------------------------------------------------------------------------------- + +## Phase 3: Incremental Refactoring + +Refactor the legacy codebase incrementally, service by service or module by +module, based on the approved migration plan. + +> [!IMPORTANT] **Autonomy & Continuity**: Once the user approves Phase 2, you +> MUST proceed autonomously through the entire refactoring phase. Do NOT stop to +> ask for permission between services or files. Continuously refactor all +> services until Phase 3 is fully complete, unless a critical architectural +> ambiguity blocks your progress. + +> [!IMPORTANT] **Java Version Policy**: ALWAYS target LTS Java versions (Java +> 17, Java 21, or Java 25). Do NOT use unreleased or experimental versions (e.g. +> Java 26). + +> [!IMPORTANT] You **MUST** read and follow +> [refactoring_guide.md](./references/refactoring_guide.md) for detailed +> procedural steps on: * Initializing target workspace modules +> (`wls_migration/`). * Executing automated bulk refactoring via OpenRewrite and +> understanding its strict manual limitations. * Replacing JNDI lookups and +> migrating EJBs (Session beans and MDBs). * Aligning security constraints with +> legacy monolith maps and configuring token validation. * Decoupling +> presentation tiers (JSP/Struts to Angular/React SPAs and REST controllers). * +> Modernizing cloud-unfriendly patterns (File I/O, Batch, JavaMail, RMI, JMX). * +> Porting test suites and verifying compilation and functional equivalence. + +> [!TIP] Use the following specialized reference guides for target code +> transformation patterns: * +> [refactoring_spring.md](./references/refactoring_spring.md) * +> [refactoring_quarkus.md](./references/refactoring_quarkus.md) * +> [web_modernization.md](./references/web_modernization.md) * +> [distributed_transactions.md](./references/distributed_transactions.md) * +> [legacy_remoting_and_jca.md](./references/legacy_remoting_and_jca.md) (for T3, +> RMI, CORBA, and JCA `.rar` adapters) * +> [sql_dialect_migration.md](./references/sql_dialect_migration.md) (for +> Oracle/PointBase SQL to ANSI SQL/PostgreSQL/MySQL) * +> [weblogic_specific_apis.md](./references/weblogic_specific_apis.md) (for +> custom security SPIs, JMX MBeans, Work Managers, and XML StAX) * +> [classloading_and_packaging.md](./references/classloading_and_packaging.md) +> (for EAR restructuring, `APP-INF/lib`, and ``) * +> [example_advanced_jms_migration.md](./assets/example_advanced_jms_migration.md) +> (for Unit-of-Order, Selectors, DLQs, and Messaging Bridges) * +> [example_soap_webservices_migration.md](./assets/example_soap_webservices_migration.md) +> (for SOAP, JAX-WS, CXF, and WS-Security) * +> [example_caching_and_clustering.md](./assets/example_caching_and_clustering.md) +> (for SFSBs, Coherence, and HTTP session replication to Redis) * +> [example_scheduling_and_timers.md](./assets/example_scheduling_and_timers.md) +> (for EJB timers, Quartz, and Cloud Scheduler/Jobs) * +> [example_observability_migration.md](./assets/example_observability_migration.md) +> (for structured JSON logging and OpenTelemetry tracing) * +> [example_servlet_filter_migration.md](./assets/example_servlet_filter_migration.md) +> (for filters, listeners, and `web.xml` / `weblogic.xml`) * View code templates +> in the `assets/` directory following the pattern `example_*.md`. + +-------------------------------------------------------------------------------- + +## Phase 4: Configuration & Infrastructure Mapping + +Map WebLogic infrastructure resources, descriptors, and services to native GCP +managed services. + +> [!IMPORTANT] You **MUST** read and follow +> [gcp_mapping.md](./references/gcp_mapping.md) for detailed configuration steps +> and code snippets for mapping: * WebLogic Datasources to GCP Cloud SQL or +> AlloyDB (including dialect updates and SQL query conversions). * Plaintext +> secrets and credentials to GCP Secret Manager and environment variables. * +> WebLogic JMS Queues and Topics to GCP Pub/Sub Topics and Subscriptions. * File +> storage and caching to Google Cloud Storage (GCS), Filestore, Parallelstore, +> and Cloud Memorystore (Redis). + +-------------------------------------------------------------------------------- + +## Phase 5: Containerization & Deployment + +Prepare the refactored microservices for serverless container execution and +automated CI/CD pipelines. + +> [!IMPORTANT] You **MUST** read and follow +> [deployment_guide.md](./references/deployment_guide.md) and +> [example_containerization.md](./assets/example_containerization.md) for +> detailed instructions on: * Authoring multi-stage framework-optimized +> Dockerfiles (including Quarkus native builds). * Generating Google Cloud Build +> configurations (`cloudbuild.yaml`). * Authoring Terraform manifests +> (`main.tf`, `variables.tf`) or declarative `gcloud` deployment commands for +> Cloud Run and Cloud Functions. * Documenting operational setup, Secret Manager +> variables, and JWT/security configurations in module READMEs. + +-------------------------------------------------------------------------------- + +## Phase 6: Verification, Audit, & Traceability + +Verify that the migration was performed correctly, validate security gates, and +establish complete endpoint traceability. + +> [!IMPORTANT] You **MUST** read and follow +> [verification_guide.md](./references/verification_guide.md) for detailed +> instructions on: * Constructing the comprehensive Before/After Endpoint +> Traceability Matrix. * Executing local integration assertions and security +> access verification gates on secured vs. public routes. * Generating the +> mandatory `walkthrough.md` audit report artifact (`RequestFeedback: true` and +> `UserFacing: true`) documenting structural comparisons, refactoring +> deviations, compilation/test suite logs, and security warnings. + diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_advanced_jms_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_advanced_jms_migration.md new file mode 100644 index 0000000000..f00a478308 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_advanced_jms_migration.md @@ -0,0 +1,177 @@ +# Advanced WebLogic JMS & Messaging Modernization Guide + +This guide provides concrete transformation recipes for modernizing advanced +WebLogic JMS features (Unit-of-Order, Message Selectors, Poison Messages/Dead +Letter Queues, and Messaging Bridges) when migrating to Google Cloud Pub/Sub in +Spring Boot and Quarkus microservices. + +-------------------------------------------------------------------------------- + +## 1. JMS Unit-of-Order (UOO) to GCP Pub/Sub Ordering Keys + +In legacy WebLogic JMS, setting a `UnitOfOrder` message property guarantees that +all messages sharing the same order name are delivered sequentially by a single +consumer instance. + +### Before: Legacy WebLogic JMS Unit-of-Order + +```java +// Legacy WebLogic JMS Unit-of-Order +import weblogic.jms.extensions.WLMessageProducer; +import javax.jms.*; + +public class OrderDispatcher { + public void sendOrderedTransaction(Session session, MessageProducer producer, String accountId, String payload) throws JMSException { + TextMessage msg = session.createTextMessage(payload); + // Set WebLogic specific Unit-of-Order property + msg.setStringProperty("JMS_BEA_UnitOfOrder", accountId); + producer.send(msg); + } +} +``` + +### After: Google Cloud Pub/Sub Ordering Keys + +In Google Cloud Pub/Sub, message ordering is guaranteed within a topic by +setting an **Ordering Key** on published messages and enabling message ordering +on the subscription. + +#### Spring Boot (Spring Cloud GCP Pub/Sub) + +```java +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class OrderDispatcher { + @Autowired + private PubSubTemplate pubSubTemplate; + + public void sendOrderedTransaction(String accountId, String payload) { + // Publish message with Ordering Key set to accountId + pubSubTemplate.publish("account-transactions-topic", payload, Collections.emptyMap(), accountId); + } +} +``` + +#### Pub/Sub Subscription Configuration (Terraform / gcloud) + +```hcl +resource "google_pubsub_subscription" "ordered_sub" { + name = "account-transactions-sub" + topic = google_pubsub_topic.transactions.name + enable_message_ordering = true # CRITICAL: Required for ordering keys +} +``` + +-------------------------------------------------------------------------------- + +## 2. JMS Message Selectors to Pub/Sub Subscription Filters + +WebLogic JMS consumers frequently use SQL-92 message selectors (e.g., +`priority = 'HIGH' AND region = 'US'`) to filter incoming messages at the broker +level. + +### Before: Legacy WebLogic JMS Message Selector + +```java +// Legacy WebLogic JMS Selector +String selector = "hospital_code = 'GENERAL' AND emergency_level >= 3"; +MessageConsumer consumer = session.createConsumer(queue, selector); +``` + +### After: Google Cloud Pub/Sub Filter Expressions + +GCP Pub/Sub supports broker-level **Subscription Filter Expressions** matching +message attributes. + +#### Spring Boot Publisher (Setting Attributes) + +```java +import com.google.cloud.spring.pubsub.support.GcpPubSubHeaders; +import org.springframework.messaging.support.MessageBuilder; + +public void publishAlert(String hospitalCode, int emergencyLevel, String payload) { + Map attributes = Map.of( + "hospital_code", hospitalCode, + "emergency_level", String.valueOf(emergencyLevel) + ); + pubSubTemplate.publish("emergency-alerts-topic", payload, attributes); +} +``` + +#### Pub/Sub Subscription Filter Configuration + +```hcl +resource "google_pubsub_subscription" "filtered_sub" { + name = "general-hospital-emergencies-sub" + topic = google_pubsub_topic.alerts.name + + # Pub/Sub filter syntax matching SQL-92 selector logic + filter = "attributes.hospital_code = \"GENERAL\" AND attributes.emergency_level >= \"3\"" +} +``` + +-------------------------------------------------------------------------------- + +## 3. Poison Messages & Dead Letter Queues (DLQ) + +In WebLogic JMS, error destinations and redelivery limits prevent failing +messages (poison messages) from looping infinitely. + +### After: Google Cloud Pub/Sub Dead-Letter Topics & Exponential Backoff + +Configure Dead-Letter Topics (DLT) and retry policies directly on the GCP +Pub/Sub subscription: + +```hcl +resource "google_pubsub_topic" "dead_letter_topic" { + name = "emergency-alerts-dlq" +} + +resource "google_pubsub_subscription" "robust_sub" { + name = "emergency-alerts-sub" + topic = google_pubsub_topic.alerts.name + + # Dead-Letter Policy (Poison Message Handling) + dead_letter_policy { + dead_letter_topic = google_pubsub_topic.dead_letter_topic.id + max_delivery_attempts = 5 + } + + # Exponential Backoff Retry Policy + retry_policy { + minimum_backoff = "10s" + maximum_backoff = "600s" + } + + # Acknowledge deadline for long-running message consumers (up to 600s) + ack_deadline_seconds = 600 +} +``` + +> [!WARNING] **Ack Deadlines & Consumer Idempotency**: If message processing +> exceeds `ack_deadline_seconds`, GCP Pub/Sub will automatically redeliver the +> message to another worker instance. All consumer logic MUST be designed to be +> completely **idempotent** (e.g., checking message deduplication IDs in Cloud +> SQL/Redis before execution) to prevent duplicate processing. + +-------------------------------------------------------------------------------- + +## 4. WebLogic Messaging Bridges to GCP Pub/Sub Connectors + +WebLogic Messaging Bridges connect external message brokers (e.g., IBM MQ, Tibco +EMS, or Apache ActiveMQ) to internal WebLogic JMS queues. + +### After: Cloud-Native Bridge Strategies + +In serverless containers, replace embedded WebLogic Messaging Bridges with: + +1. **Google Cloud Pub/Sub Connectors**: Use managed connectors (like + Kafka-to-PubSub or MQ-to-PubSub sinks/sources in Google Cloud Dataflow). +2. **Dedicated Ingestion Worker Microservice**: If specific MQ protocols are + required, deploy a lightweight Spring Boot worker using Spring JMS + (`spring-boot-starter-activemq` or IBM MQ client) that listens to the + external broker and forwards messages asynchronously into GCP Pub/Sub + topics. diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_caching_and_clustering.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_caching_and_clustering.md new file mode 100644 index 0000000000..2844867232 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_caching_and_clustering.md @@ -0,0 +1,158 @@ +# Stateful Session Beans (SFSB), Coherence Caching, & Session Replication Modernization Guide + +Legacy WebLogic applications rely on **In-Memory HTTP Session Replication** +across WebLogic Managed Server cluster members, **Oracle Coherence** data grids, +or EJB 2.x/3.x **Stateful Session Beans (SFSBs)** (`@Stateful`, +`ejbPassivate()`, `ejbActivate()`) to maintain conversational client state +across requests. + +In cloud-native serverless container environments (Google Cloud Run / Google +Kubernetes Engine / Google Cloud Functions), container replicas are ephemeral +and stateless. All in-memory conversational state and clustering protocols must +be externalized to managed cloud data grids. + +-------------------------------------------------------------------------------- + +## 1. Stateful Session Beans (SFSB) to Stateless Services & Redis + +In WebLogic, `@Stateful` session beans hold conversational state for a specific +client across multiple method invocations. + +### Before: Legacy WebLogic Stateful Session Bean (`@Stateful`) + +```java +import javax.ejb.Stateful; +import javax.ejb.Remove; + +@Stateful +public class PatientCartBean implements PatientCart { + private List selectedPrescriptionIds = new ArrayList<>(); + + public void addPrescription(Long prescriptionId) { + selectedPrescriptionIds.add(prescriptionId); + } + + @Remove + public List checkout() { + return selectedPrescriptionIds; + } +} +``` + +### After: Stateless Spring / CDI Service with Google Cloud Memorystore (Redis) + +Refactor `@Stateful` EJBs into **stateless Spring Services (`@Service`)** or +Quarkus `@ApplicationScoped` beans by externalizing conversational state into +**Google Cloud Memorystore for Redis**: + +#### Spring Boot with Spring Data Redis + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import java.time.Duration; +import java.util.List; + +@Service +public class PatientCartService { + @Autowired + private RedisTemplate redisTemplate; + + private String getCartKey(String patientId) { + return "cart:patient:" + patientId; + } + + public void addPrescription(String patientId, Long prescriptionId) { + String key = getCartKey(patientId); + redisTemplate.opsForList().rightPush(key, prescriptionId); + redisTemplate.expire(key, Duration.ofHours(2)); // TTL for conversational state + } + + public List checkout(String patientId) { + String key = getCartKey(patientId); + List items = redisTemplate.opsForList().range(key, 0, -1); + redisTemplate.delete(key); // Clear state upon checkout + return items; + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. WebLogic HTTP Session Replication & Oracle Coherence to Redis + +Legacy WebLogic web modules configure `` in `weblogic.xml` +for in-memory session replication or Coherence Web clustering. + +### Before: Legacy `weblogic.xml` Session Clustering + +```xml + + replicated_if_clustered + true + +``` + +### After: Spring Session Data Redis (Spring Boot) + +In Spring Boot, replace WebLogic in-memory replication with **Spring Session +Data Redis**, which transparently backs `HttpSession` with Google Cloud +Memorystore for Redis without requiring code changes in controllers: + +```xml + + + org.springframework.session + spring-session-data-redis + + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +```properties +# application.properties +spring.session.store-type=redis +spring.data.redis.host=${REDIS_HOST:10.0.0.3} +spring.data.redis.port=6379 +spring.session.redis.namespace=medimed:session +``` + +### After: Quarkus Redis Cache & Session + +In Quarkus, back HTTP sessions or application caches using the +`quarkus-redis-client` and `quarkus-cache` extensions: + +```xml + + io.quarkus + quarkus-redis-client + + + io.quarkus + quarkus-cache + +``` + +```properties +# application.properties +quarkus.redis.hosts=redis://${REDIS_HOST:10.0.0.3}:6379 +quarkus.cache.type=redis +``` + +```java +import io.quarkus.cache.CacheResult; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MedicalCatalogService { + + // Transparently caches result in Google Cloud Memorystore for Redis + @CacheResult(cacheName = "drug-catalog") + public DrugDetails getDrugDetails(String drugCode) { + // Heavy database lookup... + } +} +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_containerization.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_containerization.md new file mode 100644 index 0000000000..08f78cc44a --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_containerization.md @@ -0,0 +1,135 @@ +# Example: Containerization for Serverless (Cloud Run) + +This example provides Dockerfiles optimized for running Spring Boot and Quarkus +applications on Google Cloud Run. + +## 1. Spring Boot Dockerfile (Layered Jar) + +Spring Boot 2.3+ supports layered jars. This allows for better caching of +dependencies in Docker layers. + +### Maven Configuration (pom.xml) + +Ensure layering is enabled in the Spring Boot plugin: + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + + +``` + +### Dockerfile + +```dockerfile +# Stage 1: Extract layers +FROM eclipse-temurin:21-jre-alpine AS builder +WORKDIR application +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract + +# Stage 2: Build the runtime image +FROM eclipse-temurin:21-jre-alpine +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/internal-dependencies/ ./ +COPY --from=builder application/application/ ./ + +# Cloud Run sets the PORT environment variable. +# We must configure Spring Boot to listen on this port. +ENV PORT=8080 +EXPOSE 8080 + +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] +``` + +-------------------------------------------------------------------------------- + +## 2. Quarkus Dockerfile (JVM Mode) + +Optimized for fast startup in JVM mode using fast-jar. + +### Dockerfile + +```dockerfile +# Stage 1: Build stage (Optional if building locally, but recommended for CI) +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /code +COPY pom.xml . +RUN mvn dependency:go-offline +COPY src src +RUN mvn package -DskipTests + +# Stage 2: Runtime stage +FROM eclipse-temurin:21-jre-alpine +WORKDIR /work/ +# Copy the fast-jar build outputs +COPY --from=build /code/target/quarkus-app/lib/ /work/lib/ +COPY --from=build /code/target/quarkus-app/*.jar /work/ +COPY --from=build /code/target/quarkus-app/app/ /work/app/ +COPY --from=build /code/target/quarkus-app/quarkus/ /work/quarkus/ + +ENV PORT=8080 +EXPOSE 8080 + +# Quarkus looks for HTTP port configuration or PORT env var +ENV QUARKUS_HTTP_PORT=8080 + +CMD ["java", "-jar", "/work/quarkus-run.jar"] +``` + +-------------------------------------------------------------------------------- + +## 3. Quarkus Dockerfile (Native Mode) + +Provides the fastest startup time (milliseconds) and lowest memory footprint, +ideal for scale-to-zero serverless. Requires GraalVM to build. + +### Dockerfile + +```dockerfile +# Stage 1: Build the native executable +FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.0-jdk21 AS build +COPY --chown=quarkus:quarkus mvnw /code/mvnw +COPY --chown=quarkus:quarkus .mvn /code/.mvn +COPY --chown=quarkus:quarkus pom.xml /code/ +USER quarkus +WORKDIR /code +RUN ./mvnw dependency:go-offline +COPY src /code/src +# Build native executable (-Pnative) +RUN ./mvnw package -Pnative -DskipTests + +# Stage 2: Create the runtime image (using a minimal base image) +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +WORKDIR /work/ +COPY --from=build /code/target/*-runner /work/application + +# Grant execution rights +RUN chmod 775 /work /work/application \ + && chown -R 1001 /work \ + && chmod -R "g+rwX" /work \ + && chown -R 1001:root /work + +EXPOSE 8080 +USER 1001 + +ENV PORT=8080 +ENV QUARKUS_HTTP_PORT=8080 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] +``` + +*(Note: Building native images can be slow and resource-intensive, but pays off +in serverless production).* diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_ejb_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_ejb_migration.md new file mode 100644 index 0000000000..3e2558d052 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_ejb_migration.md @@ -0,0 +1,194 @@ +# Example: EJB Stateless Session Bean Migration + +This example shows how to migrate a Stateless Session Bean (EJB) that uses +Container-Managed Transactions (CMT) and JNDI lookups to both Spring Boot and +Quarkus. + +## 1. Legacy WebLogic EJB (Before) + +### Remote Interface + +```java +package com.example.legacy; + +import javax.ejb.Remote; + +@Remote +public interface CatalogService { + public Product getProduct(String id); + public void addProduct(Product product); +} +``` + +### Bean Implementation + +```java +package com.example.legacy; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.naming.InitialContext; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +@Stateless(name = "CatalogService") +public class CatalogServiceBean implements CatalogService { + + // JNDI Lookup for Datasource (Legacy way) + private Connection getConnection() throws Exception { + InitialContext ctx = new InitialContext(); + DataSource ds = (DataSource) ctx.lookup("jdbc/CatalogDS"); + return ds.getConnection(); + } + + public Product getProduct(String id) { + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name, price FROM products WHERE id = ?")) { + ps.setString(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new Product(id, rs.getString("name"), rs.getDouble("price")); + } + } + } catch (Exception e) { + throw new RuntimeException("Database error", e); + } + return null; + } + + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void addProduct(Product product) { + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement("INSERT INTO products (id, name, price) VALUES (?, ?, ?)")) { + ps.setString(1, product.getId()); + ps.setString(2, product.getName()); + ps.setDouble(3, product.getPrice()); + ps.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException("Transaction failed", e); + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. Spring Boot Migration (After) + +In Spring Boot, we use `@Service` for the bean, `@Autowired` for dependency +injection (though Spring Boot configures the `DataSource` bean automatically), +and Spring JDBC (or JPA) to simplify database access. + +### Spring Service + +```java +package com.example.modern; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +@Service +public class CatalogServiceImpl implements CatalogService { + + // Spring injects the Datasource automatically based on application.properties + @Autowired + private DataSource dataSource; + + @Override + public Product getProduct(String id) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name, price FROM products WHERE id = ?")) { + ps.setString(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new Product(id, rs.getString("name"), rs.getDouble("price")); + } + } + } catch (Exception e) { + throw new RuntimeException("Database error", e); + } + return null; + } + + @Override + @Transactional // Spring transaction management + public void addProduct(Product product) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("INSERT INTO products (id, name, price) VALUES (?, ?, ?)")) { + ps.setString(1, product.getId()); + ps.setString(2, product.getName()); + ps.setDouble(3, product.getPrice()); + ps.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException("Transaction failed", e); + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 3. Quarkus Migration (After) + +In Quarkus, we use CDI `@ApplicationScoped` and Jakarta Transactions +`@Transactional`. + +### Quarkus CDI Bean + +```java +package com.example.modern; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +@ApplicationScoped +public class CatalogServiceBean implements CatalogService { + + // CDI injects the Datasource automatically + @Inject + DataSource dataSource; + + @Override + public Product getProduct(String id) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name, price FROM products WHERE id = ?")) { + ps.setString(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new Product(id, rs.getString("name"), rs.getDouble("price")); + } + } + } catch (Exception e) { + throw new RuntimeException("Database error", e); + } + return null; + } + + @Override + @Transactional(Transactional.TxType.REQUIRED) // Jakarta transaction + public void addProduct(Product product) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("INSERT INTO products (id, name, price) VALUES (?, ?, ?)")) { + ps.setString(1, product.getId()); + ps.setString(2, product.getName()); + ps.setDouble(3, product.getPrice()); + ps.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException("Transaction failed", e); + } + } +} +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_jms_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_jms_migration.md new file mode 100644 index 0000000000..a6f9a59710 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_jms_migration.md @@ -0,0 +1,183 @@ +# Example: JMS Message-Driven Bean to GCP Pub/Sub + +This example shows how to migrate a WebLogic Message-Driven Bean (MDB) listening +to a JMS Queue to a GCP Pub/Sub subscriber in both Spring Boot and Quarkus. + +## 1. Legacy WebLogic JMS MDB (Before) + +An EJB MDB that listens to `jms/OrderQueue` and processes incoming text +messages. + +```java +package com.example.legacy; + +import javax.ejb.MessageDriven; +import javax.ejb.ActivationConfigProperty; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.TextMessage; + +@MessageDriven( + name = "OrderProcessorMDB", + activationConfig = { + @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), + @ActivationConfigProperty(propertyName = "destination", propertyValue = "jms/OrderQueue") + } +) +public class OrderProcessorMDB implements MessageListener { + + @Override + public void onMessage(Message message) { + try { + if (message instanceof TextMessage) { + String payload = ((TextMessage) message).getText(); + System.out.println("Processing order: " + payload); + // Business logic to process order + } else { + System.err.println("Message of wrong type: " + message.getClass().getName()); + } + } catch (Exception e) { + System.err.println("Error processing message: " + e.getMessage()); + // In EJB, rollback might be triggered here depending on configuration + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. Spring Boot Migration with Spring Cloud GCP Pub/Sub (After) + +In Spring Boot, we use `PubSubTemplate` to subscribe to a Pub/Sub subscription +named `order-subscription` (which is subscribed to the `order-topic`). + +### Maven Dependency + +Ensure you have the Spring Cloud GCP Pub/Sub starter: + +```xml + + com.google.cloud + spring-cloud-gcp-starter-pubsub + +``` + +### Subscriber Code + +```java +package com.example.modern; + +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import com.google.cloud.spring.pubsub.support.BasicAcknowledgeablePubsubMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class OrderProcessorSubscriber { + + @Autowired + private PubSubTemplate pubSubTemplate; + + // Start listening when the application is ready + @EventListener(ApplicationReadyEvent.class) + public void subscribeToOrders() { + String subscriptionName = "order-subscription"; + + pubSubTemplate.subscribe(subscriptionName, (BasicAcknowledgeablePubsubMessage message) -> { + String payload = message.getPubsubMessage().getData().toStringUtf8(); + try { + System.out.println("Processing order from Pub/Sub: " + payload); + // Business logic to process order + + // Acknowledge the message upon successful processing + message.ack(); + } catch (Exception e) { + System.err.println("Failed to process order: " + e.getMessage()); + // Nack (negative acknowledge) to redeliver the message + message.nack(); + } + }); + } +} +``` + +-------------------------------------------------------------------------------- + +## 3. Quarkus Migration with Google Cloud Pub/Sub Client (After) + +In Quarkus, we use the standard GCP Pub/Sub Java SDK client, initialized on +application startup. + +### Maven Dependency + +```xml + + io.quarkiverse.googlecloudservices + quarkus-google-cloud-pubsub + +``` + +### Subscriber Code + +```java +package com.example.modern; + +import com.google.cloud.pubsub.v1.AckReplyConsumer; +import com.google.cloud.pubsub.v1.MessageReceiver; +import com.google.cloud.pubsub.v1.Subscriber; +import com.google.pubsub.v1.ProjectSubscriptionName; +import com.google.pubsub.v1.PubsubMessage; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class OrderProcessorSubscriber { + + @ConfigProperty(name = "gcp.project.id") + String projectId; + + @ConfigProperty(name = "gcp.pubsub.order-subscription") + String subscriptionId; + + private Subscriber subscriber; + + void onStart(@Observes StartupEvent ev) { + ProjectSubscriptionName subscriptionName = ProjectSubscriptionName.of(projectId, subscriptionId); + + MessageReceiver receiver = (PubsubMessage message, AckReplyConsumer consumer) -> { + String payload = message.getData().toStringUtf8(); + try { + System.out.println("Processing order from Quarkus Pub/Sub: " + payload); + // Business logic to process order + consumer.ack(); + } catch (Exception e) { + System.err.println("Failed to process order: " + e.getMessage()); + consumer.nack(); + } + }; + + subscriber = Subscriber.newBuilder(subscriptionName, receiver).build(); + // Start the subscriber asynchronously + subscriber.startAsync().awaitRunning(); + } + + void onStop(@Observes ShutdownEvent ev) { + if (subscriber != null) { + // Stop the subscriber when application stops + subscriber.stopAsync().awaitTerminated(); + } + } +} +``` + +### Configuration (`application.properties`) + +```properties +gcp.project.id=my-gcp-project +gcp.pubsub.order-subscription=order-subscription +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_observability_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_observability_migration.md new file mode 100644 index 0000000000..8e7b912a9d --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_observability_migration.md @@ -0,0 +1,149 @@ +# WebLogic Diagnostics (WLDF), Structured Logging, & Traceability Modernization Guide + +Legacy WebLogic applications use **WLDF (WebLogic Diagnostics Framework)** +watches/notifications, `weblogic.logging.NonCatalogLogger`, `weblogic.i18n.*`, +or raw `log4j` / `java.util.logging` writing to local filesystem log files +(`AdminServer.log`, `access.log`). + +In Google Cloud Run and Google Kubernetes Engine (GKE), container logs are +captured from `stdout` / `stderr`. This guide explains how to modernize logging +into structured JSON for **Google Cloud Logging** and implement distributed +tracing for **Google Cloud Trace**. + +-------------------------------------------------------------------------------- + +## 1. Structured JSON Logging for Google Cloud Logging + +When microservices output standard unstructured text logs, Cloud Logging treats +the entire log line as a text payload, making filtering by severity, timestamp, +or exception trace difficult. By switching to **Structured JSON Logging**, +Google Cloud Logging automatically parses severity levels, timestamps, thread +IDs, and custom MDC attributes. + +### Spring Boot with Logback JSON (`logback-spring.xml`) + +Add the Logback JSON encoder dependency: + +```xml + + + net.logstash.logback + logstash-logback-encoder + 7.4 + +``` + +Create `src/main/resources/logback-spring.xml` configured for Google Cloud +Logging JSON structure: + +```xml + + + + + + + severity + time + message + logger + thread + + trace_id + span_id + + + + + + + +``` + +### Quarkus Logging JSON (`application.properties`) + +In Quarkus, add the `quarkus-logging-json` extension: + +```xml + + + io.quarkus + quarkus-logging-json + +``` + +```properties +# application.properties +quarkus.log.console.json=true +quarkus.log.console.json.record-separator=\n +quarkus.log.console.json.date-format=iso-8601 +# Map Quarkus log fields to GCP Cloud Logging schema +quarkus.log.console.json.field-level=severity +quarkus.log.console.json.field-message=message +``` + +-------------------------------------------------------------------------------- + +## 2. Distributed Tracing with OpenTelemetry & Google Cloud Trace + +When a WebLogic monolith is decomposed into multiple microservices (e.g., +`patient-service` calling `record-service` via REST and publishing to +`audit-topic` via Pub/Sub), tracing a single user transaction across network +hops requires **Distributed Tracing**. + +### Spring Boot with Micrometer Tracing & GCP Trace + +Add Spring Cloud GCP Trace starters: + +```xml + + + com.google.cloud + spring-cloud-gcp-starter-trace + + + io.micrometer + micrometer-tracing-bridge-otel + +``` + +```properties +# application.properties +spring.cloud.gcp.trace.enabled=true +spring.cloud.gcp.trace.sampling.probability=1.0 # Sample 100% in Dev/Test, reduce in Prod +``` + +### Quarkus with OpenTelemetry (`quarkus-opentelemetry`) + +In Quarkus, add the OpenTelemetry extension and configure the GCP OTLP exporter +sidecar or direct OTLP endpoint: + +```xml + + + io.quarkus + quarkus-opentelemetry + +``` + +```properties +# application.properties +quarkus.otel.enabled=true +quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4317 # Cloud Trace OTLP collector +quarkus.otel.traces.sampler=always_on +``` + +### Automatic W3C Trace Context Propagation + +Both Spring Cloud GCP and Quarkus OpenTelemetry automatically inject and extract +standard W3C `traceparent` headers across HTTP requests and Google Cloud Pub/Sub +message attributes: + +```http +GET /api/v1/records/1042 HTTP/1.1 +Host: record-service.run.app +traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 +``` + +This enables Google Cloud Trace to render end-to-end latency waterfall diagrams +across your entire migrated microservices landscape. diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_scheduling_and_timers.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_scheduling_and_timers.md new file mode 100644 index 0000000000..fb4bb2df85 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_scheduling_and_timers.md @@ -0,0 +1,146 @@ +# EJB Timer Service, WebLogic Timers, & Clustered Quartz Modernization Guide + +Legacy WebLogic monoliths execute background jobs and periodic tasks using the +**EJB Timer Service** (`@Schedule`, `@Timeout`, `TimerService.createTimer()`), +`weblogic.timers.Timer`, or embedded **Quartz Scheduler** clustered via Oracle +database tables (`QRTZ_*`). + +In serverless container environments (Google Cloud Run / Google Cloud +Functions), running embedded polling loops inside web instances wastes CPU and +causes duplicate execution across horizontally scaled replicas. All scheduling +must be decoupled into managed cloud task orchestrators. + +-------------------------------------------------------------------------------- + +## 1. EJB Timer Service (`@Schedule`) to Google Cloud Scheduler + +In WebLogic, `@Schedule` annotations execute cron-like periodic tasks within the +EJB container. + +### Before: Legacy EJB Timer (`@Schedule`) + +```java +import javax.ejb.Schedule; +import javax.ejb.Stateless; + +@Stateless +public class DailyReportTimer { + + @Schedule(hour = "2", minute = "30", second = "0", persistent = true) + public void generateDailySummary() { + // Heavy daily report calculation... + } +} +``` + +### After: Google Cloud Scheduler + IAM-Protected Cloud Run Endpoint + +Remove `@Schedule` and expose the business logic as a secure REST endpoint +protected by Google Cloud IAM service account authentication, triggered +externally by **Google Cloud Scheduler**: + +#### Spring Boot / Quarkus Secure Endpoint + +```java +// Spring Boot REST Endpoint (or Quarkus JAX-RS @Path) +@RestController +@RequestMapping("/internal/jobs") +public class DailyReportJobController { + @Autowired + private DailyReportService dailyReportService; + + @PostMapping("/daily-summary") + public ResponseEntity triggerDailySummary() { + dailyReportService.generateDailySummary(); + return ResponseEntity.ok().build(); + } +} +``` + +#### Google Cloud Scheduler Configuration (Terraform / gcloud) + +```hcl +resource "google_service_account" "scheduler_sa" { + account_id = "report-scheduler-sa" + display_name = "Cloud Scheduler Service Account for Daily Reports" +} + +resource "google_cloud_run_service_iam_member" "invoker" { + service = google_cloud_run_service.report_service.name + location = google_cloud_run_service.report_service.location + role = "roles/run.invoker" + member = "serviceAccount:${google_service_account.scheduler_sa.email}" +} + +resource "google_cloud_scheduler_job" "daily_summary_job" { + name = "daily-report-summary-job" + description = "Triggers daily summary report on Cloud Run" + schedule = "30 2 * * *" # Daily at 02:30 AM + time_zone = "America/New_York" + attempt_deadline = "600s" + + http_target { + http_method = "POST" + uri = "${google_cloud_run_service.report_service.status[0].url}/internal/jobs/daily-summary" + + oidc_token { + service_account_email = google_service_account.scheduler_sa.email + audience = google_cloud_run_service.report_service.status[0].url + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. Clustered Quartz Schedulers to Google Cloud Run Jobs + +When legacy WebLogic applications use embedded Quartz Schedulers backed by +Oracle database tables (`QRTZ_*`) for long-running batch computing or analytical +data processing, extract the job into a standalone entry point designed to run +as a **Google Cloud Run Job**. + +### After: Google Cloud Run Job Execution + +Unlike Cloud Run Services (which listen on an HTTP port), Cloud Run Jobs run to +completion and automatically shut down, saving compute costs: + +```hcl +resource "google_cloud_run_v2_job" "batch_analytics_job" { + name = "medimed-batch-analytics" + location = "us-central1" + + template { + template { + containers { + image = "gcr.io/my-project/medimed-analytics-job:latest" + resources { + limits = { + memory = "4Gi" + cpu = "2" + } + } + env { + name = "SPRING_PROFILES_ACTIVE" + value = "batch-job" + } + } + } + } +} + +# Trigger Cloud Run Job via Cloud Scheduler +resource "google_cloud_scheduler_job" "trigger_batch_job" { + name = "trigger-medimed-batch-analytics" + schedule = "0 0 * * 0" # Weekly on Sunday at midnight + + http_target { + http_method = "POST" + uri = "https://run.googleapis.com/v2/projects/my-project/locations/us-central1/jobs/medimed-batch-analytics:run" + + oauth_token { + service_account_email = google_service_account.scheduler_sa.email + } + } +} +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_security_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_security_migration.md new file mode 100644 index 0000000000..4e66955a4c --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_security_migration.md @@ -0,0 +1,232 @@ +# Example: Security Migration (JAAS to Spring Security OIDC) + +This example shows how to migrate from legacy WebLogic-managed security (often +based on JAAS and web.xml constraints) to a modern OAuth2/OIDC resource server +using Spring Security. + +## 1. Legacy WebLogic Security (Before) + +Typically configured in `web.xml` and `weblogic.xml`, defining security +constraints and mapping roles to WebLogic users/groups. + +### `web.xml` (Security Constraints) + +```xml + + + AdminResources + /admin/* + + + AdminRole + + + + + BASIC + myrealm + + + + AdminRole + +``` + +### `weblogic.xml` (Role Mapping) + +Maps the logical role `AdminRole` to a physical WebLogic group `Administrators`. + +```xml + + + AdminRole + Administrators + + +``` + +### Java Code (Checking Roles Programmatically) + +```java +public void doAdminTask(HttpServletRequest request) { + if (request.isUserInRole("AdminRole")) { + // Perform admin task + } else { + throw new SecurityException("Unauthorized"); + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. Spring Boot Migration with OAuth2/OIDC (After) + +In Spring Boot, we secure endpoints using Spring Security. We configure the +application as an OAuth2 Resource Server, validating JWTs issued by an Identity +Provider (e.g., Google Identity Platform). + +### Maven Dependencies + +```xml + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + +``` + +### Security Configuration + +```java +package com.example.modern; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + // Replace web.xml constraints + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(new MyRoleConverter())) + ); + return http.build(); + } +} +``` + +### Role Converter + +Convert JWT claims (e.g., groups or roles from the token) to Spring Security +GrantedAuthorities. + +```java +package com.example.modern; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class MyRoleConverter implements Converter { + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + // Extract roles/groups from JWT claim (e.g., "roles" or "groups") + List roles = jwt.getClaimAsStringList("roles"); + + Collection authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + .collect(Collectors.toList()); + + return new JwtAuthenticationToken(jwt, authorities); + } +} +``` + +### Java Code (Method Security) + +Instead of `request.isUserInRole`, use Spring Security's `@PreAuthorize` or +standard `SecurityContextHolder`. + +```java +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +@Service +public class AdminService { + + @PreAuthorize("hasRole('ROLE_ADMIN')") + public void doAdminTask() { + // Perform admin task + } +} +``` + +### Configuration (`application.properties`) + +Point to the Identity Provider's JWK Set URI to validate tokens. + +```properties +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs +``` + +*(Example configuration for Google OAuth2. Change to your IdP endpoint).* + +-------------------------------------------------------------------------------- + +## 3. Quarkus Migration with OIDC (After) + +Quarkus provides first-class support for OpenID Connect (OIDC) and JWT bearer +token authentication. + +### Maven Dependencies + +```xml + + io.quarkus + quarkus-oidc + +``` + +### Java Code (Method Security & Role Checks) + +Use standard Jakarta Security annotations (`@RolesAllowed`) directly on REST +endpoints or service methods: + +```java +package com.example.modern; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/admin") +public class AdminResource { + + @Inject + SecurityIdentity securityIdentity; + + @GET + @Path("/dashboard") + @RolesAllowed("ADMIN") // Replaces legacy web.xml auth-constraint + public Response getDashboard() { + // Access user principal and attributes cleanly + String username = securityIdentity.getPrincipal().getName(); + return Response.ok("Welcome Admin: " + username).build(); + } +} +``` + +### Configuration (`application.properties`) + +Configure Quarkus to validate JWT bearer tokens against your OIDC provider or +Google Cloud Identity: + +```properties +# Configure Quarkus OIDC application type as bearer token service +quarkus.oidc.application-type=service +quarkus.oidc.auth-server-url=https://accounts.google.com +quarkus.oidc.client-id=my-gcp-client-id +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_servlet_filter_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_servlet_filter_migration.md new file mode 100644 index 0000000000..ccb395544f --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_servlet_filter_migration.md @@ -0,0 +1,168 @@ +# Servlet Filters, Listeners, & Web Descriptors (`web.xml` / `weblogic.xml`) Modernization Guide + +Legacy WebLogic web modules heavily use complex `web.xml` filter chains +(`javax.servlet.Filter`), `weblogic.xml` security role assignments, and +lifecycle listeners (`ServletContextListener`, `HttpSessionListener`, +`ServletRequestListener`). + +When refactoring to Spring Boot or Quarkus microservices, these imperative +servlet artifacts must be modernized into declarative web filters, request +interceptors, or Spring Security filters. + +-------------------------------------------------------------------------------- + +## 1. Servlet Filters (`javax.servlet.Filter`) to Spring / Quarkus Filters + +In WebLogic, filters are declared in `web.xml` via `` and +`` tags to intercept incoming HTTP requests for logging, header +manipulation, or custom authentication. + +### Before: Legacy `web.xml` Filter Declaration + +```xml + + AuditLogFilter + com.acme.medimed.filter.AuditLogFilter + + + AuditLogFilter + /api/* + +``` + +```java +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +public class AuditLogFilter implements Filter { + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpReq = (HttpServletRequest) req; + System.out.println("Intercepted request to: " + httpReq.getRequestURI()); + chain.doFilter(req, resp); + } +} +``` + +### After: Spring Boot `@WebFilter` / `OncePerRequestFilter` + +In Spring Boot, replace `web.xml` declarations with `@WebFilter` (accompanied by +`@ServletComponentScan` on the main application class) or implement Spring +Security's `OncePerRequestFilter`: + +```java +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class AuditLogFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (request.getRequestURI().startsWith("/api/")) { + logger.info("Intercepted request to: " + request.getRequestURI()); + } + filterChain.doFilter(request, response); + } +} +``` + +### After: Quarkus JAX-RS `@Provider` / `ContainerRequestFilter` + +In Quarkus, implement a JAX-RS `ContainerRequestFilter` annotated with +`@Provider`: + +```java +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.ext.Provider; +import org.jboss.logging.Logger; +import java.io.IOException; + +@Provider +public class AuditLogFilter implements ContainerRequestFilter { + private static final Logger LOG = Logger.getLogger(AuditLogFilter.class); + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + if (requestContext.getUriInfo().getPath().startsWith("/api/")) { + LOG.infof("Intercepted request to: %s", requestContext.getUriInfo().getPath()); + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. Lifecycle Listeners (`ServletContextListener` / `HttpSessionListener`) + +Legacy applications use `ServletContextListener.contextInitialized()` to execute +startup logic (such as loading reference data or initializing custom connection +pools) and `HttpSessionListener` to track active user sessions. + +### After: Spring Boot Startup Interceptors (`ApplicationRunner` / `@EventListener`) + +Replace `ServletContextListener` with Spring Boot's `ApplicationRunner` or +`@EventListener(ApplicationReadyEvent.class)`: + +```java +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +public class ReferenceDataStartupLoader implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) throws Exception { + // Execute startup initialization logic previously in ServletContextListener.contextInitialized() + System.out.println("Loading reference medical catalogs into cache on startup..."); + } +} +``` + +### After: Quarkus Startup Event (`@Observes StartupEvent`) + +In Quarkus, observe the `StartupEvent` using CDI: + +```java +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +@ApplicationScoped +public class ReferenceDataStartupLoader { + + void onStart(@Observes StartupEvent ev) { + // Execute startup initialization logic + System.out.println("Loading reference medical catalogs into cache on startup..."); + } +} +``` + +-------------------------------------------------------------------------------- + +## 3. `weblogic.xml` Security Role Assignments to Declarative Security + +In WebLogic, `weblogic.xml` maps security role names declared in `web.xml` +(``) to WebLogic realm users and groups +(``). + +### After: Stripping `weblogic.xml` & Enforcing Role Claims in Cloud IAM + +When migrating to Spring Security or Quarkus Security: + +1. **Delete `weblogic.xml` and `web.xml`**: Imperative XML descriptor role + mappings are obsolete. +2. **Map Roles in Identity Provider (IdP)**: Configure user-to-group mappings + in your cloud identity provider (Google Cloud Identity, Okta, or Keycloak). +3. **Enforce via Annotations**: Let Spring Security + (`@PreAuthorize("hasRole('ADMIN')")`) or Quarkus (`@RolesAllowed("ADMIN")`) + evaluate the role claims directly from incoming JWT bearer tokens. diff --git a/skills/cloud/google-cloud-weblogic-migration/assets/example_soap_webservices_migration.md b/skills/cloud/google-cloud-weblogic-migration/assets/example_soap_webservices_migration.md new file mode 100644 index 0000000000..8c97c7af15 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/assets/example_soap_webservices_migration.md @@ -0,0 +1,162 @@ +# WebLogic SOAP Web Services (JAX-WS / JAX-RPC) Modernization Guide + +Legacy WebLogic monoliths (especially WebLogic 10.3 / 11g / 12c) frequently +expose and consume SOAP Web Services using JAX-WS (`@WebService`, `@WebMethod`), +JAX-RPC, `weblogic.wsee.*`, and WS-Security. This guide provides recipes for +hosting legacy SOAP endpoints in Spring Boot and Quarkus, decoupling internal +SOAP calls into REST/gRPC, and modernizing WS-Security. + +-------------------------------------------------------------------------------- + +## 1. Hosting Legacy SOAP Endpoints for Backwards Compatibility + +When external enterprise clients (e.g., hospital networks, government agencies) +cannot immediately upgrade to REST/JSON, you must continue hosting the legacy +SOAP WSDL contract from your cloud-native microservice. + +### Before: Legacy WebLogic JAX-WS Endpoint + +```java +import javax.jws.WebService; +import javax.jws.WebMethod; + +@WebService(serviceName = "PatientLookupService", targetNamespace = "http://medimed.acme.com/") +public class PatientLookupServiceImpl { + @WebMethod + public PatientRecord getPatientBySSN(String ssn) { + // Fetch patient... + } +} +``` + +### After: Spring Boot with Apache CXF / Spring Web Services + +In Spring Boot, use **Apache CXF** or **Spring Web Services** to host the SOAP +endpoint without needing a full Java EE application server. + +#### Spring Boot Apache CXF (`pom.xml` & Configuration) + +```xml + + org.apache.cxf + cxf-spring-boot-starter-jaxws + 4.0.3 + +``` + +```java +import org.apache.cxf.Bus; +import org.apache.cxf.jaxws.EndpointImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import jakarta.xml.ws.Endpoint; + +@Configuration +public class SoapWebServiceConfig { + @Autowired + private Bus bus; + + @Autowired + private PatientLookupServiceImpl patientLookupService; + + @Bean + public Endpoint endpoint() { + EndpointImpl endpoint = new EndpointImpl(bus, patientLookupService); + endpoint.publish("/PatientLookupService"); // Exposes WSDL at /services/PatientLookupService?wsdl + return endpoint; + } +} +``` + +### After: Quarkus with Quarkus CXF + +In Quarkus, use the official `quarkus-cxf` extension to host JAX-WS endpoints +with native compilation support: + +```xml + + io.quarkiverse.cxf + quarkus-cxf + +``` + +```properties +# application.properties +quarkus.cxf.endpoint."/PatientLookupService".implementor=com.acme.medimed.soap.PatientLookupServiceImpl +``` + +-------------------------------------------------------------------------------- + +## 2. Decoupling Internal SOAP Calls to REST / gRPC + +When two modules *inside* the legacy WebLogic monolith communicated via SOAP (or +when decomposing two tightly coupled microservices), eliminate the XML/SOAP +overhead by refactoring the internal contract to REST/JSON or gRPC. + +### Strategy A: Convert to REST / JSON (`@RestController`) + +Replace `@WebService` with Spring `@RestController` or Quarkus JAX-RS `@Path`, +converting XML payloads to lightweight JSON DTOs: + +```java +// Spring Boot REST Controller Replacement +@RestController +@RequestMapping("/api/v1/patients") +public class PatientLookupRestController { + @Autowired + private PatientService patientService; + + @GetMapping("/ssn/{ssn}") + public ResponseEntity getPatientBySSN(@PathVariable String ssn) { + return ResponseEntity.ok(patientService.getPatientBySSN(ssn)); + } +} +``` + +### Strategy B: Convert to gRPC / Protocol Buffers (High-Performance Internal RPC) + +For high-frequency internal calls across Cloud Run VPC networks, define a +`.proto` schema: + +```protobuf +syntax = "proto3"; +package medimed.patient; + +service PatientLookupService { + rpc GetPatientBySSN (PatientRequest) returns (PatientRecord); +} + +message PatientRequest { + string ssn = 1; +} + +message PatientRecord { + int64 id = 1; + string name = 2; + string dob = 3; +} +``` + +-------------------------------------------------------------------------------- + +## 3. WS-Security to Cloud IAM & OIDC Bearer Tokens + +Legacy WebLogic SOAP services often used WS-Security (e.g., UsernameToken +profiles or SAML assertions inside XML SOAP headers) configured via WebLogic +policies (`weblogic.wsee.security.*`). + +### After: API Gateway & OIDC Bearer Token Modernization + +In cloud-native GCP architectures, strip WS-Security encryption/authentication +from the application layer: + +1. **Transport Security**: Terminate TLS/HTTPS at Google Cloud Load Balancing + or Google Cloud API Gateway (Apigee / Cloud Endpoints). +2. **Authentication / Authorization**: Require clients to pass standard RFC + 6750 OAuth2 / OIDC **Bearer Tokens** in the HTTP Authorization header + (`Authorization: Bearer `). +3. **Service Validation**: Use Spring Security OAuth2 Resource Server or + Quarkus OIDC to validate the JWT signature and enforce role claims + (`@PreAuthorize` / `@RolesAllowed`) before invoking the SOAP endpoint + implementor. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/analysis_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/analysis_guide.md new file mode 100644 index 0000000000..787c987769 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/analysis_guide.md @@ -0,0 +1,153 @@ +# WebLogic Migration: Analysis & Discovery Guide + +This guide helps you perform a thorough analysis of a WebLogic application to +prepare for migration. + +## Table of Contents + +* [Scanning Patterns](#scanning-patterns) (Line 20) + * [1. WebLogic Specific APIs](#1-weblogic-specific-apis) (Line 25) + * [2. EJB (Enterprise JavaBeans)](#2-ejb-enterprise-javabeans) (Line 35) + * [3. JMS (Java Message Service)](#3-jms-java-message-service) (Line 53) + * [4. JNDI (Java Naming and Directory Interface)](#4-jndi-java-naming-and-directory-interface) (Line 62) + * [5. Web/Servlet Tier](#5-webservlet-tier) (Line 70) + * [6. Advanced WebLogic / Java EE Features](#6-advanced-weblogic--java-ee-features) (Line 81) + * [7. Data Access & Security](#7-data-access--security) (Line 98) + * [8. Cloud-Unfriendly Patterns](#8-cloud-unfriendly-patterns) (Line 106) + +-------------------------------------------------------------------------------- + +## Scanning Patterns + +Use these patterns (e.g., with grep or code search) to identify WebLogic +dependencies and legacy JEE patterns. + +### 1. WebLogic Specific APIs + +Look for imports starting with: + +* `import weblogic.` (Generic WebLogic classes) +* `import weblogic.logging.` (Logging) +* `import weblogic.security.` (Security/JAAS) +* `import weblogic.transaction.` (Transaction management) +* `import weblogic.jdbc.` (Weblogic JDBC extensions) + +### 2. EJB (Enterprise JavaBeans) + +Look for EJB annotations and usage: + +* `@Stateless` +* `@Stateful` +* `@Singleton` +* `@MessageDriven` +* `@Local` +* `@Remote` +* `@EJB` +* `import javax.ejb.` or `import jakarta.ejb.` + +Also look for EJB configuration files: + +* `ejb-jar.xml` +* `weblogic-ejb-jar.xml` + +### 3. JMS (Java Message Service) + +Look for JMS API usage: + +* `import javax.jms.` or `import jakarta.jms.` +* `QueueConnectionFactory`, `TopicConnectionFactory` +* `Queue`, `Topic` +* `MessageListener` + +### 4. JNDI (Java Naming and Directory Interface) + +Look for JNDI lookups: + +* `new InitialContext()` +* `Context.lookup(...)` +* `lookup("java:comp/env/...")` + +### 5. Web/Servlet Tier + +Look for: + +* `web.xml` +* `weblogic.xml` +* `javax.servlet.*` or `jakarta.servlet.*` +* `.jsp` files +* `HttpSession` or `request.getSession()` (identifies stateful sessions to be + migrated) + +### 6. Advanced WebLogic / Java EE Features + +Look for: + +* **Work Managers**: `work-manager` in XML configs or imports of + `commonj.work.*`. +* **Timers**: Usage of `javax.ejb.TimerService` or `commonj.timers.*`. +* **Batch Processing**: `javax.batch.api` or Spring Batch. +* **JMX/MBeans**: `javax.management.MBeanServer` or `weblogic.management`. +* **Resource Adapters**: Files named `ra.xml` or `weblogic-ra.xml` (JCA + connectors). +* **Classloading Customizations**: `prefer-application-packages` or + `prefer-application-resources` in `weblogic.xml` or + `weblogic-application.xml`. +* **Web Services**: `weblogic-webservices.xml` or JAX-WS annotations like + `@WebService`, `@WebMethod`. + +### 7. Data Access & Security + +Look for: + +* **Data Access**: `java.sql.Connection`, `@Entity`, `org.hibernate.Session`, + `persistence.xml`. +* **Security**: `@RolesAllowed`, `@RunAs`, `isUserInRole`. + +### 8. Cloud-Unfriendly Patterns + +Look for: + +* **File I/O**: `java.io.FileOutputStream`, `java.nio.file.Files`. +* **Legacy Remoting**: `java.rmi.*`, `UnicastRemoteObject`. +* **Email**: `javax.mail.*`, `jakarta.mail.*`. +* **Hardcoded IPs / Absolute Paths**. + +-------------------------------------------------------------------------------- + +When Phase 1 and target alignment are complete, generate a migration plan in the +root of the workspace named `wls-migration-plan.md` using the following +structure: + +```markdown +# WebLogic Migration Unified Analysis Report (Migration Plan) + +## 1. Executive Summary +[Brief overview of the application size, dependencies count, and microservices target metrics.] + +## 2. Build & Environment +[Discovered runtime targets, Java version mappings, Maven modules, Ant configs, and checked-in local JAR files.] + +## 3. Technical Inventory +[Quantitative metrics covering EJBs, JMS/JNDI lookup usages, Data Access (JDBC/JPA), Security roles, advanced Work Managers, SOAP web services, and Web tier assets like JSPs and Servlets.] + +## 4. Cloud-Unfriendly Patterns & Blockers +[List of discovered blockers requiring user decisions: Local File I/O, HTTP Sessions, Batch Jobs, RMI/CORBA, and JavaMail usage.] + +## 5. Shared Utility Evaluation +[Excluded utilities confirmed via heuristics/config, followed by a list of high fan-in coupling candidates recommended for manual review.] + +## 6. Web & Presentation Modernization Recommendation +[Custom modernization suggestions addressing server-rendered UI frameworks like Struts/JSPs, servlet routes mapping, and stateless token configuration.] + +## 7. Proposed Decomposition (Topological Analysis) +[The Louvain graph partitioning diagram depicted in Mermaid.js, followed by package, class, and database table distributions matching each proposed microservice service boundary.] + +## 8. Risks, Warnings, & Architectural Call-Outs +[Critical risks or constraints identified during discovery (e.g. shared state, legacy networking, 3rd party libraries with no source). List any security or vulnerability issues, temporary authentication bypasses, unauthorized access paths, or plaintext credentials configured for compilation that must be resolved before production deployment.] + +## 9. Deployment & Infrastructure-as-Code (IaC) Plan +[Dynamically populated during Phase 2 with target Terraform, Kubernetes manifests, or gcloud deployment plans for Cloud Run, Cloud Functions, or GKE.] + +## 10. Testing Strategy +[Dynamically populated during Phase 2 with generative unit/integration testing strategies (e.g. @WebMvcTest / @QuarkusTest) and documentation of disabled legacy Cactus tests.] +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/references/classloading_and_packaging.md b/skills/cloud/google-cloud-weblogic-migration/references/classloading_and_packaging.md new file mode 100644 index 0000000000..b754ff0026 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/classloading_and_packaging.md @@ -0,0 +1,134 @@ +# WebLogic Classloading & Shared Library (`APP-INF/lib`) Modernization Guide + +Enterprise WebLogic EAR deployments rely on complex classloader hierarchies +defined in `weblogic-application.xml` (``, +``), EAR packaging (`APP-INF/lib`, +`APP-INF/classes`), and WebLogic Shared Libraries (``). + +When decomposing an EAR monolith into standalone cloud-native microservices +(Spring Boot Fat JARs or Quarkus Fast-JARs/Containers), classloader filtering +and shared libraries must be restructured into clean, version-managed build +dependencies. + +## Table of Contents + +* [1. Restructuring APP-INF/lib and WebLogic Shared Libraries ()](#1-restructuring-app-inf-lib-and-weblogic-shared-libraries-library-ref) (Line 20) +* [2. Resolving Classloader Conflicts ()](#2-resolving-classloader-conflicts-prefer-web-inf-classes) (Line 89) + +-------------------------------------------------------------------------------- + +## 1. Restructuring `APP-INF/lib` and WebLogic Shared Libraries (``) + +In WebLogic, JAR files placed in an EAR's `APP-INF/lib` directory or registered +as WebLogic Shared Libraries via `` in `weblogic-application.xml` +are shared across all web modules (`.war`) and EJB modules (`.jar`) in the EAR. + +### Before: Legacy WebLogic EAR Structure & `` + +``` +medimed-ear.ear +├── APP-INF/ +│ └── lib/ +│ ├── medimed-domain.jar # Shared DTOs and Entity classes +│ └── medimed-utils.jar # Shared utility classes +├── META-INF/ +│ ├── application.xml +│ └── weblogic-application.xml # Declares to shared-log4j-lib +├── patient-web.war +└── patient-ejb.jar +``` + +### After: Maven Multi-Module Build with Shared Domain & Utils Modules + +In a cloud-native microservice architecture, replace `APP-INF/lib` and +`` by extracting shared domain classes and utility helpers into +standalone **internal Maven modules** or a **Bill of Materials (BOM)**. + +#### Modernized Maven Multi-Module Structure + +``` +medimed-microservices-root/ +├── pom.xml # Parent POM / BOM declaring dependency management +├── medimed-shared-domain/ # Extracted from APP-INF/lib/medimed-domain.jar +│ ├── pom.xml +│ └── src/main/java/... +├── medimed-shared-utils/ # Extracted from APP-INF/lib/medimed-utils.jar +│ ├── pom.xml +│ └── src/main/java/... +├── patient-service/ # Independent Spring Boot or Quarkus microservice +│ ├── pom.xml # Imports medimed-shared-domain and medimed-shared-utils +│ ├── Dockerfile +│ └── src/main/java/... +└── record-service/ # Independent microservice + ├── pom.xml + └── src/main/java/... +``` + +#### Microservice `pom.xml` Dependency Declaration + +```xml + + + + com.acme.medimed + medimed-shared-domain + ${project.version} + + + + + com.acme.medimed + medimed-shared-utils + ${project.version} + + +``` + +-------------------------------------------------------------------------------- + +## 2. Resolving Classloader Conflicts (``) + +In WebLogic, `` or `` in +`weblogic.xml` / `weblogic-application.xml` forces the WebLogic classloader to +load application-bundled JARs (such as older Hibernate versions, custom XML +parsers, or Apache Commons libraries) instead of WebLogic's internal container +libraries. + +### Why Classloader Filtering is Unnecessary in Cloud-Native Containers + +In Spring Boot Fat JARs and Quarkus Fast-JARs running in Docker containers: + +1. **No Application Server Collision**: There is no overarching application + server (like WebLogic or WebSphere) injecting conflicting container + libraries into your classpath. The only libraries present in the JVM are the + exact libraries declared in your `pom.xml` or `build.gradle`. +2. **Clean Dependency Shading**: If third-party transitive dependencies collide + (e.g., two different versions of `guava` or `jackson`), resolve them + declaratively using Maven `` or ``: + +```xml + + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + + + + + org.legacy.library + old-client-sdk + + + + commons-logging + commons-logging + + + + +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/references/decomposition_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/decomposition_guide.md new file mode 100644 index 0000000000..596f31fdc6 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/decomposition_guide.md @@ -0,0 +1,205 @@ +# WebLogic Migration Decomposition Guide + +This guide explains how to use the mathematical analysis CLI to discover +microservice boundaries in a legacy WebLogic monolith during **Phase 1 (Analysis +& Discovery)**. + +> [!NOTE] For detailed scanning patterns, WebLogic-specific API definitions +> (EJBs, JMS, JNDI), and blocker inventories (File I/O, JavaMail, HTTP Sessions, +> RMI/CORBA), refer to [analysis_guide.md](./analysis_guide.md). + +-------------------------------------------------------------------------------- + +## Table of Contents + +* [1. Phase 1 — Internal Discovery & Mathematical Optimization Loop](#1-phase-1--internal-discovery--mathematical-optimization-loop) + (Line 28) + * [Step 1.1: Multi-Resolution Baseline Scan (JSON)](#step-11-multi-resolution-baseline-scan-json) + (Line 34) + * [Step 1.2: Internal Clustering Optimization & Knob Tuning](#step-12-internal-clustering-optimization--knob-tuning-the-refinement-loop) + (Line 45) +* [2. Knob Tuning Troubleshooting Guide](#2-knob-tuning-troubleshooting-guide) + (Line 155) +* [3. Toolchain & Environment Troubleshooting](#3-toolchain--environment-troubleshooting) + (Line 172) + +-------------------------------------------------------------------------------- + +## 1. Phase 1 — Internal Discovery & Mathematical Optimization Loop + +To avoid report bloat and ensure the user only reviews a clean, finalized +microservice architecture, you must follow an iterative internal discovery loop +before advancing to Phase 2: + +### Step 1.1: Multi-Resolution Baseline Scan (JSON) + +First, run the consolidated analysis tool on the target codebase to perform all +scanning and generate candidate boundaries for multiple resolutions in a JSON +format. Save this output to a scratch location (e.g. the `/tmp/` folder). + +```bash +pip install -r /scripts/requirements.txt +python3 /scripts/cli.py analyze /path/to/target/codebase --format json --resolution 0.1 0.5 1.0 1.5 --output /tmp/decomposition_candidates.json +``` + +### Step 1.2: Internal Clustering Optimization & Knob Tuning (The Refinement Loop) + +Before presenting any architecture to the user, you must internally evaluate and +optimize the mathematical decomposition: + +1. **Evaluate Cluster Candidates**: **STRICT RULE: Do NOT rely on your + pre-trained knowledge of the application to guess the architecture + boundaries.** You MUST rely strictly on the mathematical topologies + generated by the CLI. Look at the proposed candidates in the report and YOU + must pick the best one that creates the most logical boundaries for the + business domain. **DO NOT ask the user to pick a resolution.** You must + evaluate the candidates internally, iterate if needed, and only present your + final chosen decomposition to the user in Phase 2. + * **CRITICAL LIMITATION**: The algorithm will NEVER merge completely + disconnected sub-graphs (e.g. standalone singletons or isolated filters) + regardless of how low you set `--resolution`. Do not get stuck in a + loop! Once you pick the best mathematical candidate for the main + connected components, note the remaining tiny disconnected singletons so + you can **manually consolidate** them in the markdown report during + Phase 2. +2. **MANDATORY ITERATION - Exclude Shared Utilities (God Glue)**: Inspect the + top-level `"utility_evaluation"` section of the JSON report (specifically + the `"candidates"` list). Legacy monoliths heavily rely on common base + classes (like `*BaseAction`) or shared data objects. If the report lists ANY + high fan-in candidates, you MUST semantically evaluate their source code. + * **LLM-Assisted Semantic Evaluation**: Read the source files of candidate + utilities. You must distinguish between true business aggregate roots + (e.g. `PatientRecord`, which should NOT be excluded) and domain-agnostic + utilities (e.g. `StringHelper` or `*BaseAction`, which MUST be + excluded). + * **Why?** Failure to do this will result in the mathematical clustering + algorithm merging completely unrelated domains into a giant monolith + because the base classes act as "God Glue" horizontally connecting them. +3. **Universal Framework Discovery & Generalization**: When migrating legacy + WebLogic monoliths, do not assume their codebase matches structures in code + you have seen before. Inspect `static_analysis.web_tier` and + `package_dependencies` in the JSON report to discover the application's + unique presentation frameworks (Struts, Spring MVC, JSF, Servlets, Swing) + and utility namespaces. Ensure you do not overfit: + * **False Presentation Matches**: If an application has business entities + like `PolicyForm` (insurance form) or `BillingClient` (customer client) + and they represent database tables, do not allow the default rules to + strip them into a UI SPA. Scope presentation patterns to specific web/UI + packages. + * **Alternative Web Frameworks**: If the application uses Spring MVC + (`*Controller`, `*View`, `.mvc.`), JSF (`*Bean`, `*Page`, `.jsf.`), GWT + (`*Presenter`), Wicket, or custom servlets/filters, use the custom CLI + knobs to ensure they are parsed and handled cleanly. +4. **Decomposition Tuning Knobs (with Smart Defaults)**: You can tune the + mathematical clustering using the following CLI arguments (which come + pre-configured with smart defaults so they work out-of-the-box across + Struts, Spring MVC, JSF, Servlets, Swing, and standard infrastructure): + * `--presentation-package-pattern` (Optional): Specify custom regex + patterns to append to or override default presentation layer patterns + (smart default covers `.*\.actions\..*`, `.*Controller$`, `.*View$`, + `.*SwingClient$`, etc.). + * `--infrastructure-package-pattern` (Optional): Specify custom regex + patterns for cross-cutting utility or infrastructure noise (smart + default covers `.*Filter$`, `.*Log4jInit$`, `.*Listener$`, + `.*\.util\..*`, etc.). + * `--presentation-mode` (Default: `extract_spa`): + * `extract_spa`: Automatically decouples server-side UI + controllers/servlets/frames into a unified `ui_spa_and_api_gateway` + service (smart default for modern React/Angular migrations). + * `vertical_slice`: UI controllers remain in the graph and group into + the backend domain clusters they invoke (via strong edge weights). + * `ignore`: UI classes are treated as standard nodes in the graph. + * `--weight-domain-anchor` (Default: `5.0`): The weight of matching + Session-to-Entity bindings (e.g. `PatientSession` -> `PatientEJB`). This + should be the strongest bond in the graph to ensure vertical slices + anchor around the domain model. + * `--weight-entry-exclusive` (Default: `3.0`): The weight given to + "Exclusive Entry Points" (UI Actions/Controllers that import EXACTLY 1 + backend Session). Strong weight here ensures the UI merges vertically + with its corresponding backend domain. + * `--weight-entry-glue` (Default: `0.1`): The penalty applied to + "Orchestrator" entry points (Actions/Controllers importing MULTIPLE + backend Sessions). Prevents UI glue from artificially fusing distinct + backend domains. + * `--weight-db-penalty` (Default: `0.01`): The penalty applied to + cross-entity DB relationships (e.g. `PatientEJB` -> `RecordEJB` via + foreign key). Aggressively severs monolithic DB links so the algorithm + does not merge the entire backend into a single cluster. + * `--weight-class-table` (Default: `5.0`): The weight binding domain + classes to the database tables they query or map. Keep data access + operations grouped with their business logic. +5. **MANDATORY EXPERIMENTATION & RE-RUN LOOP**: If the initial clustering + results are suboptimal (e.g., a single giant monolith, over-fragmented + singletons, or UI classes tangled into backend services), **you MUST + actively experiment with the tuning knobs and re-run the CLI**. + * **Consult Troubleshooting Table**: Check Section 2 below to match your + symptom to the exact knob adjustments required. + * **Experiment with Knobs**: Systematically adjust `--resolution`, + `--utility-threshold`, `--weight-domain-anchor`, + `--weight-entry-exclusive`, `--presentation-mode`, or pass custom + regexes to `--presentation-package-pattern` and + `--infrastructure-package-pattern`. + * **Re-Run & Iterate**: Re-run Step 1.1 with your modified parameters and + inspect the new JSON topology. You MUST repeat this experimental loop + internally until you reach a stable, well-balanced mathematical + topology. + +> [!IMPORTANT] **Phase 1 Completion**: Once you have reached a stable, +> mathematically optimized microservice topology in scratch memory, Phase 1 is +> complete. **Do NOT generate `wls-migration-plan.md` or ask technical stack +> alignment questions here.** Advance to Phase 2 in `SKILL.md` and follow +> `migration_plan_guide.md`. + +-------------------------------------------------------------------------------- + +## 2. Knob Tuning Troubleshooting Guide + +Symptom | Probable Cause | Action/Remedy +--------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------- +**Over-fragmented graph / too many tiny singletons** | Louvain resolution is too high | Lower `--resolution` (e.g., to `0.5` or `0.1`). +**Distinct domain components fused into a single giant monolith** | Base classes or common helpers acting as "God Glue" | Lower `--utility-threshold` to flag more utilities, or pass identified base class patterns to `--exclude-patterns`. +**UI/Presentation classes scattered across random backend clusters** | Presentation classes are coupled horizontally | Set `--presentation-mode extract_spa` to isolate UI classes cleanly into `ui_spa_and_api_gateway`. +**UI Controllers/JSPs decoupled from their specific backend domain (in vertical slice mode)** | Exclusive UI edges are too weak | Increase `--weight-entry-exclusive` (e.g., from `3.0` to `8.0`) or switch to `--presentation-mode vertical_slice`. +**Database tables lumped into a single "DB Cluster" or merging backend services together** | Foreign keys or legacy reporting tables acting as database-level "God Glue" | Lower `--weight-db-penalty` (e.g., to `0.001` or `0.0`) to aggressively sever cross-entity DB links, or increase `--weight-class-table` (e.g., to `10.0`) to bind tables strictly to their querying domain classes. +**Domain Entities (`*EJB`) and their Session Facades (`*SessionEJB`) split into separate services** | Session-to-Entity domain anchor bonds are too weak relative to service-to-service calls | Increase `--weight-domain-anchor` (e.g., from `5.0` to `10.0` or `15.0`) to ensure Session EJBs anchor tightly around their underlying domain entities. +**Orchestrator/Dashboard controllers calling multiple domains pull all backend domains into one cluster** | Orchestrator entry points have too much horizontal pull across domains ("Glue Entry Points") | Lower `--weight-entry-glue` (e.g., from `0.1` to `0.01` or `0.0`) so multi-session orchestrators do not artificially fuse distinct business domains. +**Custom UI frameworks (e.g., GWT `*Presenter`, Wicket, or custom desktop frames) mixed into backend services** | Default presentation patterns do not cover the Specific UI naming convention | Pass custom regexes to `--presentation-package-pattern` (e.g., `".*\.gwt\..*",".*Presenter$"`) while using `--presentation-mode extract_spa`. +**Custom cross-cutting classes (`*SecurityInterceptor`, `*AuditListener`, scheduling jobs) create noisy singletons or bridge services** | Unrecognized infrastructure noise escaping default regexes | Pass custom regexes to `--infrastructure-package-pattern` (e.g., `".*\.audit\..*",".*Interceptor$"`) to prune them before clustering. +**Shared DTO / Value Object package (`com.company.vo.*`) captured by one specific domain microservice** | The DTO package is slightly more heavily imported by one domain, but is actually a shared library | Pass the shared DTO/VO package regex to `--exclude-patterns` (e.g., `".*\.vo\..*",".*\.dto\..*"`) so it is treated as a common library/dependency rather than owned by a single microservice. + +-------------------------------------------------------------------------------- + +## 3. Toolchain & Environment Troubleshooting + +The mathematical decomposition CLI is orchestrated by Python scripts that +delegate to specialized analyzers: + +* **`cli.py`**: The entry point that coordinates the analysis pipeline. +* **`static_analyzer.py`**: Runs a Java Maven project under + `scripts/ast_parser` to extract AST-level metrics (EJBs, JMS, JNDI, etc.). +* **`import_analyzer.py`**: Performs lexical analysis of import statements to + construct the class dependency graph. +* **`db_mapper.py`**: Maps database tables to classes by analyzing JPA + annotations and EJB 2.x EJBGen tags in Javadocs. + +### Common Issues & Remedies + +| Symptom | Probable Cause | Action/Remedy | +| ---------------------- | ------------------------ | ------------------------ | +| **AST Parser | JVM incompatibility | The analyzer | +: Warnings/Errors : (e.g., running on JDK : automatically attempts : +: ("Failed to parse X : 22+ or JDK 8). : to locate and use JDK : +: files")** : OpenRewrite requires LTS : 21/17/11. If it fails : +: : JDK (11, 17, or 21). : (warns that it cannot : +: : : find one), install a : +: : : compatible JDK and : +: : : manually set `JAVA_HOME` : +: : : and update `PATH` before : +: : : running the CLI. : +| **Maven | Offline environment or | Verify Maven | +: build/dependency : repository access : installation and check : +: resolution failures in : blocked. : proxy/airlock settings. : +: `ast_parser`** : : The `ast_parser` must : +: : : compile successfully for : +: : : AST metrics fallback to : +: : : be avoided. : diff --git a/skills/cloud/google-cloud-weblogic-migration/references/deployment_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/deployment_guide.md new file mode 100644 index 0000000000..5a8cacc6e5 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/deployment_guide.md @@ -0,0 +1,56 @@ +# WebLogic to Cloud-Native Containerization & Deployment Guide + +This guide provides detailed procedural instructions for executing Phase 5 +(Containerization & Deployment) of the WebLogic migration workflow. + +## 1. Generate Dockerfile + +Create a multi-stage `Dockerfile` optimized for the target framework (including +native compilation instructions for Quarkus if requested). + +* Use multi-stage builds to separate the Maven/Gradle compilation artifact + from the lightweight production runtime image. +* Ensure proper JVM memory flags and non-root container user execution. + +## 2. Generate Build Config + +Create `cloudbuild.yaml` for GCP Cloud Build to automate continuous integration +and image tagging. + +## 3. Generate Deployment Manifests (Generative IaC) + +Once containerization is complete, you must generatively author the +Infrastructure-as-Code (IaC) scripts to deploy the microservices based on the +approved architecture in `wls-migration-plan.md`. + +* **Serverless Targets (Cloud Run / Cloud Functions)**: Generate comprehensive + Terraform configs (`main.tf`, `variables.tf`, `outputs.tf`) or declarative + `gcloud` deployment scripts that wire up the new serverless containers, + configuring CPU/memory limits, concurrency, and VPC connectors. +* **Kubernetes Targets (Google Kubernetes Engine - GKE)**: If GKE was selected + during Phase 2 alignment, generate standard Kubernetes manifests + (`deployment.yaml`, `service.yaml`, `hpa.yaml`, `configmap.yaml`, + `secret-provider-class.yaml`) or Terraform GKE module configs to provision + cluster resources and workload identity bindings. +* **Supporting GCP Infrastructure**: Ensure the Terraform scripts provision + necessary supporting infrastructure mapped in Phase 4 (e.g., Cloud SQL / + AlloyDB instances, Pub/Sub topics/subscriptions, Secret Manager secrets, + Cloud Memorystore Redis clusters, Cloud Storage buckets). + +## 4. Document Cloud-Native Setup + +Create a new `README.md` file inside the root `wls_migration/` folder and inside +each microservice module's subdirectory. This document serves as the operational +guide for the new cloud-native application, detailing: + +* The microservices module layout and local execution scripts (e.g., + Maven/Gradle wrappers). +* Environment variables required (including GCP Secret Manager references for + database credentials and API keys). +* **Security Documentation**: Detail the authentication and authorization + setup, including JWT cryptographic signature details (e.g., HS256/RS256, + public/private keys configurations), OIDC JWKS cert endpoints (where + applicable), or explicitly flag if any temporary mock security is currently + active. +* Instructions on how to trigger deployments using the generated Cloud Build + or Terraform configs. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/distributed_transactions.md b/skills/cloud/google-cloud-weblogic-migration/references/distributed_transactions.md new file mode 100644 index 0000000000..e92de7f6ac --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/distributed_transactions.md @@ -0,0 +1,198 @@ +# WebLogic to GCP: Distributed Transactions Migration Guide + +Legacy WebLogic applications often rely on **JTA (Java Transaction API)** and +**XA (eXtended Architecture)** to perform distributed transactions (Two-Phase +Commit / 2PC) across multiple resources, such as: + +* Updating multiple databases in a single transaction. +* Updating a database and sending a JMS message in a single transaction (DB + + JMS). + +## Table of Contents + +* [The Serverless/Cloud-Native Challenge](#the-serverlesscloud-native-challenge) (Line 21) +* [1. Discovery: Identifying XA/2PC Usage](#1-discovery-identifying-xa2pc-usage) (Line 36) +* [2. Migration Pattern: Transactional Outbox (DB + Messaging)](#2-migration-pattern-transactional-outbox-db-messaging) (Line 53) +* [3. Migration Pattern: Saga Pattern (Multi-Service/DB)](#3-migration-pattern-saga-pattern-multi-servicedb) (Line 146) +* [4. Crucial Requirement: Idempotency](#4-crucial-requirement-idempotency) (Line 184) + +-------------------------------------------------------------------------------- + +## The Serverless/Cloud-Native Challenge + +Distributed transactions (2PC) are generally **anti-patterns** in cloud-native +microservices and serverless architectures for the following reasons: + +1. **High Latency**: 2PC requires locking resources across multiple systems + until the transaction completes, which drastically reduces throughput. +2. **Single Point of Failure**: If the transaction coordinator (which would + have to run on Cloud Run) crashes during the commit phase, resources can + remain locked indefinitely. +3. **Scale-to-Zero Compatibility**: Cloud Run instances can be terminated + abruptly, making them unreliable transaction coordinators. + +-------------------------------------------------------------------------------- + +## 1. Discovery: Identifying XA/2PC Usage + +During Phase 1 (Analysis), look for: + +* **XA Datasource Configurations**: In WebLogic config or deployment + descriptors (look for `javax.sql.XADataSource` or driver class names + containing `XA`). +* **UserTransaction Usage**: Explicit programmatic transaction demarcation + using `javax.transaction.UserTransaction` or + `jakarta.transaction.UserTransaction`. +* **EJB CMT with XA**: EJBs configured to use container-managed transactions + that span multiple datasources or JMS resources. +* **Spring JtaTransactionManager**: If the application already uses Spring but + configured with a JTA manager (e.g., `WebLogicJtaTransactionManager`). + +-------------------------------------------------------------------------------- + +## 2. Migration Pattern: Transactional Outbox (DB + Messaging) + +Many legacy applications use XA to ensure that a database update and a JMS +message send either both succeed or both fail. + +### Legacy XA Pattern (Before) + +``` +[Client] -> [WebLogic EJB] + | + +--> (Start JTA Transaction) + | + +--> [Database] (Insert Order) + +--> [JMS Queue] (Send OrderCreated Event) + | + +--> (Commit JTA Transaction - 2PC) +``` + +### Cloud-Native Alternative: Transactional Outbox (After) + +Instead of 2PC, write the event to an `outbox` table in the *same* database as +the business data, using a local transaction. A separate, reliable process then +reads from the outbox table and publishes to GCP Pub/Sub. + +``` +[Client] -> [Cloud Run Service] + | + +--> (Start Local Transaction) + +--> [Cloud SQL] (Insert Order & Insert Outbox Event) + +--> (Commit Local Transaction) + +[Outbox Publisher (CDC/Poller)] -> Reads Outbox -> Publishes to [GCP Pub/Sub] +``` + +#### Code Implementation (Spring Boot Outbox Pattern) + +1. **Entity and Repository**: + + ```java + @Entity + @Table(name = "outbox") + public class OutboxEvent { + @Id @GeneratedValue + private Long id; + private String aggregateType; + private String aggregateId; + private String type; + private String payload; // JSON payload + // getters/setters + } + ``` + +2. **Service (Writing Business Data + Outbox)**: + + ```java + @Service + public class OrderService { + @Autowired private OrderRepository orderRepository; + @Autowired private OutboxRepository outboxRepository; + @Autowired private ObjectMapper objectMapper; + + @Transactional // Local transaction + public void placeOrder(Order order) throws Exception { + // 1. Save order + orderRepository.save(order); + + // 2. Save event to outbox + OutboxEvent event = new OutboxEvent(); + event.setAggregateType("Order"); + event.setAggregateId(order.getId()); + event.setType("OrderCreated"); + event.setPayload(objectMapper.writeValueAsString(order)); + outboxRepository.save(event); + } + } + ``` + +3. **Publisher (CDC / Poller with Concurrency Control)**: + + * **Change Data Capture (CDC)**: Recommended for enterprise production. + Use tools like **Debezium** or **Google Cloud Datastream** to stream + outbox inserts directly from the PostgreSQL/MySQL transaction log + without querying the table. + * **Scheduled Polling (Multi-Instance Safe)**: If polling via a scheduled + Spring/Quarkus task across horizontally scaled Cloud Run replicas, you + MUST prevent concurrent workers from publishing duplicate events by + using row-level locking with **`SELECT ... FOR UPDATE SKIP LOCKED`**: + `sql -- PostgreSQL / MySQL 8.0+ concurrency-safe outbox polling SELECT * + FROM outbox WHERE processed = false ORDER BY id ASC LIMIT 50 FOR UPDATE + SKIP LOCKED;` + +-------------------------------------------------------------------------------- + +## 3. Migration Pattern: Saga Pattern (Multi-Service/DB) + +If the legacy application updates multiple databases, you should split it into +microservices, each owning its database, and use the **Saga Pattern** to manage +consistency. + +A Saga is a sequence of local transactions. Each local transaction updates the +database and publishes a message/event. If a step fails, the Saga executes +**compensating transactions** to undo the changes made by the preceding local +transactions. + +### Types of Sagas: + +1. **Choreography**: Participants event-drivenly listen to events and execute + their local transactions. +2. **Orchestration**: A central controller (Orchestrator) tells the + participants what local transactions to execute. + +### GCP Implementation with Cloud Workflows (Orchestration) + +You can use **Google Cloud Workflows** as a serverless orchestrator to manage a +Saga across multiple Cloud Run services. + +```mermaid +graph TD + Start --> CreateOrder[Cloud Run: Create Order] + CreateOrder -- Success --> ReserveCredit[Cloud Run: Reserve Credit] + ReserveCredit -- Success --> ConfirmOrder[Cloud Run: Confirm Order] + ReserveCredit -- Fail --> CompensateOrder[Cloud Run: Cancel Order] + ConfirmOrder --> End +``` + +* **Cloud Workflows** handles the state machine, retries, and compensation + logic. +* Each step in the workflow calls a REST API on a Cloud Run microservice. + +-------------------------------------------------------------------------------- + +## 4. Crucial Requirement: Idempotency + +Because the Outbox pattern and GCP Pub/Sub guarantee **at-least-once delivery**, +messages may be delivered more than once. + +> [!IMPORTANT] All consumers (subscribers) of events in the migrated system MUST +> be **idempotent**. They must be able to handle duplicate messages without +> causing inconsistent state. + +### Implementing Idempotency + +* **Unique Message IDs**: Track processed message IDs in a database table. + Before processing, check if the ID has already been processed. +* **Idempotent Business Logic**: Design operations to be naturally idempotent + (e.g., "Set status to PAID" instead of "Deduct amount"). diff --git a/skills/cloud/google-cloud-weblogic-migration/references/gcp_mapping.md b/skills/cloud/google-cloud-weblogic-migration/references/gcp_mapping.md new file mode 100644 index 0000000000..9d13f46f3a --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/gcp_mapping.md @@ -0,0 +1,375 @@ +# WebLogic to GCP Resource Mapping Guide + +This guide describes how to map WebLogic infrastructure resources (Datasources, +JMS) to Google Cloud Platform (GCP) services. + +## Table of Contents + +* [1. Database: WebLogic Datasource to Cloud SQL](#1-database-weblogic-datasource-to-cloud-sql) + (Line 21) +* [2. Messaging: WebLogic JMS to GCP Pub/Sub](#2-messaging-weblogic-jms-to-gcp-pubsub) + (Line 81) +* [3. Secrets & Credentials: WebLogic Security Realms to GCP Secret Manager](#3-secrets--credentials-weblogic-security-realms-to-gcp-secret-manager) + (Line 214) +* [4. File Storage: Local File I/O to Google Cloud Storage (GCS) or Filestore](#4-file-storage-local-file-io-to-google-cloud-storage-gcs-or-filestore) + (Line 267) +* [5. Caching & Session State: WebLogic Sessions & Coherence to Cloud Memorystore (Redis)](#5-caching--session-state-weblogic-sessions--coherence-to-cloud-memorystore-redis) + (Line 318) + +-------------------------------------------------------------------------------- + +## 1. Database: WebLogic Datasource to Cloud SQL + +WebLogic applications typically use JNDI to look up a `DataSource` configured in +the WebLogic console. In a serverless environment, database connection details +are provided via environment variables or configuration files, and connections +are managed by the application framework. + +### Recommended Target: GCP Cloud SQL + +Use Cloud SQL (PostgreSQL, MySQL, or SQL Server) and connect using the **Cloud +SQL JDBC Socket Factory** for secure, IAM-authorized connections without needing +public IPs. + +### Spring Boot Configuration + +Add dependency: + +```xml + + com.google.cloud + spring-cloud-gcp-starter-sql-postgresql + +``` + +Configure `application.properties`: + +```properties +# Using Spring Cloud GCP starter which automatically configures the datasource +spring.cloud.gcp.sql.database-name=mydb +spring.cloud.gcp.sql.instance-connection-name=my-project:us-central1:my-instance +# If using IAM database authentication: +spring.datasource.username=iam-user@my-project.iam.gserviceaccount.com +# Password is not required when using IAM auth with the socket factory (it uses token) +``` + +### Quarkus Configuration + +Add dependency: + +```xml + + io.quarkus + quarkus-jdbc-postgresql + + +``` + +If using Cloud SQL Auth Proxy as a sidecar (common pattern for Cloud Run): +Configure `application.properties`: + +```properties +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=dbuser +quarkus.datasource.password=dbpass +# Connect to localhost where the proxy is running +quarkus.datasource.jdbc.url=jdbc:postgresql://127.0.0.1:5432/mydb +``` + +-------------------------------------------------------------------------------- + +## 2. Messaging: WebLogic JMS to GCP Pub/Sub + +WebLogic JMS (Java Message Service) queues and topics should be migrated to +**Google Cloud Pub/Sub** (or Apache Kafka on GCP if order preservation and +replayability are critical, but Pub/Sub is preferred for serverless simplicity). + +### Mapping Concept + +* **WebLogic JMS Queue/Topic** -> **GCP Pub/Sub Topic** +* **WebLogic JMS Consumer (or EJB MDB)** -> **GCP Pub/Sub Subscription** + + **Subscriber Code** + +### Spring Boot Refactoring (using Spring Cloud GCP Pub/Sub) + +Add dependency: + +```xml + + com.google.cloud + spring-cloud-gcp-starter-pubsub + +``` + +#### Refactoring a Message-Driven Bean (MDB) + +**Legacy EJB MDB:** + +```java +import javax.ejb.MessageDriven; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.TextMessage; + +@MessageDriven(name = "OrderProcessorMDB") +public class OrderProcessorMDB implements MessageListener { + public void onMessage(Message message) { + try { + if (message instanceof TextMessage) { + String body = ((TextMessage) message).getText(); + // Process order + } + } catch (Exception e) { + // handle error + } + } +} +``` + +**Spring Boot Pub/Sub Subscriber:** + +```java +import com.google.cloud.pubsub.v1.AckReplyConsumer; +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import com.google.cloud.spring.pubsub.support.BasicAcknowledgeablePubsubMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class OrderProcessorSubscriber { + + @Autowired + private PubSubTemplate pubSubTemplate; + + @EventListener(ApplicationReadyEvent.class) + public void subscribe() { + pubSubTemplate.subscribe("order-subscription", (BasicAcknowledgeablePubsubMessage message) -> { + String payload = message.getPubsubMessage().getData().toStringUtf8(); + try { + // Process order payload + message.ack(); + } catch (Exception e) { + message.nack(); + } + }); + } +} +``` + +### Quarkus Refactoring (using Google Cloud Pub/Sub Client) + +Add dependency: + +```xml + + io.quarkiverse.googlecloudservices + quarkus-google-cloud-pubsub + +``` + +**Quarkus Pub/Sub Subscriber:** + +```java +import com.google.cloud.pubsub.v1.MessageReceiver; +import com.google.cloud.pubsub.v1.Subscriber; +import com.google.pubsub.v1.ProjectSubscriptionName; +import com.google.pubsub.v1.PubsubMessage; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class OrderProcessorSubscriber { + + @ConfigProperty(name = "gcp.project.id") + String projectId; + + void onStart(@Observes StartupEvent ev) { + ProjectSubscriptionName subscriptionName = ProjectSubscriptionName.of(projectId, "order-subscription"); + + MessageReceiver receiver = (PubsubMessage message, com.google.cloud.pubsub.v1.AckReplyConsumer consumer) -> { + String payload = message.getData().toStringUtf8(); + try { + // Process order + consumer.ack(); + } catch (Exception e) { + consumer.nack(); + } + }; + + Subscriber subscriber = Subscriber.newBuilder(subscriptionName, receiver).build(); + subscriber.startAsync().awaitRunning(); + } +} +``` + +*(Note: Quarkus can also use SmallRye Reactive Messaging with a Pub/Sub +connector, which provides a more declarative model, similar to MDBs).* + +-------------------------------------------------------------------------------- + +## 3. Secrets & Credentials: WebLogic Security Realms to GCP Secret Manager + +Legacy WebLogic applications often store database passwords, third-party API +keys, and security realm credentials in plaintext properties files or domain +descriptors (`config.xml`). In a cloud-native serverless environment, hardcoded +secrets must be externalized to **Google Cloud Secret Manager**. + +### Recommended Target: GCP Secret Manager + +Store sensitive configuration values in Secret Manager and inject them at +runtime as environment variables in Cloud Run / Cloud Functions, or fetch them +programmatically using Spring/Quarkus Secret Manager extensions. + +### Spring Boot Configuration + +Add dependency: + +```xml + + com.google.cloud + spring-cloud-gcp-starter-secretmanager + +``` + +Configure `application.properties`: + +```properties +# Reference Secret Manager secrets directly in Spring properties using sm:// syntax +spring.datasource.password=${sm://projects/my-project/secrets/db-password/versions/latest} +api.external.key=${sm://my-api-key} +``` + +### Quarkus Configuration + +Add dependency: + +```xml + + io.quarkiverse.googlecloudservices + quarkus-google-cloud-secret-manager + +``` + +Configure `application.properties`: + +```properties +# Reference Secret Manager secrets using gcp-secret-manager: prefix +quarkus.datasource.password=${gcp-secret-manager:db-password} +api.external.key=${gcp-secret-manager:projects/my-project/secrets/api-key/versions/latest} +``` + +-------------------------------------------------------------------------------- + +## 4. File Storage: Local File I/O to Google Cloud Storage (GCS) or Filestore + +WebLogic applications frequently write uploaded documents, generated reports, or +temporary files directly to the local filesystem (`java.io.File` or +`java.nio.file.Files`). Because serverless containers (Cloud Run / Cloud +Functions) have ephemeral, stateless local filesystems, local file I/O must be +refactored. + +### Recommended Targets: + +* **Google Cloud Storage (GCS)**: Recommended for object storage (user + uploads, images, PDFs, reports). +* **Google Cloud Filestore (Managed NFS)**: Recommended if legacy code + requires POSIX-compliant shared file locking or directory hierarchies + without refactoring. +* **Google Cloud Parallelstore**: Recommended for high-throughput scratch + space in analytics/computing workloads. + +### Refactoring to Google Cloud Storage (Spring Boot & Quarkus) + +**Legacy File I/O Snippet:** + +```java +File targetDir = new File("/var/app/uploads/"); +FileOutputStream fos = new FileOutputStream(new File(targetDir, fileName)); +fos.write(fileBytes); +fos.close(); +``` + +**Modernized GCS Client Snippet (Java 17+ / 21+):** + +```java +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; + +public class CloudStorageService { + private final Storage storage = StorageOptions.getDefaultInstance().getService(); + private final String bucketName = System.getenv().getOrDefault("GCS_BUCKET_NAME", "my-app-uploads"); + + public void uploadFile(String fileName, byte[] fileBytes) { + BlobId blobId = BlobId.of(bucketName, fileName); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("application/octet-stream").build(); + storage.create(blobInfo, fileBytes); + } +} +``` + +-------------------------------------------------------------------------------- + +## 5. Caching & Session State: WebLogic Sessions & Coherence to Cloud Memorystore (Redis) + +Monolithic WebLogic applications often rely on in-memory HTTP session +replication across cluster nodes or Oracle Coherence distributed caches. In +serverless cloud architectures, instances scale to zero and are completely +stateless. + +### Recommended Target: Google Cloud Memorystore for Redis + +Externalize HTTP session state and distributed application caching to **Cloud +Memorystore (Redis)**. + +### Spring Boot Configuration (Spring Session Data Redis) + +Add dependency: + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.session + spring-session-data-redis + +``` + +Configure `application.properties`: + +```properties +# Enable Spring Session backed by Redis +spring.session.store-type=redis +spring.data.redis.host=10.0.0.5 +spring.data.redis.port=6379 +``` + +### Quarkus Configuration (Quarkus Redis Cache) + +Add dependency: + +```xml + + io.quarkus + quarkus-redis-client + + + io.quarkus + quarkus-cache + +``` + +Configure `application.properties`: + +```properties +# Configure Redis connection for Quarkus caching +quarkus.redis.hosts=redis://10.0.0.5:6379 +quarkus.cache.type=redis +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/references/legacy_remoting_and_jca.md b/skills/cloud/google-cloud-weblogic-migration/references/legacy_remoting_and_jca.md new file mode 100644 index 0000000000..000c69c7aa --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/legacy_remoting_and_jca.md @@ -0,0 +1,204 @@ +# WebLogic Legacy Remoting & JCA (Java Connector Architecture) Modernization Guide + +This guide provides concrete transformation recipes for modernizing legacy +WebLogic remoting protocols (T3, RMI, CORBA/IIOP, EJB 2.x Remote Home +interfaces) and JCA Resource Adapters (`.rar`) when migrating to cloud-native +serverless microservices on GCP (Spring Boot or Quarkus on Cloud Run or Cloud +Functions). + +## Table of Contents + +* [1. Legacy Remoting: T3 Protocol, RMI/IIOP, & EJB 2.x Remote Homes](#1-legacy-remoting-t3-protocol-rmiiiop--ejb-2x-remote-homes) (Line 16) +* [2. JCA (Java Connector Architecture) & Mainframe Resource Adapters (.rar)](#2-jca-java-connector-architecture--mainframe-resource-adapters-rar) (Line 137) + +-------------------------------------------------------------------------------- + +## 1. Legacy Remoting: T3 Protocol, RMI/IIOP, & EJB 2.x Remote Homes + +In legacy WebLogic multi-JVM architectures, clients and server components +communicate across network boundaries using WebLogic's specific T3 protocol +(`t3://`) or RMI over IIOP. + +### Before: Legacy WebLogic Remoting Pattern + +Clients perform JNDI lookups using specific initial context factories and +provider URLs, casting results to remote EJB home interfaces that throw +`java.rmi.RemoteException`: + +```java +// Legacy WebLogic T3 / RMI EJB Client Lookup +Properties props = new Properties(); +props.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); +props.put(Context.PROVIDER_URL, "t3://weblogic-prod-01:7001,weblogic-prod-02:7001"); + +InitialContext ctx = new InitialContext(props); +Object obj = ctx.lookup("ejb/PatientRecordRemoteHome"); +PatientRecordHome home = (PatientRecordHome) PortableRemoteObject.narrow(obj, PatientRecordHome.class); +PatientRecord remoteEJB = home.create(); + +try { + PatientDTO patient = remoteEJB.getPatientDetails(1042L); +} catch (RemoteException re) { + // Handle network / RMI failure +} +``` + +### After: Cloud-Native REST & gRPC Modernization on GCP + +In a cloud-native serverless environment (GKE / Cloud Run / Cloud Functions), +proprietary binary protocols like T3 and RMI/IIOP are blocked by modern HTTP +load balancers and firewalls. All inter-service communication must be decoupled +into stateless HTTP/S protocols. + +#### Strategy A: REST over HTTPS with JSON Payloads (Recommended for general microservices) + +1. **Server Side (Target Microservice)**: Rewrite the Remote EJB interface and + implementation into a stateless REST controller: + + * **Spring Boot**: + + ```java + @RestController + @RequestMapping("/api/v1/patients") + public class PatientRecordController { + @Autowired + private PatientService patientService; + + @GetMapping("/{id}") + public ResponseEntity getPatientDetails(@PathVariable Long id) { + return ResponseEntity.ok(patientService.getPatientDetails(id)); + } + } + ``` + + * **Quarkus (JAX-RS)**: + + ```java + @Path("/api/v1/patients") + @Produces(MediaType.APPLICATION_JSON) + public class PatientRecordResource { + @Inject + PatientService patientService; + + @GET + @Path("/{id}") + public PatientDTO getPatientDetails(@PathParam("id") Long id) { + return patientService.getPatientDetails(id); + } + } + ``` + +2. **Client Side (Calling Microservice)**: Replace + `InitialContext.lookup("t3://...")` and `RemoteException` handling with + declarative HTTP clients or `RestClient`: + + * **Spring Boot (Spring Cloud OpenFeign or RestClient)**: + + ```java + @Service + public class BillingService { + @Autowired + private RestClient patientRestClient; // Configured with target Cloud Run service URL + + public void processBilling(Long patientId) { + try { + PatientDTO patient = patientRestClient.get() + .uri("/api/v1/patients/{id}", patientId) + .retrieve() + .body(PatientDTO.class); + } catch (RestClientResponseException ex) { + // Handle HTTP error status codes (404 Not Found, 503 Service Unavailable) instead of RemoteException + } + } + } + ``` + + * **Quarkus (MicroProfile Rest Client)**: + + ```java + @RegisterRestClient(configKey = "patient-api") + @Path("/api/v1/patients") + public interface PatientClient { + @GET + @Path("/{id}") + PatientDTO getPatientDetails(@PathParam("id") Long id); + } + ``` + +#### Strategy B: gRPC over HTTP/2 (Recommended for ultra-high performance internal RPCs) + +When legacy RMI was used for high-frequency, low-latency internal binary +messaging between tightly coupled backend modules, map the remote interface to a +Protocol Buffers (`.proto`) schema and deploy gRPC services over Cloud Run +internal VPC networks. + +-------------------------------------------------------------------------------- + +## 2. JCA (Java Connector Architecture) & Mainframe Resource Adapters (`.rar`) + +Enterprise WebLogic monoliths in banking and insurance frequently connect to +legacy mainframe systems (CICS, IMS, AS/400, SAP) or proprietary ERPs using JCA +Resource Adapters deployed as `.rar` archives in WebLogic, interacting via +`javax.resource.cci.*` (Common Client Interface). + +### Before: Legacy WebLogic JCA / CCI Pattern + +```java +// Legacy WebLogic JCA CCI Lookup +InitialContext ctx = new InitialContext(); +ConnectionFactory cf = (ConnectionFactory) ctx.lookup("eis/CicsMainframeAdapter"); +Connection conn = cf.getConnection(); +Interaction interaction = conn.createInteraction(); + +CicsRecordInput input = new CicsRecordInput("TXN_8842"); +CicsRecordOutput output = new CicsRecordOutput(); + +// Execute synchronous mainframe transaction over JCA adapter +interaction.execute(null, input, output); +conn.close(); +``` + +### After: Cloud-Native Mainframe Integration on GCP + +In serverless container environments (Cloud Run / Cloud Functions), deploying +embedded JCA `.rar` resource adapters is unsupported and anti-pattern. + +#### Strategy 1: Google Cloud Integration Connectors (Apigee / Application Integration) + +Instead of embedding mainframe proprietary drivers directly inside microservice +containers, offload legacy EIS connectivity to **Google Cloud Integration +Connectors** or **Apigee Mainframe Adapters**: + +1. Configure an Integration Connector resource in GCP pointing to the + CICS/IMS/SAP mainframe endpoint. +2. Refactor the microservice code to make a standard REST API call or gRPC + invocation to the Integration Connector endpoint. + +```java +// Cloud-Native Mainframe Invocation via GCP Integration Connector / REST Proxy +@Service +public class MainframeIntegrationService { + @Autowired + private RestClient gcpConnectorClient; + + public CicsResponse executeTransaction(String txnId) { + return gcpConnectorClient.post() + .uri("/v1/projects/my-project/locations/us-central1/connections/cics-connector:execute") + .body(new CicsRequest(txnId)) + .retrieve() + .body(CicsResponse.class); + } +} +``` + +#### Strategy 2: Asynchronous Mainframe Messaging over GCP Pub/Sub + +If the legacy JCA adapter was used for inbound message listening or asynchronous +transaction processing: + +1. Re-route mainframe outbound queues (e.g., IBM MQ or Mainframe Event Streams) + into **Google Cloud Pub/Sub topics** (via Google Cloud Pub/Sub Connector or + Kafka Bridge). +2. Replace JCA message listeners in the microservice with standard **Spring + Cloud GCP Pub/Sub `@GcpPubSubSubscription` listeners** or **Quarkus Reactive + Messaging `@Incoming` annotations**. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/migration_plan_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/migration_plan_guide.md new file mode 100644 index 0000000000..627166baa4 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/migration_plan_guide.md @@ -0,0 +1,263 @@ +# WebLogic Migration: Technical Stack Alignment & Plan Review Guide + +This guide explains how to conduct dynamic technical stack alignment with the +user and manage the interactive review and refinement loop for +`wls-migration-plan.md` during **Phase 2 (Technical Stack Alignment & +Decomposition Review)**. + +-------------------------------------------------------------------------------- + +## Table of Contents + +* [1. Dynamic & Context-Relevant Technical Stack Alignment](#1-dynamic--context-relevant-technical-stack-alignment) + (Line 22) +* [2. Initial Generation of wls-migration-plan.md](#2-initial-generation-of-wls-migration-planmd) + (Line 131) +* [3. Interactive Human Reviewer Feedback & Refinement Loop](#3-interactive-human-reviewer-feedback--refinement-loop) + (Line 213) +* [4. Review Findings Checklist](#4-review-findings-checklist) (Line 243) + +-------------------------------------------------------------------------------- + +## 1. Dynamic & Context-Relevant Technical Stack Alignment + +Once you have finalized your microservice topology and exclusion patterns +internally in Phase 1, you must proactively align with the user on target cloud +modernization choices using the `ask_question` tool. Do not write the +`wls-migration-plan.md` artifact yet. + +### Dynamic Alignment Rules + +The technical stack alignment questions listed below represent strong, highly +relevant baseline examples. However, **you must NOT treat this as a rigid, +static checklist**. Based on your static analysis and blocker discovery in Phase +1, you must dynamically evaluate what technical choices and architectural +decisions are **genuinely relevant** to *this specific monolith*: + +* **Add Custom Questions for Discovered Blockers**: If Phase 1 uncovered + unique middleware dependencies, custom JCA resource adapters, + weblogic-specific security providers (JAAS LoginModules), RMI/CORBA + integrations, third-party scheduling libraries (Quartz/EJB Timers), or + weblogic-specific caching (Coherence), you MUST formulate and ask custom + clarifying questions to align on their modernization strategy. +* **Omit Irrelevant Questions**: If the application has no database + interactions or no local file I/O, do NOT ask database or file storage + questions. +* **Skip Pre-Answered Questions**: If the user's initial prompt or workspace + context already explicitly specifies target selections (e.g. they requested + "Migrate this app to Spring Boot on GKE"), do **not** ask those questions. + Automatically pre-fill the selections and proceed. +* **Consolidate Questions**: Group all independent alignment questions (e.g., + target framework, GCP target service, UI modernization, database choice, + JMS, sessions) into a single `ask_question` tool call using its native array + structure. This allows the user to answer them all in a single UI modal, + reducing human-in-the-loop wait times from multiple turns to one. Only ask + sequential questions if a later question strictly depends on the answer to a + previous one. +* **Tool Execution Fallback**: If the `ask_question` tool is available (e.g. + in antigravity), you MUST use it to present selectable options. If you are + running in a generic orchestrator where `ask_question` is not available, + print the questions and options together as standard markdown text in your + response and wait for the user to reply. +* **Ensure Unambiguous Questions & Options**: All questions and options MUST + be clear, precise, and single-focused. Do NOT group distinct technical + choices into a single option (e.g., do not combine "React/Angular" into + "React/Angular SPA"; instead, list "React SPA" and "Angular SPA" as separate + options). The options must represent concrete, actionable decisions that the + agent can execute during the refactoring phase. +* Always include an opinionated, contextually relevant recommendation as the + first option (prefix the option text with `(Recommended)`), with a short one + sentence justification. + +### Baseline Alignment Questions (Ask Sequentially as Relevant) + +1. **Select Target Framework and Build System**: e.g., `(Recommended) Spring + Boot with Maven` (if Spring is partially used), or `(Recommended) Quarkus + with Maven` (for optimal serverless performance). +2. **Select Target GCP Service**: e.g. `(Recommended) Google Kubernetes Engine` + (for most enterprise apps), `(Recommended) Cloud Run` (for most web apps) or + `(Recommended) Cloud Functions` (for event-driven tasks). +3. **Modernize Web Tier (Presentation Layer)**: Ask how to modernize legacy + JSPs/Struts UI. Provide options: + + * `(Recommended) Decouple into a standalone Angular SPA calling REST APIs` + * `(Recommended) Decouple into a standalone React SPA calling REST APIs` + * `Refactor into server-rendered Spring Boot Thymeleaf` + * `Port JSPs directly to embedded Tomcat Jasper` + +4. **Confirm Packaging Preference**: e.g., `(Recommended) Maven Multi-module in + a Single Repo with a separation of SPA application from the backend`. + +5. **Address Cloud-Unfriendly Patterns & State**: Explicitly prompt the user + for architectural decisions on discovered blockers using the tool. Provide + options like: + + * **HTTP Sessions**: e.g., `(Recommended) Migrate stateful sessions to + Google Cloud Memorystore (Redis)` or `Refactor to stateless JWTs` + * **Batch Processing**: e.g., `(Recommended) Extract batch jobs into + independent Cloud Run Jobs` + * **Email (JavaMail)**: e.g., `(Recommended) Route emails through SendGrid + API` + * **Legacy Remoting (RMI/CORBA)**: e.g., `(Recommended) Refactor to REST + over HTTP` + +6. **Modernize File Storage (if File I/O is detected)**: If the application + contains local file operations (`java.io` or `java.nio`), ask how to + modernize file storage. Provide options: + + * `(Recommended) Migrate local file access to Google Cloud Storage (GCS) + (standard cloud object storage)` + * `Mount Google Cloud Filestore (NFS) (shared POSIX-compliant file system + for legacy file operations and concurrent locking)` + * `Mount Google Cloud Parallelstore (Managed Lustre) (ultra-high + performance scratch space for heavy computing/analytics workloads)` + +7. **Select Target Cloud Database (if Database / Oracle / PointBase + dependencies are detected)**: If the application queries relational + databases or contains Oracle/PointBase SQL dialects (`(+)` joins, `NVL`, + `DUAL`, sequences), ask how to modernize the database layer. Provide + options: + + * `(Recommended) Google Cloud SQL for PostgreSQL (fully managed relational + database with standard ANSI SQL / dialect translation)` + * `(Recommended) Google Cloud AlloyDB for PostgreSQL (ultra-high + performance PostgreSQL-compatible enterprise database)` + * `Google Cloud SQL for MySQL` + * `Google Cloud SQL for SQL Server` + * `Google Cloud Spanner (globally distributed relational database)` + +-------------------------------------------------------------------------------- + +## 2. Initial Generation of `wls-migration-plan.md` + +Once target stack choices and architectural decisions are aligned, generate the +formal migration plan for human review: + +1. Run the analysis script a final time targeting **only** your selected + resolution in **markdown** format. Output the result to scatch space + (e.g.`/tmp/wls_migration_report_raw.md`). Pass your finalized + `--exclude-patterns` and edge weights to the CLI. + + ```bash + python3 /scripts/cli.py analyze /path/to/target/codebase --format markdown --resolution 1.0 --exclude-patterns ".*Base.*" --output /tmp/wls_migration_report_raw.md + ``` + +2. **Enforce Template Integrity**: Structure your final `wls-migration-plan.md` + to match the **exact 10-section template** defined in + [analysis_guide.md](./analysis_guide.md#L108-L139). Do NOT simply dump the + raw CLI output. You MUST copy, reorganize, and enrich the CLI output to fit + the template. Ensure NO sections are omitted. + +3. **Integrate Alignments**: Explicitly integrate the user's dynamic technical + stack selections (such as React/Angular SPA, Spring/Quarkus, and specific + cloud data mappings) into the appropriate report sections. + +4. **Populate Shared Utility Evaluation (Section 5)**: Explain the + architectural strategy for the excluded classes (e.g., DTOs/Value Objects). + Define whether they will be shared via a common Maven module, duplicated, or + refactored. + +5. **Populate Risks, Warnings, & Architectural Call-Outs (Section 8)**: + Document all identified risks, including: + + * Any tool failures during Phase 1 (e.g., if the AST parser crashed and + you had to fall back to lexical metrics, document this as a risk to + boundary accuracy). + * Database translation risks (e.g., PointBase/Oracle proprietary SQL + features). + * Security concerns (e.g., hardcoded credentials, custom authenticator + migration). + +6. **Manual Singleton Consolidation**: If the raw CLI report contains + disconnected singletons (e.g. standalone web filters or 1-class actions + without database tables) that Louvain could not merge, you MUST manually + consolidate them in the markdown report (e.g. folding DAOs into their + respective business services or grouping filters into an API Gateway/common + library). + +7. **Append Contextualized Deployment & Testing Strategies**: You must + dynamically author and append two new sections to the bottom of the report + based on the user's specific alignment choices: + + * **9. Deployment & Infrastructure-as-Code (IaC) Plan**: Explicitly list + which Terraform scripts (`main.tf`, `variables.tf`, `outputs.tf`), + Kubernetes manifests, or `gcloud` commands you will generatively author + in Phase 5 to deploy their specific target (e.g., Cloud Run vs Cloud + Functions vs GKE) and provision their requested services (e.g., Cloud + SQL, Memorystore). + * **10. Testing Strategy**: Explicitly explain how you will generatively + author net-new test suites (e.g., `@WebMvcTest` for Spring Boot vs + `@QuarkusTest` for Quarkus) to cover their new endpoints, and note that + un-portable legacy tests (like Cactus) will be strategically disabled + and documented. + +8. **Tool Execution Fallback**: + + * If running in antigravity, use the `write_to_file` tool to save the + customized content as a UI Artifact named `wls-migration-plan.md` (MUST + include `ArtifactMetadata` with `RequestFeedback: true` and `UserFacing: + true` to trigger the UI approval flow). + * If running in a generic orchestrator, simply save the file as + `wls-migration-plan.md` using standard file writes, print a summary in + your response, and explicitly ask the user to type "Approved" to + proceed. + +> [!IMPORTANT] **Wait for Approval**: Once you have saved the +> `wls-migration-plan.md` artifact, you **MUST STOP calling tools immediately to +> end your turn**. Do NOT run any further commands, do not ask further +> questions, and do not perform any other steps in this turn. Wait for the user +> to review the plan and approve it or provide feedback. + +-------------------------------------------------------------------------------- + +## 3. Interactive Human Reviewer Feedback & Refinement Loop + +`wls-migration-plan.md` is an **iterative, living review artifact**. When the +human reviewer inspects the plan and leaves review comments or feedback (e.g., +requesting boundary adjustments, service renaming, package moves, custom +exclusions, or alternative cloud data mappings), you must enter an interactive +refinement loop: + +1. **Read & Analyze Comments**: Read all human reviewer comments and feedback + attached to the plan. +2. **Execute Adjustments**: + * If the reviewer requests changes to microservice boundaries or + exclusions, re-run `cli.py` with adjusted weights, `--exclude-patterns`, + or `--confirmed-utilities`, and re-integrate stack selections. + * If the reviewer requests naming changes, package relocations, or + architectural tweaks, edit the markdown structure directly. +3. **Overwrite Plan Artifact**: Overwrite the `wls-migration-plan.md` artifact + with the refined architecture (ensuring `RequestFeedback: true` and + `UserFacing: true` remain set). +4. **STOP Calling Tools Immediately**: End your turn immediately upon + overwriting the artifact to let the human reviewer inspect the updated plan. + +> [!IMPORTANT] **Repeat Until Approved**: Repeat this interactive feedback loop +> as many times as necessary until all reviewer comments are satisfactorily +> addressed and the human reviewer explicitly approves the plan (clicking +> "Proceed" or typing "Approved"). Only then may you advance to Phase 3 +> (Incremental Refactoring). + +-------------------------------------------------------------------------------- + +## 4. Review Findings Checklist + +When reviewing or refining `wls-migration-plan.md`, ensure the following aspects +are verified: + +* **Executive Summary**: Assess the general size of the monolith and total + WebLogic API dependencies. +* **Build & Environment**: Verify the target Java version, identify Maven or + Ant configurations, and check for any local legacy libraries (`lib/*.jar`). +* **Technical Inventory**: Check EJB volumes, JMS usages, and JNDI lookup + occurrences. Look for advanced features like Work Managers and SOAP Web + Services. +* **Data Access & Security**: Identify JDBC/JPA configurations and + declarative/programmatic security roles. +* **Cloud-Unfriendly Patterns**: Note occurrences of local File I/O, HTTP + Sessions, Batch processing, RMI/CORBA, JMX, and JavaMail. +* **Proposed Decomposition (Topological Analysis)**: Review the mathematically + generated microservice boundaries (including the Mermaid diagram) that + partition packages and database tables to minimize coupling. Ensure + `wls-migration-plan.md` accurately reflects the final high-quality + microservices. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/refactoring_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_guide.md new file mode 100644 index 0000000000..8a74720bb7 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_guide.md @@ -0,0 +1,206 @@ +# WebLogic to Cloud-Native Incremental Refactoring Guide + +This guide provides detailed procedural instructions for executing Phase 3 +(Incremental Refactoring) of the WebLogic migration workflow. + +## Table of Contents + +* [1. Initialize Target Workspace & Copy Files](#1-initialize-target-workspace--copy-files) (Line 23) +* [2. Dependency Validation (CRITICAL)](#2-dependency-validation-critical) (Line 42) +* [3. Automated Bulk Refactoring (Scope & Limitations)](#3-automated-bulk-refactoring-scope--limitations) (Line 52) +* [4. Replace JNDI Lookups](#4-replace-jndi-lookups) (Line 88) +* [5. Refactor EJBs](#5-refactor-ejbs) (Line 93) +* [6. Refactor Security (Align with Legacy Constraints)](#6-refactor-security-align-with-legacy-constraints) (Line 99) +* [7. Update APIs & Weblogic-specific Helpers](#7-update-apis--weblogic-specific-helpers) (Line 125) +* [8. Modernize Web Tier (Presentation Layer)](#8-modernize-web-tier-presentation-layer) (Line 130) +* [9. Refactor Cloud-Unfriendly Patterns (Deterministic Handling)](#9-refactor-cloud-unfriendly-patterns-deterministic-handling) (Line 144) +* [10. Port and Refactor Legacy Tests](#10-port-and-refactor-legacy-tests) (Line 160) +* [11. Track Refactoring Decisions](#11-track-refactoring-decisions) (Line 187) +* [12. Verify Refactored Code (Compilation & Test Suite Execution)](#12-verify-refactored-code-compilation--test-suite-execution) (Line 195) + +-------------------------------------------------------------------------------- + +## 1. Initialize Target Workspace & Copy Files + +* Create a target folder named `wls_migration` in the root of the workspace + directory to keep migrated modules isolated: + + ```bash + mkdir -p wls_migration + ``` + +* Initialize the target microservices structure under `wls_migration/` (e.g., + creating subdirectories for each microservice candidate identified in + `wls-migration-plan.md` Section 7). + +* **Isolate and Copy Source Files**: Copy the Java classes, resources, XML + configuration files, and web assets mapped to each microservice from the + original monolith directories into the target source directories inside + `wls_migration/` (e.g., copy EJB classes mapped to `patient-service` to + `wls_migration/patient-service/src/main/java/...`). + +## 2. Dependency Validation (CRITICAL) + +> [!IMPORTANT] **Mandatory Dependency Scanning**: Before any new external +> package, library, or framework (e.g., Spring Boot, Quarkus, JWT libraries, DB +> drivers) is imported or added to the microservices' `pom.xml` or +> `build.gradle`, you MUST invoke the `scan_dependencies` skill to validate its +> safety. The `scan_dependencies` skill is the exclusive authority for package +> validation. Do not generate code with new imports until the tool confirms +> safety and provides the approved versioning. + +## 3. Automated Bulk Refactoring (Scope & Limitations) + +Run OpenRewrite *inside the target sub-project folders in `wls_migration/`* to +automate boilerplate migrations (like `javax.*` to `jakarta.*` packages, or +upgrading Java version): + +```bash +/scripts/run_openrewrite.sh wls_migration/[service_name] [recipe_type] +``` + +* Use recipe `jakarta` for Java EE to Jakarta EE migration. +* Use recipe `java17` to upgrade Java syntax to Java 17. +* Use recipe `spring3` if migrating to Spring Boot 3.x. +* *Note: If no pom.xml is present (e.g., in Ant projects), the script will + automatically detect the Java source root, generate a temporary pom.xml for + OpenRewrite execution, and clean it up afterwards.* + +> [!IMPORTANT] **OpenRewrite Core Limitations**: Do NOT rely on OpenRewrite to +> handle weblogic configurations or structural rewrites. The following must be +> performed manually: +> +> 1. **EJBGen (`.ejb` files)**: WebLogic's javadoc-style EJBGen annotations +> (like `@ejbgen:entity` or `@ejbgen:relation`) are not supported by +> standard recipes. Translate these annotations to JPA (`@Entity`, `@Table`) +> and Spring annotations manually. +> 2. **EJB 2.x CMP Entity Beans to JPA**: Converting legacy CMP classes +> extending `GenericEntityBean` into modern POJOs with `@Entity` annotations +> and Spring Data JPA Repositories is a manual architectural rewrite. +> 3. **Weblogic Specific APIs**: Usages of weblogic libraries like +> `weblogic.xml.stream.*` must be manually rewritten to standard Java StAX +> parsing (`javax.xml.stream.*`). 4. **Microservice Decomposition**: +> OpenRewrite cannot design microservice boundaries or setup REST/PubSub +> inter-service communications. Run OpenRewrite only to clean syntax and +> packages within the already isolated target modules under +> `wls_migration/`. + +## 4. Replace JNDI Lookups + +Replace remaining dynamic JNDI lookups with Dependency Injection (DI) using +`@Autowired` (Spring) or `@Inject` (Quarkus/CDI). + +## 5. Refactor EJBs + +* Convert Stateless Session Beans to standard Spring Services (`@Service`) or + Quarkus Beans. See [example_ejb_migration.md](../assets/example_ejb_migration.md) for a concrete before-and-after mapping. +* Convert Message-Driven Beans (MDBs) to GCP Pub/Sub listeners or Spring JMS listeners. See [example_jms_migration.md](../assets/example_jms_migration.md) for a concrete before-and-after mapping. + +## 6. Refactor Security (Align with Legacy Constraints) + +Replace WebLogic-specific security (JAAS, WebLogic helper classes, web.xml +constraints) with Spring Security or Quarkus Security. + +* **Align with Monolith Security Maps**: Security constraints (requiring + authentication/authorization) must only be applied to endpoints and services + that were secured in the legacy monolith (e.g., routes protected by + `web.xml` security constraints, EJB roles, or JAAS intercepts). Endpoints + that were public/open in the legacy monolith **must remain public** in the + new microservices to avoid breaking client compatibility. +* **Flag Mock Security**: Mock tokens or plaintext security configurations + (such as temporary mock token generation endpoints or `NoOpPasswordEncoder` + for database passwords) are acceptable *for baseline compatibility*, but + they **must be flagged** as warning items in the final `walkthrough.md` + report. +* **Token Sign & Validate**: For secured endpoints, use token-based + authentication (JWT or OIDC integration). If a custom token service (e.g., + custom auth-service) is implemented, it must issue cryptographically signed + JWTs (e.g., HS256/RS256). All secured downstream services must validate this + token signature and verify credentials (such as expiration, scopes, and + signature keys). +* Refer to + [example_security_migration.md](../assets/example_security_migration.md) for + Spring Security configuration steps. + +## 7. Update APIs & Weblogic-specific Helpers + +Replace WebLogic-specific helper APIs with standard Java or target framework +APIs. + +## 8. Modernize Web Tier (Presentation Layer) + +* **Decouple JSPs/JSF**: For server-rendered user interfaces (JSP/JSF), + isolate the presentation logic and migrate pages to a standalone SPA + framework (e.g., **React** or **Angular**). +* **Convert MVC to REST**: Rewrite legacy controller classes (Struts Actions, + JSF Backing Beans, or HttpServlet subclasses) into stateless REST + controllers: + * Spring: `@RestController` with `@RequestMapping` + * Quarkus/CDI: JAX-RS resource classes with `@Path`, `@GET`, `@POST` +* **Session Management**: Remove dependency on stateful server-side sessions + (`HttpSession`). Transition to token-based security (JWT) or store session + state in an external cache (e.g., Redis) based on user decision. + +## 9. Refactor Cloud-Unfriendly Patterns (Deterministic Handling) + +* **File I/O**: Replace `java.io` and `java.nio` local file operations with + the GCP Storage Client Library to read/write from Cloud Storage buckets (or + map to Filestore/Parallelstore as aligned). +* **Batch Processing**: Extract `javax.batch` or Spring Batch jobs into + standalone entry points designed to run as GCP Cloud Run Jobs. +* **JavaMail**: Replace direct SMTP implementations (`javax.mail`) with REST + API calls to modern email providers (e.g., SendGrid API) based on user + preference. +* **RMI/CORBA**: Replace binary remote communication with standard RESTful + endpoints (`@RestController` or JAX-RS) and JSON payloads. +* **JMX/MBeans**: Remove custom MBeans and replace with Micrometer (Spring + Boot) or SmallRye Metrics (Quarkus) for standard Prometheus/Cloud Monitoring + exposition. + +## 10. Port and Refactor Legacy Tests + +Locate all legacy unit, integration, and E2E tests related to the migrated +classes in the monolith. + +* **Copy to Target**: Copy these test source files into the corresponding test + folders in the target sub-project (e.g., + `wls_migration/[service_name]/src/test/java/...`). +* **Modernize Frameworks**: Refactor legacy JUnit 3/4 syntax to JUnit 5 + (`org.junit.jupiter.api.*`). +* **Mock Dependencies**: Replace legacy in-memory database setups or local + mock connectors with Spring Boot test mocks (`@SpringBootTest`, `@MockBean`) + or Quarkus test mocks (`@QuarkusTest`, `@InjectMock`). +* **Handle Network Calls**: If legacy integration tests made local invocations + to classes that are now separated into other microservices, refactor them to + use HTTP mock servers (e.g., `WireMock` or Spring `MockRestServiceServer`) + to stub the HTTP communication. +* **Generative Testing for Net-New Endpoints**: For newly created REST + controllers or Pub/Sub listeners that replace legacy EJB/Servlet entry + points, you MUST generatively author new Spring Boot (`@WebMvcTest`) or + Quarkus tests to validate their behavior. +* **Disabled Tests Documentation**: If legacy tests cannot be ported or + replaced (e.g., container-bound Cactus tests that are fundamentally + incompatible with modern serverless targets), you must disable them (e.g., + using `@Disabled` or commenting them out) and carefully document exactly + which tests were disabled and why in the final `walkthrough.md` report. + +## 11. Track Refactoring Decisions + +Keep track of any deviations, architectural trade-offs, or custom modifications +made during refactoring (e.g., custom bean registrations or specific package +shifts). Do NOT modify the approved `wls-migration-plan.md` plan. Instead, +document these details for inclusion in the final validation walkthrough report +(`walkthrough.md`) in Phase 6. + +## 12. Verify Refactored Code (Compilation & Test Suite Execution) + +After migrating each class or module, immediately verify its correctness: + +* **Compile Code**: Run compilation (e.g., `./mvnw compile` or `./gradlew + compileJava` inside the microservice subdirectory under `wls_migration/`) to + ensure no compilation issues or syntax regressions. +* **Run Test Suite**: Run the ported unit and integration tests (e.g., `./mvnw + test`). Ensure they all pass. If tests are completely missing for a migrated + service, author a lightweight JUnit test using Mockito to mock + database/network resources and assert that migrated business functions + return identical logical results. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/refactoring_quarkus.md b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_quarkus.md new file mode 100644 index 0000000000..9440bed1a5 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_quarkus.md @@ -0,0 +1,161 @@ +# WebLogic to Quarkus Refactoring Guide + +This guide provides patterns for refactoring WebLogic and Java EE (Jakarta EE) +components to Quarkus. Quarkus uses Jakarta EE standards (like CDI, JTA, JAX-RS) +which makes the transition from WebLogic relatively straightforward, but +requires replacing specific WebLogic features and EJB specific details. + +## Table of Contents + +* [1. EJB to Quarkus Beans (CDI)](#1-ejb-to-quarkus-beans-cdi) (Line 17) +* [2. Dependency Injection (JNDI to CDI Inject)](#2-dependency-injection-jndi-to-cdi-inject) (Line 60) +* [3. Transaction Management](#3-transaction-management) (Line 112) +* [4. Logging](#4-logging) (Line 143) + +-------------------------------------------------------------------------------- + +## 1. EJB to Quarkus Beans (CDI) + +Quarkus does not support full EJB container features (like remote EJBs, EJB +timers). EJB Session Beans should be converted to standard CDI beans. + +### Stateless Session Beans (SLSB) + +Convert to `@ApplicationScoped` (singleton-like) or `@RequestScoped` beans. + +**Legacy EJB:** + +```java +import javax.ejb.Stateless; + +@Stateless +public class OrderServiceBean implements OrderService { + public void placeOrder(Order order) { ... } +} +``` + +**Quarkus (CDI):** + +```java +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class OrderServiceBean implements OrderService { + @Override + public void placeOrder(Order order) { ... } +} +``` + +### Stateful Session Beans (SFSB) + +> [!WARNING] Serverless environments (like Cloud Run) are designed to be +> stateless. Stateful Session Beans should be refactored to be stateless, +> storing session state in an external cache (e.g., Memorystore/Redis) or +> database. + +If you must keep state, you can use `@SessionScoped` (if using Quarkus RESTEasy +with session support), but this is discouraged for cloud-native serverless +deployments. + +## 2. Dependency Injection (JNDI to CDI Inject) + +Replace `@EJB` and JNDI lookups with `@Inject`. + +**Legacy EJB Injection:** + +```java +@EJB +private OrderService orderService; +``` + +**Quarkus (CDI):** + +```java +import jakarta.inject.Inject; + +public class OrderResource { + @Inject + OrderService orderService; +} +``` + +For configuration properties (MicroProfile Config): Replace JNDI env-entry with +`@ConfigProperty`. + +**Legacy JNDI Env Entry:** + +```java +InitialContext ctx = new InitialContext(); +String myConfig = (String) ctx.lookup("java:comp/env/myConfigParam"); +``` + +**Quarkus:** + +```java +import org.eclipse.microprofile.config.inject.ConfigProperty; +import jakarta.inject.Inject; + +@ApplicationScoped +public class MyService { + @Inject + @ConfigProperty(name = "my.config.param") + String myConfig; +} +``` + +Define in `src/main/resources/application.properties`: + +```properties +my.config.param=some-value +``` + +## 3. Transaction Management + +Quarkus supports JTA `@Transactional` (from `jakarta.transaction`), which is +very similar to EJB CMT. + +**Legacy EJB CMT:** + +```java +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +@Stateless +public class OrderServiceBean { + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void placeOrder(Order order) { ... } +} +``` + +**Quarkus:** + +```java +import jakarta.transaction.Transactional; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class OrderServiceBean { + @Transactional(Transactional.TxType.REQUIRED) + public void placeOrder(Order order) { ... } +} +``` + +## 4. Logging + +Quarkus uses JBoss Logging by default, but also supports SLF4J. + +**Quarkus (using SLF4J):** + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class MyService { + private static final Logger log = LoggerFactory.getLogger(MyService.class); + + public void doWork() { + log.info("Doing work in Quarkus"); + } +} +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/references/refactoring_spring.md b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_spring.md new file mode 100644 index 0000000000..50d08743fb --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/refactoring_spring.md @@ -0,0 +1,192 @@ +# WebLogic to Spring Boot Refactoring Guide + +This guide provides patterns for refactoring WebLogic and Java EE (Jakarta EE) +components to Spring Boot. + +## Table of Contents + +* [1. EJB to Spring Beans](#1-ejb-to-spring-beans) (Line 15) +* [2. Dependency Injection (JNDI to Spring DI)](#2-dependency-injection-jndi-to-spring-di) (Line 72) +* [3. Transaction Management](#3-transaction-management) (Line 126) +* [4. Logging](#4-logging) (Line 157) + +-------------------------------------------------------------------------------- + +## 1. EJB to Spring Beans + +### Stateless Session Beans (SLSB) + +Stateless EJBs are easily mapped to Spring `@Service` or `@Component` beans. +Spring beans are singletons by default, which is generally appropriate if they +are stateless. + +**Legacy EJB:** + +```java +import javax.ejb.Stateless; + +@Stateless(name = "OrderService") +public class OrderServiceBean implements OrderService { + public void placeOrder(Order order) { + // Business logic + } +} +``` + +**Spring Boot:** + +```java +import org.springframework.stereotype.Service; + +@Service +public class OrderServiceImpl implements OrderService { + @Override + public void placeOrder(Order order) { + // Business logic + } +} +``` + +### Stateful Session Beans (SFSB) + +> [!WARNING] Serverless environments (like Cloud Run) are designed to be +> stateless. Stateful Session Beans should be refactored to be stateless, +> storing session state in an external cache (e.g., Memorystore/Redis) or +> database. + +If you must keep session state in the application tier (not recommended for +serverless scaled-out instances): Use Spring `@SessionScope`. + +```java +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.SessionScope; + +@Component +@SessionScope +public class UserSession { + private User user; + // getter/setter +} +``` + +## 2. Dependency Injection (JNDI to Spring DI) + +Replace manual JNDI lookups with Spring's `@Autowired` or `@Resource`. + +**Legacy JNDI Lookup:** + +```java +InitialContext ctx = new InitialContext(); +OrderService orderService = (OrderService) ctx.lookup("java:comp/env/ejb/OrderService"); +``` + +**Spring Boot:** + +```java +import org.springframework.beans.factory.annotation.Autowired; + +@RestController +public class OrderController { + + @Autowired + private OrderService orderService; + + // ... +} +``` + +For configuration properties (previously looked up via JNDI environment +entries): Use `@Value` or `@ConfigurationProperties`. + +**Legacy JNDI Env Entry:** + +```java +InitialContext ctx = new InitialContext(); +String myConfig = (String) ctx.lookup("java:comp/env/myConfigParam"); +``` + +**Spring Boot:** + +```java +import org.springframework.beans.factory.annotation.Value; + +@Component +public class MyComponent { + @Value("${my.config.param}") + private String myConfig; +} +``` + +Define the value in `application.properties`: + +```properties +my.config.param=some-value +``` + +## 3. Transaction Management + +Replace EJB Container-Managed Transactions (CMT) with Spring's `@Transactional`. + +**Legacy EJB CMT:** + +```java +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +@Stateless +public class OrderServiceBean { + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void placeOrder(Order order) { ... } +} +``` + +**Spring Boot:** + +```java +import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Service; + +@Service +public class OrderServiceImpl implements OrderService { + @Override + @Transactional + public void placeOrder(Order order) { ... } +} +``` + +## 4. Logging + +Replace `weblogic.logging` (if used) or standard `java.util.logging` with SLF4J +(backed by Logback in Spring Boot). + +**Legacy:** + +```java +import java.util.logging.Logger; +// or import weblogic.logging.NonCatalogLogger; + +public class MyClass { + private static final Logger log = Logger.getLogger(MyClass.class.getName()); + + public void doWork() { + log.info("Doing work"); + } +} +``` + +**Spring Boot:** + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MyClass { + private static final Logger log = LoggerFactory.getLogger(MyClass.class); + + public void doWork() { + log.info("Doing work"); + } +} +``` + +(Alternatively, use Lombok's `@Slf4j` annotation). diff --git a/skills/cloud/google-cloud-weblogic-migration/references/sql_dialect_migration.md b/skills/cloud/google-cloud-weblogic-migration/references/sql_dialect_migration.md new file mode 100644 index 0000000000..38e7c6cd61 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/sql_dialect_migration.md @@ -0,0 +1,178 @@ +# Oracle & PointBase SQL Dialect to Standard ANSI SQL Translation Guide + +Legacy WebLogic monoliths almost universally ran on Oracle Database in +production (or PointBase for local development and testing). When migrating to +cloud-native managed databases on GCP (Google Cloud SQL for PostgreSQL/MySQL or +Google Cloud AlloyDB), vendor-specific SQL syntax embedded in JPA native +queries, JDBC `PreparedStatement` strings, or CMP finder expressions must be +translated to standard ANSI SQL. + +## Table of Contents + +* [1. Oracle Specific Outer Joins ((+))](#1-oracle-specific-outer-joins) (Line 20) +* [2. Specific SQL Functions & Null Handling](#2-specific-sql-functions--null-handling) (Line 57) +* [3. Sequences and SELECT ... FROM DUAL](#3-sequences-and-select--from-dual) (Line 87) +* [4. Oracle Hierarchical Queries (CONNECT BY PRIOR)](#4-oracle-hierarchical-queries-connect-by-prior) (Line 120) +* [5. Dialect Configuration in Framework Persistence Layers](#5-dialect-configuration-in-framework-persistence-layers) (Line 155) + +-------------------------------------------------------------------------------- + +## 1. Oracle Specific Outer Joins (`(+)`) + +In legacy Oracle SQL, outer joins were written using the oracle `(+)` operator +instead of ANSI `LEFT OUTER JOIN` or `RIGHT OUTER JOIN`. + +### Before: Legacy Oracle Outer Join Syntax + +```sql +-- Oracle Specific Left Outer Join +SELECT p.patient_id, p.name, a.city, a.state +FROM patient_table p, address_table a +WHERE p.address_id = a.address_id(+) + AND p.status = 'ACTIVE'; + +-- Oracle Specific Right Outer Join +SELECT d.dept_name, e.emp_id, e.emp_name +FROM department_table d, employee_table e +WHERE d.dept_id(+) = e.dept_id; +``` + +### After: Standard ANSI SQL (Cloud SQL / AlloyDB Compatible) + +```sql +-- Standard ANSI Left Outer Join +SELECT p.patient_id, p.name, a.city, a.state +FROM patient_table p +LEFT OUTER JOIN address_table a ON p.address_id = a.address_id +WHERE p.status = 'ACTIVE'; + +-- Standard ANSI Right Outer Join +SELECT d.dept_name, e.emp_id, e.emp_name +FROM employee_table e +LEFT OUTER JOIN department_table d ON e.dept_id = d.dept_id; +``` + +-------------------------------------------------------------------------------- + +## 2. Specific SQL Functions & Null Handling + +Oracle and PointBase provide oracle specific scalar functions that fail when +executed on PostgreSQL or MySQL. + +| Legacy Oracle / | Standard ANSI SQL / | Description | +: PointBase Function : PostgreSQL Equivalent : : +| :---------------------- | :---------------------- | :----------------------- | +| `NVL(col, default_val)` | `COALESCE(col, | Returns `default_val` if | +: : default_val)` : `col` is NULL. : +| `DECODE(col, v1, r1, | `CASE WHEN col = v1 | Inline conditional | +: v2, r2, def)` : THEN r1 WHEN col = v2 : branch / switch : +: : THEN r2 ELSE def END` : statement. : +| `SYSDATE` | `CURRENT_TIMESTAMP` (or | Returns current system | +: : `NOW()`) : date and time. : +| `TO_DATE('2026-07-06', | `TO_DATE('2026-07-06', | Date string parsing | +: 'YYYY-MM-DD')` : 'YYYY-MM-DD')` or : (PostgreSQL supports : +: : `'2026-07-06'\:\:date` : standard casting). : +| `TRUNC(SYSDATE)` | `CURRENT_DATE` (or | Strips time component | +: : `DATE_TRUNC('day', : from timestamp. : +: : CURRENT_TIMESTAMP)`) : : +| `INSTR(str, substr)` | `POSITION(substr IN | Returns 1-based index of | +: : str)` : substring in string. : +| `SUBSTR(str, start, | `SUBSTRING(str FROM | Substring extraction. | +: len)` : start FOR len)` : : +| `ROWNUM <= 10` | `LIMIT 10` (at end of | Limits row count | +: : query) : returned by query. : + +-------------------------------------------------------------------------------- + +## 3. Sequences and `SELECT ... FROM DUAL` + +Oracle requires selecting from a dummy table named `DUAL` to evaluate +expressions or fetch sequence generators. + +### Before: Legacy Oracle Sequence & DUAL + +```sql +-- Fetching sequence in Oracle +SELECT PATIENT_SEQ.NEXTVAL FROM DUAL; + +-- Evaluating constant expression in Oracle +SELECT 'PING' AS status FROM DUAL; +``` + +### After: Cloud SQL / AlloyDB (PostgreSQL / MySQL) + +```sql +-- PostgreSQL: Fetching sequence (DUAL is unnecessary) +SELECT nextval('patient_seq'); + +-- PostgreSQL / MySQL: Evaluating constant expression without FROM clause +SELECT 'PING' AS status; + +-- Modern Schema Best Practice: Replace sequence lookups with IDENTITY / SERIAL columns in DDL +CREATE TABLE patient_table ( + patient_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL +); +``` + +-------------------------------------------------------------------------------- + +## 4. Oracle Hierarchical Queries (`CONNECT BY PRIOR`) + +Legacy Oracle SQL supports tree traversal using `CONNECT BY PRIOR` and `START +WITH`. + +### Before: Legacy Oracle Hierarchical Query + +```sql +SELECT emp_id, emp_name, manager_id +FROM employee_table +START WITH manager_id IS NULL +CONNECT BY PRIOR emp_id = manager_id; +``` + +### After: Standard ANSI Recursive Common Table Expressions (CTE) + +```sql +WITH RECURSIVE org_chart AS ( + -- Base case: Top-level managers + SELECT emp_id, emp_name, manager_id, 1 AS level + FROM employee_table + WHERE manager_id IS NULL + + UNION ALL + + -- Recursive step: Employees reporting to managers in org_chart + SELECT e.emp_id, e.emp_name, e.manager_id, o.level + 1 + FROM employee_table e + INNER JOIN org_chart o ON e.manager_id = o.emp_id +) +SELECT emp_id, emp_name, manager_id FROM org_chart; +``` + +-------------------------------------------------------------------------------- + +## 5. Dialect Configuration in Framework Persistence Layers + +When refactoring Spring Boot or Quarkus microservices, ensure the JPA / +Hibernate database dialect property is explicitly updated to match the target +cloud managed database: + +### Spring Boot (`application.properties`) + +```properties +# Cloud SQL / AlloyDB PostgreSQL Dialect +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate + +# Or for Cloud SQL MySQL +# spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect +``` + +### Quarkus (`application.properties`) + +```properties +# Quarkus automatically infers dialect from db-kind, but can be explicitly set +quarkus.datasource.db-kind=postgresql +quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/references/verification_guide.md b/skills/cloud/google-cloud-weblogic-migration/references/verification_guide.md new file mode 100644 index 0000000000..86b8b26f15 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/verification_guide.md @@ -0,0 +1,59 @@ +# WebLogic Migration Verification, Audit, & Traceability Guide + +This guide provides detailed procedural instructions for executing Phase 6 +(Verification, Audit, & Traceability) of the WebLogic migration workflow. + +## 1. Map Endpoints (Traceability Matrix) + +Compile an API mapping table showing before/after routes: + +* Legacy URLs (e.g., Struts `/editProfile.do` or EJB remote methods) vs. + Migrated REST URLs (e.g., `/api/profile/save`). +* Map incoming parameter formats (Form payload vs. JSON payload). + +## 2. Validate Integration, Database Schema & Security Gates + +Locally spin up the migrated services (running in Dev mode or against a +Dockerized test database) and execute verification assertions: + +* **Execute Integration and Security Assertions**: Run mock HTTP calls (e.g., + using `curl` or automated integration scripts) to verify: + * *Access Verification on Secured Routes*: Verify that endpoints mapped to + legacy security constraints (secured routes) return `401 Unauthorized` + (or `403 Forbidden`) when queried without credentials or with an invalid + token. + * *Access Verification on Public Routes*: Verify that routes that were + public in the legacy monolith are successfully accessible without any + credentials (returning `200 OK` and expected schema payload). + * *Authorized Access*: Ensure secured routes process requests successfully + (returning `200 OK` and correct schema) only when a valid + cryptographically signed token (or mock token if mock mode is flagged) + is supplied. + * *Schema Alignment*: Response JSON structures match original payloads and + database mappings are correctly persisted. + +## 3. Generate Migration Audit Report (`walkthrough.md`) + +Create a UI Artifact named `walkthrough.md` using the `write_to_file` tool. You +MUST provide `ArtifactMetadata` with `RequestFeedback: true` and `UserFacing: +true`. Include the following mandatory sections: + +1. **Structural Comparison**: Total classes migrated, excluded utility + libraries, and service boundary summaries. +2. **Refactoring Decisions & Deviations**: List any architectural trade-offs, + deviations from the original approved `wls-migration-plan.md` plan, or + custom modifications (like custom bean configurations or package shifts) + made during refactoring, along with their justifications. +3. **Compilation & Test Suite Verification**: Document the build outcomes + (compilation logs summary) and test suite results (e.g., number of + unit/integration tests passed, failed, or skipped) to verify functional + equivalence and compilation success. +4. **Endpoint Matrix**: Before/After trace paths, specifying whether each + endpoint is secured or public. +5. **Security Warnings & Flags**: If any temporary mock security configurations + or dev secrets are active (e.g., mock tokens, `NoOpPasswordEncoder`, default + local passwords), list them here as critical security findings to be + addressed prior to production release. +6. **Validation Log**: Output snippets of test executions (or local compile + success reports) demonstrating that all migrated services are functioning + correctly. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/web_modernization.md b/skills/cloud/google-cloud-weblogic-migration/references/web_modernization.md new file mode 100644 index 0000000000..1a7445103d --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/web_modernization.md @@ -0,0 +1,193 @@ +# Web Tier Modernization Reference Guide + +This guide explains how to migrate legacy server-rendered presentation +components (JSPs, servlets, and web MVC actions) to modern decoupled Single Page +Applications (SPAs) backed by RESTful APIs. + +## Table of Contents + +* [1. JSP to React/Angular Migration Strategy](#1-jsp-to-react-angular-migration-strategy) (Line 15) +* [2. Migrating Servlets to REST Endpoints](#2-migrating-servlets-to-rest-endpoints) (Line 115) +* [3. Session State Decoupling](#3-session-state-decoupling) (Line 165) + +-------------------------------------------------------------------------------- + +## 1. JSP to React/Angular Migration Strategy + +Legacy Java web applications compile JSP pages dynamically into servlets. The +modernization flow requires separating the HTML structure from the Java-based +data flow. + +### Before: Legacy Struts/JSP Page (`editProfile.jsp`) + +```html +<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> +<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> + + +
+ + +
+
+ + +
+ +
+``` + +### After: Decoupled REST + React SPA + +#### A. Spring Boot REST Controller (`ProfileController.java`) + +```java +package com.acme.medimed.web; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/profile") +@CrossOrigin(origins = "*") // Configure CORS for SPA access +public class ProfileController { + + private final PatientService patientService; + + public ProfileController(PatientService patientService) { + this.patientService = patientService; + } + + @PostMapping("/save") + public ResponseEntity saveProfile(@RequestBody ProfileRequest request) { + patientService.updatePatientProfile(request.getEmail(), request.getPhone()); + return ResponseEntity.ok().build(); + } +} +``` + +#### B. React Frontend Component (`EditProfile.jsx`) + +```jsx +import React, { useState } from 'react'; + +export default function EditProfile() { + const [profile, setProfile] = useState({ email: '', phone: '' }); + + const handleSubmit = async (e) => { + e.preventDefault(); + const response = await fetch('/api/profile/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(profile) + }); + if (response.ok) { + alert('Profile updated successfully!'); + } + }; + + return ( +
+
+ + setProfile({...profile, email: e.target.value})} + /> +
+
+ + setProfile({...profile, phone: e.target.value})} + /> +
+ +
+ ); +} +``` + +-------------------------------------------------------------------------------- + +## 2. Migrating Servlets to REST Endpoints + +Legacy servlets directly write HTML output streams or handle request +dispatching. + +### Before: Custom HttpServlet (`AdminReportServlet.java`) + +```java +public class AdminReportServlet extends HttpServlet { + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + out.print("{\"status\":\"active\", \"total_users\": 104}"); + out.flush(); + } +} +``` + +### After: Quarkus JAX-RS Endpoint (`AdminReportResource.java`) + +```java +package com.acme.medimed.web; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +@Path("/api/admin/report") +@Produces(MediaType.APPLICATION_JSON) +public class AdminReportResource { + + @GET + public AdminReport getReport() { + return new AdminReport("active", 104); + } + + public static class AdminReport { + public String status; + public int totalUsers; + + public AdminReport(String status, int totalUsers) { + this.status = status; + this.totalUsers = totalUsers; + } + } +} +``` + +-------------------------------------------------------------------------------- + +## 3. Session State Decoupling + +Legacy Java web applications store state in the server-side `HttpSession`. In +Cloud Run or serverless containers, instances scale to zero and shift +dynamically, which breaks session persistence. + +### Before: Server-bound Session Writes + +```java +HttpSession session = request.getSession(); +session.setAttribute("user_key", userObject); +``` + +### After: Token-based Stateless JWT + +Expose auth headers in responses and verify claims at the API Gateway or +framework level: + +```java +// Spring Security Config utilizing JWT verification filters: +http.sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); +``` + +If state must persist, use a **Redis Cache** shared across your Cloud Run +instances: + +* Add the `spring-session-data-redis` library. +* Annotate configuration with `@EnableRedisHttpSession`. diff --git a/skills/cloud/google-cloud-weblogic-migration/references/weblogic_specific_apis.md b/skills/cloud/google-cloud-weblogic-migration/references/weblogic_specific_apis.md new file mode 100644 index 0000000000..43706ccfbf --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/references/weblogic_specific_apis.md @@ -0,0 +1,264 @@ +# WebLogic Specific APIs & Container Services Modernization Guide + +This guide provides concrete refactoring recipes for modernizing +WebLogic-specific specific container APIs (Custom Security Providers, Work +Managers, Custom JMX MBeans, and XML Streaming APIs) into standard cloud-native +frameworks on GCP. + +-------------------------------------------------------------------------------- + +## Table of Contents + +* [1. Weblogic Specific Security Providers & JAAS Interceptors](#1-weblogic-specific-security-providers--jaas-interceptors-weblogicsecurity) + (Line 21) +* [2. WebLogic Work Managers & Raw Threading](#2-weblogic-work-managers--raw-threading-commonjworkworkmanager) + (Line 88) +* [3. Custom JMX MBeans](#3-custom-jmx-mbeans-weblogicmanagement-standardmbean) + (Line 166) +* [4. WebLogic XML Streaming APIs](#4-weblogic-xml-streaming-apis-weblogicxmlstream) + (Line 227) + +-------------------------------------------------------------------------------- + +## 1. Weblogic Specific Security Providers & JAAS Interceptors (`weblogic.security.*`) + +WebLogic applications frequently implement custom Authentication Providers +(extending `weblogic.security.spi.AuthenticationProvider`), custom Role Mappers +(`weblogic.security.spi.RoleMapper`), or use `weblogic.security.SubjectUtils` +and programmatic `@SecurityRoleRef` annotations. + +### Before: Legacy WebLogic Programmatic Subject Check + +```java +// Legacy WebLogic SubjectUtils Role Assertion +import weblogic.security.SubjectUtils; +import weblogic.security.service.PrivilegedActions; +import javax.security.auth.Subject; + +public class AdminReportService { + public void generateReport() { + Subject currentSubject = SubjectUtils.getUserSubject(); + if (!SubjectUtils.isUserInRole(currentSubject, "MedicalAdminRole")) { + throw new SecurityException("User lacks MedicalAdminRole in WebLogic Security Realm"); + } + // Proceed with report generation... + } +} +``` + +### After: Cloud-Native Declarative Security + +In a cloud-native microservice architecture, specific realm checks are replaced +by declarative role-based access control (RBAC) inspecting claims within +cryptographically signed JSON Web Tokens (JWTs) or OAuth2 / OIDC tokens. + +#### Spring Boot Security (`@PreAuthorize`) + +```java +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +@Service +public class AdminReportService { + + // Declarative Spring Security check evaluating JWT role claims + @PreAuthorize("hasRole('MedicalAdminRole')") + public void generateReport() { + // Proceed with report generation... + } +} +``` + +#### Quarkus Security (`@RolesAllowed`) + +```java +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AdminReportService { + + @RolesAllowed("MedicalAdminRole") + public void generateReport() { + // Proceed with report generation... + } +} +``` + +-------------------------------------------------------------------------------- + +## 2. WebLogic Work Managers & Raw Threading (`commonj.work.WorkManager`) + +Legacy Java EE 5/6 prohibited spawning raw threads (`new Thread()`). WebLogic +applications used `commonj.work.WorkManager` or configured WebLogic Work +Managers (looked up via JNDI `java:comp/env/wm/MyWorkManager`) to execute +asynchronous background tasks or parallel processing within container-managed +thread pools. + +### Before: Legacy WebLogic WorkManager Execution + +```java +import commonj.work.WorkManager; +import commonj.work.Work; +import javax.naming.InitialContext; + +public class AsyncBillingDispatcher { + public void dispatchInvoice(final Long invoiceId) throws Exception { + InitialContext ctx = new InitialContext(); + WorkManager wm = (WorkManager) ctx.lookup("java:comp/env/wm/BillingWorkManager"); + + wm.schedule(new Work() { + public void run() { + // Execute background billing calculation + } + public boolean isDaemon() { return false; } + public void release() {} + }); + } +} +``` + +### After: Cloud-Native Asynchronous Execution & Virtual Threads + +In modern Spring Boot and Quarkus microservices running on Java 21+, +container-managed Work Managers are modernized into declarative asynchronous +tasks backed by lightweight **Java 21 Virtual Threads** (`Project Loom`) or +event-driven Pub/Sub workers. + +#### Spring Boot (`@Async` with Virtual Threads) + +```java +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +public class AsyncBillingDispatcher { + + // Executes asynchronously on a Java 21 Virtual Thread + @Async + public void dispatchInvoice(Long invoiceId) { + // Execute background billing calculation + } +} + +// In Spring Boot 3.2+ Application Configuration: +// spring.threads.virtual.enabled=true +``` + +#### Quarkus (`@Asynchronous` / `@RunOnVirtualThread`) + +```java +import io.smallrye.common.annotation.RunOnVirtualThread; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.concurrent.CompletableFuture; + +@ApplicationScoped +public class AsyncBillingDispatcher { + + @RunOnVirtualThread + public CompletableFuture dispatchInvoice(Long invoiceId) { + // Execute background billing calculation on Java 21 Virtual Thread + return CompletableFuture.completedFuture(null); + } +} +``` + +-------------------------------------------------------------------------------- + +## 3. Custom JMX MBeans (`weblogic.management.*`, `StandardMBean`) + +WebLogic applications frequently register custom MBeans (extending +`StandardMBean` or registering via `MBeanServer` looked up from +`java:comp/env/jmx/runtime`) to expose application performance counters, cache +eviction triggers, and dynamic configuration knobs to the WebLogic Console or +WLST (WebLogic Scripting Tool). + +### After: Cloud-Native Metrics & Dynamic Operations + +#### Metrics Exposition: Micrometer / SmallRye Metrics + +Replace JMX metric counters with standard cloud-native metrics exposed at +`/actuator/prometheus` (Spring) or `/q/metrics` (Quarkus), scraped automatically +by **Google Cloud Managed Service for Prometheus**: + +```java +// Spring Boot Micrometer Counter +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Counter; +import org.springframework.stereotype.Component; + +@Component +public class PatientMetrics { + private final Counter registrationCounter; + + public PatientMetrics(MeterRegistry registry) { + this.registrationCounter = registry.counter("patients.registered.total"); + } + + public void incrementRegistration() { + registrationCounter.increment(); + } +} +``` + +#### Dynamic Configuration: Spring Cloud Config / GCP Secret Manager + +Replace JMX setter operations and WLST reload scripts with dynamic configuration +reloading via `@RefreshScope` (Spring Boot) or GCP Secret Manager environment +variable re-injection: + +```java +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RefreshScope +public class MedicalFeatureToggles { + @Value("${feature.telemedicine.enabled:false}") + private boolean telemedicineEnabled; + + public boolean isTelemedicineEnabled() { + return telemedicineEnabled; + } +} +``` + +-------------------------------------------------------------------------------- + +## 4. WebLogic XML Streaming APIs (`weblogic.xml.stream.*`) + +Legacy monoliths often imported WebLogic's bundled XML streaming parsers +directly (`weblogic.xml.stream.XMLInputStream`, `XMLStreamReader`, +`XMLInputFactory`). + +### After: Standard JDK StAX (`javax.xml.stream.*`) + +WebLogic's specific XML streaming APIs map 1-to-1 with standard JDK StAX +(Streaming API for XML) included in standard Java Runtime Environments: + +Legacy WebLogic XML API | Standard JDK StAX Equivalent +:--------------------------------------- | :------------------------------------ +`weblogic.xml.stream.XMLInputFactory` | `javax.xml.stream.XMLInputFactory` +`weblogic.xml.stream.XMLStreamReader` | `javax.xml.stream.XMLStreamReader` +`weblogic.xml.stream.XMLStreamException` | `javax.xml.stream.XMLStreamException` +`weblogic.xml.stream.events.XMLEvent` | `javax.xml.stream.events.XMLEvent` + +```java +// Standard JDK StAX Replacement +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import java.io.InputStream; + +public class XmlParserService { + public void parseMedicalRecord(InputStream in) throws Exception { + XMLInputFactory factory = XMLInputFactory.newInstance(); + XMLStreamReader reader = factory.createXMLStreamReader(in); + while (reader.hasNext()) { + int event = reader.next(); + // Process StAX events... + } + reader.close(); + } +} +``` diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/__init__.py b/skills/cloud/google-cloud-weblogic-migration/scripts/__init__.py new file mode 100644 index 0000000000..617e33d2af --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/__init__.py @@ -0,0 +1,2 @@ +# wls_migrate package +__version__ = "1.0.0" diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/pom.xml b/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/pom.xml new file mode 100644 index 0000000000..95453a5e6b --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + com.google.cloud.modernization.skills + ast-parser + 1.0-SNAPSHOT + + 11 + 11 + 8.1.5 + + + + org.openrewrite + rewrite-java + ${rewrite.version} + + + org.openrewrite + rewrite-java-11 + ${rewrite.version} + + + diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/src/main/java/OpenRewriteAstAnalyzer.java b/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/src/main/java/OpenRewriteAstAnalyzer.java new file mode 100644 index 0000000000..659891af3b --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/ast_parser/src/main/java/OpenRewriteAstAnalyzer.java @@ -0,0 +1,700 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.Parser; +import org.openrewrite.SourceFile; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeTree; +import org.openrewrite.tree.ParseError; + +/** + * OpenRewriteAstAnalyzer is a standalone Abstract Syntax Tree (AST) static analyzer powered by + * OpenRewrite. + * + *

During Phase 1 (Analysis & Discovery) of the WebLogic migration workflow, this analyzer + * scans a target legacy Java EE / WebLogic repository to extract precise, quantitative AST metrics. + * It identifies and counts occurrences of WebLogic specific APIs, Enterprise JavaBeans (EJBs), Java + * Message Service (JMS), Java Naming and Directory Interface (JNDI), distributed transactions, data + * access frameworks, security annotations, web tier components, advanced container features, and + * cloud-unfriendly patterns. + * + *

The output is serialized as a structured JSON string matching the exact schema required by + * {@code static_analyzer.py}. + */ +public class OpenRewriteAstAnalyzer { + + /** + * Data container class holding integer counters for each category of WebLogic/Java EE artifact + * discovered during AST traversal of the codebase. + */ + static class Metrics { + // --- WebLogic Specific APIs & Middleware --- + int wlsImportsCount = 0; + int loggingCount = 0; + int securityCount = 0; + int transactionCount = 0; + int wlsTransactionManagerCount = 0; + int wlsUserTransactionCount = 0; + int coherenceCount = 0; + int workmanagerCount = 0; + int jwsApiCount = 0; + + // --- Enterprise JavaBeans (EJB) --- + int statelessCount = 0; + int statefulCount = 0; + int singletonCount = 0; + int mdbCount = 0; + int entityBeanCount = 0; + int ejb2xHomeInterfacesCount = 0; + + // --- Java Message Service (JMS) --- + int jmsImportsCount = 0; + int jmsProducerCount = 0; + int jmsConsumerCount = 0; + int jmsQueueCount = 0; + int jmsTopicCount = 0; + + // --- Java Naming and Directory Interface (JNDI) --- + int initialContextCount = 0; + int lookupsCount = 0; + + // --- Transactions & Distributed Coordination --- + int userTransactionCount = 0; + int xaDatasourceCount = 0; + int xaResourceCount = 0; + int declarativeTransactionCount = 0; + int standardTransactionManagerCount = 0; + + // --- Data Access & Persistence --- + int jdbcConnectionsCount = 0; + int jpaEntitiesCount = 0; + int hibernateSessionsCount = 0; + int springDataRepositoriesCount = 0; + + // --- Security & Authorization --- + int rolesAllowedCount = 0; + int runAsCount = 0; + int userInRoleCount = 0; + int userPrincipalCount = 0; + + // --- Web Tier & Presentation Layer --- + int servletsCount = 0; + int filtersCount = 0; + int listenersCount = 0; + int jaxRsEndpointsCount = 0; + int springControllersCount = 0; + int strutsCount = 0; + int jsfCount = 0; + int httpSessionsCount = 0; + + // --- Advanced Container Features & Web Services --- + int timersCount = 0; + int webServicesCount = 0; + int jaxRpcCount = 0; + int jaxWsCount = 0; + int batchProcessingCount = 0; + int jmxMbeansCount = 0; + + // --- Cloud-Unfriendly Patterns & Migration Blockers --- + int rawThreadsCount = 0; + int nativeProcessesCount = 0; + int directSocketsCount = 0; + int jaasLoginModulesCount = 0; + int specificProxiesCount = 0; + int fileIoCount = 0; + int rmiCorbaCount = 0; + int javaMailCount = 0; + } + + /** + * Main entry point for CLI execution. + * + *

Recursively walks the target directory for Java source files ({@code .java} and {@code + * .jws}), constructs OpenRewrite AST compilation units, traverses them with {@link + * MetricsVisitor}, and prints the compiled JSON report to standard output. + * + * @param args Command-line arguments; {@code args[0]} must specify the target project root + * directory. + * @throws IOException If an I/O error occurs while reading source files from disk. + */ + public static void main(String[] args) throws IOException { + // Validate command-line argument count + if (args.length < 1) { + System.err.println("Usage: OpenRewriteAstAnalyzer "); + System.exit(1); + } + + Path targetDir = Paths.get(args[0]); + List javaFiles; + + // Recursively walk filesystem to locate all .java and .jws (Java Web Service) files + try (Stream walk = Files.walk(targetDir)) { + javaFiles = + walk.filter( + p -> { + String s = p.toString().toLowerCase(); + return s.endsWith(".java") || s.endsWith(".jws") || s.endsWith(".ejb"); + }) + .collect(Collectors.toList()); + } + + // Convert file paths into OpenRewrite Parser.Input stream suppliers + List inputs = + javaFiles.stream() + .map( + p -> { + Path targetPath = p; + if (p.toString().endsWith(".ejb")) { + targetPath = Paths.get(p.toString() + ".java"); + } + final Path finalPath = targetPath; + return new Parser.Input( + finalPath, + () -> { + try { + return Files.newInputStream(p); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + }) + .collect(Collectors.toList()); + + // Build OpenRewrite JavaParser configured for current Java LTS syntax + JavaParser parser = JavaParser.fromJavaVersion().build(); + + // Parse source inputs into AST CompilationUnit nodes in an in-memory execution context + List sourceFiles = + parser + .parseInputs(inputs, targetDir, new InMemoryExecutionContext()) + .collect(Collectors.toList()); + + List cus = + sourceFiles.stream() + .filter(s -> s instanceof J.CompilationUnit) + .map(J.CompilationUnit.class::cast) + .collect(Collectors.toList()); + + List parseErrors = + sourceFiles.stream() + .filter(s -> s instanceof ParseError) + .map(ParseError.class::cast) + .collect(Collectors.toList()); + + if (!parseErrors.isEmpty()) { + System.err.println("Warning: Failed to parse " + parseErrors.size() + " files:"); + for (ParseError error : parseErrors) { + System.err.println(" " + error.getSourcePath() + " -> " + error.toString()); + } + } + + // Initialize metrics container and visitor + Metrics metrics = new Metrics(); + MetricsVisitor visitor = new MetricsVisitor(); + + // Traverse each parsed compilation unit to populate metrics + for (J.CompilationUnit cu : cus) { + visitor.visit(cu, metrics); + } + + // Serialize collected metrics to JSON and output to stdout + System.out.println(buildJson(metrics)); + } + + /** + * Serializes the populated {@link Metrics} data container into a formatted JSON string. + * + *

The resulting JSON matches the exact schema expected by the Python static analysis pipeline + * ({@code static_analyzer.py}). + * + * @param m The populated {@link Metrics} instance. + * @return A formatted JSON string representing the AST analysis results. + */ + private static String buildJson(Metrics m) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + + // Serialize WebLogic specific API metrics + sb.append(" \"weblogic_api\": {\n"); + sb.append(" \"imports_count\": ").append(m.wlsImportsCount).append(",\n"); + sb.append(" \"logging_count\": ").append(m.loggingCount).append(",\n"); + sb.append(" \"security_count\": ").append(m.securityCount).append(",\n"); + sb.append(" \"transaction_count\": ").append(m.transactionCount).append(",\n"); + sb.append(" \"wls_transaction_manager_count\": ") + .append(m.wlsTransactionManagerCount) + .append(",\n"); + sb.append(" \"wls_user_transaction_count\": ") + .append(m.wlsUserTransactionCount) + .append(",\n"); + sb.append(" \"coherence_count\": ").append(m.coherenceCount).append(",\n"); + sb.append(" \"workmanager_count\": ").append(m.workmanagerCount).append(",\n"); + sb.append(" \"jws_api_count\": ").append(m.jwsApiCount).append("\n"); + sb.append(" },\n"); + + // Serialize EJB metrics + sb.append(" \"ejb\": {\n"); + sb.append(" \"stateless_count\": ").append(m.statelessCount).append(",\n"); + sb.append(" \"stateful_count\": ").append(m.statefulCount).append(",\n"); + sb.append(" \"singleton_count\": ").append(m.singletonCount).append(",\n"); + sb.append(" \"mdb_count\": ").append(m.mdbCount).append(",\n"); + sb.append(" \"entity_bean_count\": ").append(m.entityBeanCount).append(",\n"); + sb.append(" \"ejb_2x_home_interfaces_count\": ") + .append(m.ejb2xHomeInterfacesCount) + .append("\n"); + sb.append(" },\n"); + + // Serialize JMS metrics + sb.append(" \"jms\": {\n"); + sb.append(" \"imports_count\": ").append(m.jmsImportsCount).append(",\n"); + sb.append(" \"producer_count\": ").append(m.jmsProducerCount).append(",\n"); + sb.append(" \"consumer_count\": ").append(m.jmsConsumerCount).append(",\n"); + sb.append(" \"queue_count\": ").append(m.jmsQueueCount).append(",\n"); + sb.append(" \"topic_count\": ").append(m.jmsTopicCount).append("\n"); + sb.append(" },\n"); + + // Serialize JNDI metrics + sb.append(" \"jndi\": {\n"); + sb.append(" \"initial_context_count\": ").append(m.initialContextCount).append(",\n"); + sb.append(" \"lookups_count\": ").append(m.lookupsCount).append("\n"); + sb.append(" },\n"); + + // Serialize Transaction metrics + sb.append(" \"transactions\": {\n"); + sb.append(" \"user_transaction_count\": ").append(m.userTransactionCount).append(",\n"); + sb.append(" \"xa_datasource_count\": ").append(m.xaDatasourceCount).append(",\n"); + sb.append(" \"xa_resource_count\": ").append(m.xaResourceCount).append(",\n"); + sb.append(" \"declarative_transaction_count\": ") + .append(m.declarativeTransactionCount) + .append(",\n"); + sb.append(" \"standard_transaction_manager_count\": ") + .append(m.standardTransactionManagerCount) + .append("\n"); + sb.append(" },\n"); + + // Serialize Data Access metrics + sb.append(" \"data_access\": {\n"); + sb.append(" \"jdbc_connections_count\": ").append(m.jdbcConnectionsCount).append(",\n"); + sb.append(" \"jpa_entities_count\": ").append(m.jpaEntitiesCount).append(",\n"); + sb.append(" \"hibernate_sessions_count\": ").append(m.hibernateSessionsCount).append(",\n"); + sb.append(" \"spring_data_repositories_count\": ") + .append(m.springDataRepositoriesCount) + .append("\n"); + sb.append(" },\n"); + + // Serialize Security metrics + sb.append(" \"security\": {\n"); + sb.append(" \"roles_allowed_count\": ").append(m.rolesAllowedCount).append(",\n"); + sb.append(" \"run_as_count\": ").append(m.runAsCount).append(",\n"); + sb.append(" \"user_in_role_count\": ").append(m.userInRoleCount).append(",\n"); + sb.append(" \"user_principal_count\": ").append(m.userPrincipalCount).append("\n"); + sb.append(" },\n"); + + // Serialize Web Tier metrics + sb.append(" \"web_tier\": {\n"); + sb.append(" \"servlets_count\": ").append(m.servletsCount).append(",\n"); + sb.append(" \"filters_count\": ").append(m.filtersCount).append(",\n"); + sb.append(" \"listeners_count\": ").append(m.listenersCount).append(",\n"); + sb.append(" \"jax_rs_endpoints_count\": ").append(m.jaxRsEndpointsCount).append(",\n"); + sb.append(" \"spring_controllers_count\": ").append(m.springControllersCount).append(",\n"); + sb.append(" \"struts_count\": ").append(m.strutsCount).append(",\n"); + sb.append(" \"jsf_count\": ").append(m.jsfCount).append(",\n"); + sb.append(" \"http_sessions_count\": ").append(m.httpSessionsCount).append("\n"); + sb.append(" },\n"); + + // Serialize Advanced Container Features metrics + sb.append(" \"advanced_features\": {\n"); + sb.append(" \"work_managers_count\": ").append(m.workmanagerCount).append(",\n"); + sb.append(" \"timers_count\": ").append(m.timersCount).append(",\n"); + sb.append(" \"web_services_count\": ").append(m.webServicesCount).append(",\n"); + sb.append(" \"jax_rpc_count\": ").append(m.jaxRpcCount).append(",\n"); + sb.append(" \"jax_ws_count\": ").append(m.jaxWsCount).append(",\n"); + sb.append(" \"batch_processing_count\": ").append(m.batchProcessingCount).append(",\n"); + sb.append(" \"jmx_mbeans_count\": ").append(m.jmxMbeansCount).append("\n"); + sb.append(" },\n"); + + // Serialize Cloud-Unfriendly Patterns metrics + sb.append(" \"cloud_unfriendly_patterns\": {\n"); + sb.append(" \"raw_threads_count\": ").append(m.rawThreadsCount).append(",\n"); + sb.append(" \"native_processes_count\": ").append(m.nativeProcessesCount).append(",\n"); + sb.append(" \"direct_sockets_count\": ").append(m.directSocketsCount).append(",\n"); + sb.append(" \"jaas_login_modules_count\": ").append(m.jaasLoginModulesCount).append(",\n"); + sb.append(" \"specific_proxies_count\": ").append(m.specificProxiesCount).append(",\n"); + sb.append(" \"file_io_count\": ").append(m.fileIoCount).append(",\n"); + sb.append(" \"rmi_corba_count\": ").append(m.rmiCorbaCount).append(",\n"); + sb.append(" \"java_mail_count\": ").append(m.javaMailCount).append("\n"); + sb.append(" }\n"); + + sb.append("}"); + return sb.toString(); + } + + /** + * AST Visitor implementation that inspects Java language syntax nodes to populate {@link + * Metrics}. + * + *

Overrides OpenRewrite visitor hooks for package imports, annotations, method invocations, + * class/interface declarations, constructors, and variable declarations. + */ + private static class MetricsVisitor extends JavaVisitor { + + /** + * Inspects package import statements to identify library dependencies and framework usage. + * + * @param imp The AST import node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitImport(J.Import imp, Metrics m) { + String pkg = imp.getPackageName(); + if (pkg != null) { + // Detect WebLogic specific APIs + if (pkg.startsWith("weblogic")) { + m.wlsImportsCount++; + } + if (pkg.startsWith("weblogic.logging")) { + m.loggingCount++; + } + if (pkg.startsWith("weblogic.security")) { + m.securityCount++; + } + if (pkg.startsWith("weblogic.transaction")) { + m.transactionCount++; + } + if (pkg.startsWith("weblogic.work")) { + m.workmanagerCount++; + } + if (pkg.startsWith("weblogic.jws")) { + m.jwsApiCount++; + } + + // Detect Oracle Coherence distributed caching + if (pkg.startsWith("com.tangosol") || pkg.startsWith("coherence")) { + m.coherenceCount++; + } + + // Detect messaging and web services APIs + if (pkg.startsWith("javax.jms") || pkg.startsWith("jakarta.jms")) { + m.jmsImportsCount++; + } + if (pkg.startsWith("javax.xml.rpc")) { + m.jaxRpcCount++; + } + if (pkg.startsWith("javax.jws") || pkg.startsWith("jakarta.jws")) { + m.jaxWsCount++; + } + + // Detect batch processing and legacy remoting + if (pkg.startsWith("javax.batch") || pkg.startsWith("org.springframework.batch")) { + m.batchProcessingCount++; + } + if (pkg.startsWith("java.rmi") || pkg.startsWith("javax.rmi")) { + m.rmiCorbaCount++; + } + if (pkg.startsWith("javax.mail") || pkg.startsWith("jakarta.mail")) { + m.javaMailCount++; + } + + // Detect legacy presentation frameworks + if (pkg.startsWith("org.apache.struts")) { + m.strutsCount++; + } + if (pkg.startsWith("javax.faces") || pkg.startsWith("jakarta.faces")) { + m.jsfCount++; + } + } + return super.visitImport(imp, m); + } + + /** + * Inspects Java annotations to identify EJB types, declarative transactions, JPA entities, + * security roles, servlets, web filters, and REST/SOAP endpoints. + * + * @param annotation The AST annotation node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitAnnotation(J.Annotation annotation, Metrics m) { + String name = annotation.getSimpleName(); + + // EJB component annotations + if (name.equals("Stateless")) { + m.statelessCount++; + } + if (name.equals("Stateful")) { + m.statefulCount++; + } + if (name.equals("Singleton")) { + m.singletonCount++; + } + if (name.equals("MessageDriven")) { + m.mdbCount++; + } + + // Declarative transaction and persistence annotations + if (name.equals("Transactional") || name.equals("TransactionAttribute")) { + m.declarativeTransactionCount++; + } + if (name.equals("Entity")) { + m.jpaEntitiesCount++; + } + + // Security and authorization annotations + if (name.equals("RolesAllowed")) { + m.rolesAllowedCount++; + } + if (name.equals("RunAs")) { + m.runAsCount++; + } + + // Web tier and servlet container annotations + if (name.equals("WebServlet")) { + m.servletsCount++; + } + if (name.equals("WebFilter")) { + m.filtersCount++; + } + if (name.equals("WebListener")) { + m.listenersCount++; + } + + // Modern REST controllers and JSF backing beans + if (name.equals("Path") || name.equals("GET") || name.equals("POST") || name.equals("PUT")) { + m.jaxRsEndpointsCount++; + } + if (name.equals("Controller") || name.equals("RestController")) { + m.springControllersCount++; + } + if (name.equals("ManagedBean")) { + m.jsfCount++; + } + + // SOAP Web Service annotations + if (name.equals("WebService") || name.equals("WebMethod")) { + m.webServicesCount++; + } + + return super.visitAnnotation(annotation, m); + } + + /** + * Inspects method invocations to detect JNDI lookups, HTTP session state access, programmatic + * security checks, and native OS process execution. + * + * @param method The AST method invocation node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitMethodInvocation(J.MethodInvocation method, Metrics m) { + String name = method.getSimpleName(); + if (name.equals("lookup")) { + m.lookupsCount++; + } + if (name.equals("getSession")) { + m.httpSessionsCount++; + } + if (name.equals("isUserInRole")) { + m.userInRoleCount++; + } + if (name.equals("getUserPrincipal")) { + m.userPrincipalCount++; + } + if (name.equals("exec")) { + m.nativeProcessesCount++; + } + return super.visitMethodInvocation(method, m); + } + + /** + * Inspects class and interface declarations to identify legacy EJB 2.x BMP/CMP entity beans, + * MDBs, servlet filters, listeners, JAAS login modules, and WebLogic specific cluster servlets. + * + * @param classDecl The AST class declaration node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitClassDeclaration(J.ClassDeclaration classDecl, Metrics m) { + // Check implemented interfaces + if (classDecl.getImplements() != null) { + for (TypeTree type : classDecl.getImplements()) { + String ts = type.toString(); + if (ts.contains("EntityBean")) { + m.entityBeanCount++; + } + if (ts.contains("MessageDrivenBean")) { + m.mdbCount++; + } + if (ts.contains("SessionBean")) { + if ("Stateful".equals(getSessionEJBType(classDecl))) { + m.statefulCount++; + } else { + m.statelessCount++; + } + } + if (ts.contains("Filter")) { + m.filtersCount++; + } + if (ts.contains("ServletContextListener")) { + m.listenersCount++; + } + if (ts.contains("LoginModule")) { + m.jaasLoginModulesCount++; + } + } + } + + // Check extended superclasses + if (classDecl.getExtends() != null) { + String ts = classDecl.getExtends().toString(); + if (ts.contains("EntityBean")) { + m.entityBeanCount++; + } + if (ts.contains("GenericSessionBean") || ts.contains("SessionBean")) { + if ("Stateful".equals(getSessionEJBType(classDecl))) { + m.statefulCount++; + } else { + m.statelessCount++; + } + } + if (ts.contains("GenericMessageDrivenBean") || ts.contains("MessageDrivenBean")) { + m.mdbCount++; + } + if (ts.contains("EJBHome") + || ts.contains("EJBLocalHome") + || ts.contains("EJBObject") + || ts.contains("EJBLocalObject")) { + m.ejb2xHomeInterfacesCount++; + } + if (ts.contains("HttpServlet")) { + m.servletsCount++; + } + if (ts.contains("UsernamePasswordLoginModule")) { + m.jaasLoginModulesCount++; + } + if (ts.contains("HttpClusterServlet") || ts.contains("HttpProxyServlet")) { + m.specificProxiesCount++; + } + } + return super.visitClassDeclaration(classDecl, m); + } + + /** + * Inspects constructor invocations (new object instantiations) to detect JNDI InitialContext + * creations, raw thread spawning, direct sockets, local filesystem I/O, and native process + * builders. + * + * @param newClass The AST constructor invocation node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitNewClass(J.NewClass newClass, Metrics m) { + String type = newClass.getClazz() != null ? newClass.getClazz().toString() : ""; + if (type.contains("InitialContext")) { + m.initialContextCount++; + } + if (type.contains("Thread")) { + m.rawThreadsCount++; + } + if (type.contains("Socket") || type.contains("ServerSocket")) { + m.directSocketsCount++; + } + if (type.contains("FileOutputStream") + || type.contains("FileWriter") + || type.contains("RandomAccessFile")) { + m.fileIoCount++; + } + if (type.contains("ProcessBuilder")) { + m.nativeProcessesCount++; + } + return super.visitNewClass(newClass, m); + } + + /** + * Inspects variable declarations to detect WebLogic transaction managers, programmatic user + * transactions, JMS producers/consumers/resources, XA datasources, JDBC connections, + * Hibernate/HTTP sessions, timers, and JMX MBeans. + * + * @param multiVariable The AST variable declaration node being visited. + * @param m The metrics container to increment. + * @return The visited AST node. + */ + @Override + public J visitVariableDeclarations(J.VariableDeclarations multiVariable, Metrics m) { + String type = + multiVariable.getTypeExpression() != null + ? multiVariable.getTypeExpression().toString() + : ""; + if (type.contains("TransactionManager") || type.contains("TxHelper")) { + m.wlsTransactionManagerCount++; + } + if (type.contains("UserTransaction")) { + m.wlsUserTransactionCount++; + } + if (type.contains("MessageProducer") + || type.contains("QueueSender") + || type.contains("TopicPublisher")) { + m.jmsProducerCount++; + } + if (type.contains("MessageConsumer") + || type.contains("QueueReceiver") + || type.contains("TopicSubscriber")) { + m.jmsConsumerCount++; + } + if (type.contains("Queue")) { + m.jmsQueueCount++; + } + if (type.contains("Topic")) { + m.jmsTopicCount++; + } + if (type.contains("XADataSource")) { + m.xaDatasourceCount++; + } + if (type.contains("XAResource")) { + m.xaResourceCount++; + } + if (type.contains("Connection") || type.contains("DataSource")) { + m.jdbcConnectionsCount++; + } + if (type.contains("Session") && type.contains("hibernate")) { + m.hibernateSessionsCount++; + } + if (type.contains("HttpSession")) { + m.httpSessionsCount++; + } + if (type.contains("TimerService") || type.contains("Timer")) { + m.timersCount++; + } + if (type.contains("MBeanServer")) { + m.jmxMbeansCount++; + } + return super.visitVariableDeclarations(multiVariable, m); + } + + private String getSessionEJBType(J.ClassDeclaration classDecl) { + if (classDecl.getPrefix() == null || classDecl.getPrefix().getComments() == null) { + return "Stateless"; + } + java.util.regex.Pattern pattern = + java.util.regex.Pattern.compile( + "@ejbgen:session[^\\*]*type\\s*=\\s*Stateful", java.util.regex.Pattern.DOTALL); + for (org.openrewrite.java.tree.Comment comment : classDecl.getPrefix().getComments()) { + String text = comment.toString(); + if (pattern.matcher(text).find()) { + return "Stateful"; + } + } + return "Stateless"; + } + } +} diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/build_parser.py b/skills/cloud/google-cloud-weblogic-migration/scripts/build_parser.py new file mode 100644 index 0000000000..6be514bfe7 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/build_parser.py @@ -0,0 +1,263 @@ +"""Build system parser for scanning Maven pom.xml and Apache Ant build.xml configurations in WebLogic monoliths. + +This module recursively scans legacy applications to identify and parse build +files. +It analyzes: +- pom.xml (Maven dependencies, plugins, compiler configurations, and version +properties). +- build.xml (Apache Ant scripts locations to flag build-system modernization +requirements). + +It maps dependency listings, flags specific or outdated WebLogic and Java EE +library imports, and determines the target compiler Java version of the legacy +monolith modules. +""" + +import os +import re +import xml.etree.ElementTree as ET + +# ===================================================================== +# GENERAL XML UTILITIES +# ===================================================================== + + +def strip_ns(tag): + """Strips the namespace prefix from an XML tag. + + Args: + tag (str): The XML element tag string (possibly containing namespace). + + Returns: + str: The raw tag name without namespace. + """ + if "}" in tag: + return tag.split("}", 1)[1] + return tag + + +# ===================================================================== +# MAVEN POM PARSING & DEPENDENCY METRICS +# ===================================================================== +def parse_pom_dependencies(root): + """Parses pom.xml dependencies under the `` node. + + Args: + root (xml.etree.ElementTree.Element): The XML root element of pom.xml. + + Returns: + list[dict]: A list of dependency dictionaries containing groupId, + artifactId, and version. + """ + dependencies = [] + ns = "" + if root.tag.startswith("{"): + ns = root.tag.split("}")[0] + "}" + + deps_node = root.find(f"./{ns}dependencies") + if deps_node is not None: + for dep in deps_node.findall(f"./{ns}dependency"): + group_id_node = dep.find(f"./{ns}groupId") + artifact_id_node = dep.find(f"./{ns}artifactId") + version_node = dep.find(f"./{ns}version") + + group_id = group_id_node.text.strip() if group_id_node is not None else "" + artifact_id = ( + artifact_id_node.text.strip() if artifact_id_node is not None else "" + ) + version = ( + version_node.text.strip() if version_node is not None else "managed" + ) + + dependencies.append( + {"groupId": group_id, "artifactId": artifact_id, "version": version} + ) + return dependencies + + +def parse_pom_properties(root): + """Parses properties defined inside the `` block of pom.xml. + + Args: + root (xml.etree.ElementTree.Element): The XML root element of pom.xml. + + Returns: + dict: A dictionary of key-value property settings. + """ + properties = {} + ns = "" + if root.tag.startswith("{"): + ns = root.tag.split("}")[0] + "}" + + props_node = root.find(f"./{ns}properties") + if props_node is not None: + for prop in props_node: + tag = strip_ns(prop.tag) + properties[tag] = prop.text.strip() if prop.text else "" + return properties + + +def get_java_version(root, properties): + """Determines the target Java version from POM properties or the maven-compiler-plugin config. + + Args: + root (xml.etree.ElementTree.Element): The XML root element of pom.xml. + properties (dict): Already parsed POM properties mapping. + + Returns: + str: String version identifier (e.g. '1.8', '11', '17') or 'Unknown'. + """ + ns = "" + if root.tag.startswith("{"): + ns = root.tag.split("}")[0] + "}" + + java_version_keys = [ + "java.version", + "maven.compiler.source", + "maven.compiler.target", + "jdk.version", + ] + for key in java_version_keys: + if key in properties: + return properties[key] + + plugins = root.findall(f".//{ns}plugin") + for plugin in plugins: + art_id = plugin.find(f"./{ns}artifactId") + if art_id is not None and art_id.text == "maven-compiler-plugin": + config = plugin.find(f"./{ns}configuration") + if config is not None: + source = config.find(f"./{ns}source") + target = config.find(f"./{ns}target") + if source is not None: + return source.text.strip() + if target is not None: + return target.text.strip() + return "Unknown" + + +def analyze_dependencies(dependencies): + """Categorizes list of parsed dependencies into WebLogic-specific, legacy Java EE standard, or other third party libraries. + + Args: + dependencies (list[dict]): List of dependencies to categorize. + + Returns: + dict: A nested dictionary of categorized dependency lists. + """ + analysis = {"weblogic_specific": [], "java_ee_legacy": [], "others": []} + + wls_patterns = [ + re.compile(r"com\.oracle\.weblogic"), + re.compile(r"weblogic"), + ] + + java_ee_patterns = [ + re.compile(r"javax\.j2ee"), + re.compile(r"javax\.ejb"), + re.compile(r"javax\.jms"), + re.compile(r"javax\.servlet"), + re.compile(r"javax\.transaction"), + re.compile(r"glassfish"), + re.compile(r"jboss"), + ] + + for dep in dependencies: + g_id = dep["groupId"] + a_id = dep["artifactId"] + + is_wls = any(p.search(g_id) or p.search(a_id) for p in wls_patterns) + is_legacy = any(p.search(g_id) or p.search(a_id) for p in java_ee_patterns) + + if is_wls: + analysis["weblogic_specific"].append(dep) + elif is_legacy: + analysis["java_ee_legacy"].append(dep) + else: + analysis["others"].append(dep) + + return analysis + + +def parse_pom(path): + """Parses a single pom.xml file to extract properties, Java version, and dependency analysis. + + Args: + path (str): File path to pom.xml. + + Returns: + dict: A summary dictionary of properties and dependencies. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + + properties = parse_pom_properties(root) + java_version = get_java_version(root, properties) + dependencies = parse_pom_dependencies(root) + dep_analysis = analyze_dependencies(dependencies) + + return { + "java_version": java_version, + "properties": properties, + "dependencies_summary": { + "total": len(dependencies), + "weblogic_specific_count": len(dep_analysis["weblogic_specific"]), + "java_ee_legacy_count": len(dep_analysis["java_ee_legacy"]), + }, + "weblogic_dependencies": dep_analysis["weblogic_specific"], + "legacy_java_ee_dependencies": dep_analysis["java_ee_legacy"], + } + except Exception as e: + return {"error": f"Failed to parse pom.xml: {str(e)}"} + + +# ===================================================================== +# BUILD SYSTEM DISCOVERY AND RUNTIME ORCHESTRATION +# ===================================================================== +def find_files(target_dir, filenames): + """Recursively scans the directory to find files matching given file names. + + Args: + target_dir (str): Directory path to scan. + filenames (list[str]): File names to search. + + Returns: + list[str]: Absolute paths to files found. + """ + found = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.lower() in [name.lower() for name in filenames]: + found.append(os.path.join(root, f)) + return found + + +def parse_build_files(target_dir): + """Locates and parses all Maven and Ant build descriptors across the monolith directory. + + Args: + target_dir (str): The repository root directory. + + Returns: + dict: A compiled report dictionary categorizing project structures. + """ + report = {"maven_projects": [], "ant_projects": []} + + poms = find_files(target_dir, ["pom.xml"]) + for pom in poms: + report["maven_projects"].append( + {"path": os.path.relpath(pom, target_dir), "analysis": parse_pom(pom)} + ) + + ants = find_files(target_dir, ["build.xml"]) + for ant in ants: + report["ant_projects"].append({ + "path": os.path.relpath(ant, target_dir), + "info": ( + "Ant build file detected. Use analyze_jars.py to scan local" + " libraries if dependencies are checked-in." + ), + }) + + return report diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/cli.py b/skills/cloud/google-cloud-weblogic-migration/scripts/cli.py new file mode 100644 index 0000000000..c2222c570d --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/cli.py @@ -0,0 +1,786 @@ +"""Command-line interface orchestrator for the WebLogic monolith decomposition analyzer. + +This module acts as the primary entry point for the analysis and microservices +boundary mapping tool. +It orchestrates the execution of all parsing and analysis engines, including: +- Directory scans and cloud-unfriendly pattern detection (static_analyzer) +- Java EE descriptors and XML configuration parsers (config_parser) +- Maven POM and Ant build configurations (build_parser) +- WebLogic domain config and resources (domain_parser) +- Local JAR dependencies (jar_analyzer) +- Code imports and directed dependency graphing (import_analyzer) +- Database table access patterns (db_mapper) + +It passes the extracted dependency graph and database maps to the clustering +engine (clusterer) to run the Louvain community detection algorithm under +multiple resolutions and custom edge-weighting knobs. +Finally, it compiles these results into a unified JSON schema or a comprehensive +Markdown report designed to be rendered as an interactive UI Artifact. + +Usage: + python3 cli.py analyze [options] + +Subcommands: + analyze Run the full static analysis and + microservice decomposition pipeline. + +Options: + --base-package The base package prefix (e.g. com.example) + to filter internal dependencies. + --format {json,markdown} Output format. Default is json. + --confirmed-utilities Comma-separated list of FQCNs to exclude + from clustering as utilities. + --exclude-patterns Comma-separated list of regex patterns to + exclude from the clustering graph. + --resolution [res1 res2 ...] List of resolution parameters for Louvain + clustering (default: 1.0). + --presentation-package-pattern Regex patterns matching + UI/presentation classes (smart default). + --infrastructure-package-pattern Regex patterns matching + cross-cutting noise (smart default). + --presentation-mode {extract_spa,vertical_slice,ignore} UI classes + partitioning mode (default: extract_spa). + --weight-domain-anchor Weight for strong domain anchor bonds + (default: 5.0). + --weight-entry-exclusive Weight for exclusive entry point bonds + (default: 3.0). + --weight-entry-glue Weight for orchestrator/glue entry points + (default: 0.1). + --weight-db-penalty Penalty weight for cross-entity foreign keys + (default: 0.01). + --weight-class-table Weight for class-to-database-table bindings + (default: 5.0). + --utility-threshold Fan-in ratio threshold for shared utilities + (default: 0.07). + --output Write report to file instead of stdout. +""" + +import argparse +import json +import os +import sys +import build_parser +import clusterer +import config_parser +import db_mapper +import domain_parser +import import_analyzer +import jar_analyzer + +# Import modules from the same directory +import static_analyzer + +# ===================================================================== +# DEFAULT MODULE NAME PATTERNS FOR PRESENTATION AND INFRASTRUCTURE +# ===================================================================== +DEFAULT_PRESENTATION_PATTERNS = [ + r".*\.(web|ui|actions|controllers|views|presentation|swing|client|struts|jsf|faces|mvc)\..*", + r".*(Action|Servlet|Controller|Presenter|View|PageBean|SwingClient|Frame)$", +] +DEFAULT_INFRASTRUCTURE_PATTERNS = [ + r".*\.(util|utils|common|helpers|constants|config|logging|exceptions|filters|security)\..*", + r".*(Exception|Error|Utils|Util|Constants|Helper|Factory|Locator|Filter|Listener|Log4jInit|Home|HomeFactory|Properties)$", +] + + +# ===================================================================== +# CORE ANALYSIS PIPELINE ORCHESTRATION +# ===================================================================== + + +def run_full_analysis( + target_dir, + base_package=None, + confirmed_utilities=None, + exclude_patterns=None, + resolutions=None, + presentation_package_pattern=None, + infrastructure_package_pattern=None, + presentation_mode="extract_spa", + weight_kwargs=None, +): + """Orchestrates the entire decomposition pipeline. + + Orchestrates the entire decomposition pipeline by running all analyzer + modules, including static code analysis, config XML parsing, build setups, + jar dependencies, Java package imports, database tables mapping, and Louvain + graph clustering. + + Args: + target_dir (str): The target legacy monolith repository directory. + base_package (str, optional): The base package filter (e.g. + 'com.acme.medimed'). + confirmed_utilities (list[str], optional): Explicit utility FQCNs to + exclude from clustering. + exclude_patterns (list[str], optional): Regex patterns of classes to + exclude from clustering. + resolutions (list[float], optional): List of Louvain resolutions to + execute. + presentation_package_pattern (list[str], optional): Regex patterns + matching presentation classes. + infrastructure_package_pattern (list[str], optional): Regex patterns + matching infrastructure noise. + presentation_mode (str): Mode for UI/presentation classes partitioning + (extract_spa|vertical_slice|ignore). + weight_kwargs (dict, optional): Custom edge weight overrides for community + detection. + + Returns: + dict: A compiled unified JSON dictionary containing all analysis reports + and microservice clusters. + """ + if resolutions is None: + resolutions = [1.0] + if weight_kwargs is None: + weight_kwargs = {} + + static_report = static_analyzer.analyze_static(target_dir) + configs_report = config_parser.parse_configurations(target_dir) + build_report = build_parser.parse_build_files(target_dir) + domain_report = domain_parser.parse_domain_config(target_dir) + jars_report = jar_analyzer.analyze_local_jars(target_dir) + imports_report = import_analyzer.analyze_imports(target_dir, base_package) + db_report = db_mapper.map_db_usage(target_dir) + + # Run clustering using the outputs of imports and db mapping + clustering_reports = {} + top_level_utility_eval = {} + for res in resolutions: + res_report = clusterer.find_service_clusters( + imports_report, + db_report, + confirmed_utilities, + exclude_patterns, + res, + presentation_package_pattern, + infrastructure_package_pattern, + presentation_mode, + **weight_kwargs, + ) + if not top_level_utility_eval and "utility_evaluation" in res_report: + top_level_utility_eval = res_report.pop("utility_evaluation") + elif "utility_evaluation" in res_report: + res_report.pop("utility_evaluation") + clustering_reports[str(res)] = res_report + + return { + "target_directory": os.path.abspath(target_dir), + "static_analysis": static_report, + "configuration_descriptors": configs_report, + "build_configuration": build_report, + "weblogic_domain_configuration": domain_report, + "local_jars": jars_report, + "package_dependencies": imports_report, + "database_usage": db_report, + "utility_evaluation": top_level_utility_eval, + "microservices_clustering": clustering_reports, + } + + +# ===================================================================== +# REPORT FORMATTING AND MARKDOWN GENERATION +# ===================================================================== + + +def format_markdown_report(report): + """Formats the unified analysis JSON report into a readable Markdown document. + + Outputs a clean summary table for multi-resolution runs to avoid document + bloat, and a detailed package/class boundary map and Mermaid diagram for + single resolution runs. + + Args: + report (dict): The compiled unified analysis JSON report dictionary. + + Returns: + str: A Markdown formatted document. + """ + static = report["static_analysis"] + build = report["build_configuration"] + domain = report["weblogic_domain_configuration"] + jars = report["local_jars"] + clustering_dict = report.get("microservices_clustering", {}) + + clustering = report["microservices_clustering"] + + md = [] + md.append("# WebLogic Migration Unified Analysis Report") + md.append(f"**Target Directory:** `{report['target_directory']}`\n") + + md.append("## 1. Executive Summary") + gen = static["general"] + web_tier = static["web_tier"] + wls_deps = static["weblogic_api"]["imports_count"] + first_res = list(clustering.keys())[0] if clustering else None + proposed_count = ( + clustering.get(first_res, {}) + .get("summary", {}) + .get("proposed_services_count", 0) + if first_res + else 0 + ) + md.append(f"* **Total Files Scanned:** {gen.get('total_files', 0)}") + md.append( + f"* **Java Source Files (.java, .ejb):** {gen.get('java_files', 0)}" + ) + md.append( + f"* **JWS Web Service Source Files (.jws):** {gen.get('jws_files', 0)}" + ) + md.append(f"* **XML Configurations (.xml):** {gen.get('xml_files', 0)}") + md.append( + " * **Standard Web Descriptors (web.xml):**" + f" {web_tier.get('web_xml_count', 0)}" + ) + md.append( + " * **WebLogic Web Descriptors (weblogic.xml):**" + f" {web_tier.get('weblogic_xml_count', 0)}" + ) + md.append( + f"* **JSP/JSPX View Templates:** {web_tier.get('jsp_files_count', 0)}" + ) + md.append( + "* **JSF/Facelets View Templates (.xhtml, .jsf, .faces):**" + f" {gen.get('jsf_files', 0)}" + ) + md.append( + f"* **HTML View Files (.html, .htm):** {gen.get('html_files', 0)}" + ) + md.append(f"* **Properties Files:** {gen.get('properties_files', 0)}") + md.append(f"* **WebLogic API Dependencies:** {wls_deps} occurrences") + md.append(f"* **Proposed Microservices:** {proposed_count}\n") + + md.append("## 2. Build & Environment") + for proj in build.get("maven_projects", []): + md.append(f"### Maven Project: `{proj['path']}`") + analysis = proj["analysis"] + if "error" in analysis: + md.append(f"* Error: {analysis['error']}") + else: + md.append(f"* **Target Java Version:** {analysis['java_version']}") + md.append( + "* **Dependencies Count:**" + f" {analysis['dependencies_summary']['total']}" + ) + md.append( + "* **WebLogic Libraries:**" + f" {analysis['dependencies_summary']['weblogic_specific_count']}" + ) + md.append( + "* **Legacy Java EE Libraries:**" + f" {analysis['dependencies_summary']['java_ee_legacy_count']}" + ) + + for proj in build.get("ant_projects", []): + md.append(f"### Ant Project: `{proj['path']}`") + md.append(f"* {proj['info']}") + md.append("") + + if jars["weblogic_specific"] or jars["java_ee_legacy"]: + md.append("### Checked-in Libraries (lib/ directory)") + for jar in jars["weblogic_specific"]: + md.append( + f"* `{jar['relative_path']}`: **WebLogic Specific**" + f" ({jar.get('title')} v{jar.get('version')})" + ) + for jar in jars["java_ee_legacy"]: + md.append( + f"* `{jar['relative_path']}`: **Legacy Java EE**" + f" ({jar.get('title')} v{jar.get('version')})" + ) + md.append("") + + md.append("## 3. Technical Inventory") + ejb = static["ejb"] + md.append("### Enterprise JavaBeans (EJB)") + md.append(f"* Stateless Session Beans: {ejb['stateless_count']}") + md.append(f"* Stateful Session Beans: {ejb['stateful_count']}") + md.append(f"* Message-Driven Beans (MDB): {ejb['mdb_count']}") + md.append(f"* CMP/BMP Entity Beans: {ejb.get('entity_bean_count', 0)}") + md.append( + "* EJB 2.x Home/Local Interfaces:" + f" {ejb.get('ejb_2x_home_interfaces_count', 0)}" + ) + md.append(f"* EJB XML Descriptors: {ejb['ejb_descriptors_count']}") + + jms = static["jms"] + md.append("### JMS & JNDI") + md.append(f"* JMS Imports: {jms['imports_count']}") + md.append(f"* JMS JNDI Lookups: {jms['jndi_lookups_count']}") + md.append(f"* JMS Senders / Producers: {jms.get('producer_count', 0)}") + md.append(f"* JMS Receivers / Consumers: {jms.get('consumer_count', 0)}") + md.append(f"* JMS Queues Referenced: {jms.get('queue_count', 0)}") + md.append(f"* JMS Topics Referenced: {jms.get('topic_count', 0)}") + md.append( + f"* InitialContext Creations: {static['jndi']['initial_context_count']}" + ) + md.append(f"* Total JNDI Lookups: {static['jndi']['lookups_count']}\n") + + tx = static["transactions"] + md.append("### Transactions & Distributed Coordination") + md.append( + "* Programmatic Transactions (`UserTransaction`):" + f" {tx['user_transaction_count']}" + ) + md.append( + "* Declarative Transactions (`@Transactional`," + f" `@TransactionAttribute`): {tx.get('declarative_transaction_count', 0)}" + ) + md.append( + "* Standard Transaction Managers Referenced:" + f" {tx.get('standard_transaction_manager_count', 0)}" + ) + md.append("* XA Distributed Transactions:") + md.append(f" * XA Data Sources: {tx['xa_datasource_count']}") + md.append( + f" * XA Resources (`XAResource`): {tx.get('xa_resource_count', 0)}\n" + ) + + advanced = static["advanced_features"] + md.append("### Advanced Container Features & Web Services") + md.append(f"* Work Managers: {advanced['work_managers_count']}") + md.append(f"* Timers: {advanced['timers_count']}") + md.append(f"* Resource Adapters: {advanced['resource_adapters_count']}") + md.append(f"* Total Web Services: {advanced['web_services_count']}") + md.append(f" * JAX-RPC (Legacy): {advanced.get('jax_rpc_count', 0)}") + md.append(f" * JAX-WS (Standard): {advanced.get('jax_ws_count', 0)}\n") + + web_tier = static["web_tier"] + md.append("### Web Tier & Presentation Layer") + md.append(f"* JSP/JSPX Files: {web_tier['jsp_files_count']}") + md.append( + "* JSF/Facelets Templates (.xhtml, .jsf, .faces):" + f" {gen.get('jsf_files', 0)}" + ) + md.append(f"* HTML Files (.html, .htm): {gen.get('html_files', 0)}") + md.append(f"* Servlets: {web_tier['servlets_count']}") + md.append(f"* Servlet Filters: {web_tier.get('filters_count', 0)}") + md.append(f"* Servlet Listeners: {web_tier.get('listeners_count', 0)}") + md.append( + "* Modern REST Endpoints (JAX-RS):" + f" {web_tier.get('jax_rs_endpoints_count', 0)}" + ) + md.append( + "* Spring MVC Controllers:" + f" {web_tier.get('spring_controllers_count', 0)}" + ) + md.append(f"* Struts Usages: {web_tier.get('struts_count', 0)}") + md.append(f"* JSF Usages (in Java Code): {web_tier.get('jsf_count', 0)}") + md.append( + f"* Standard XML Descriptors (web.xml): {web_tier['web_xml_count']}" + ) + md.append( + "* WebLogic XML Descriptors (weblogic.xml):" + f" {web_tier['weblogic_xml_count']}\n" + ) + + md.append("### Cloud-Unfriendly Patterns") + unfriendly = static.get("cloud_unfriendly_patterns", {}) + md.append( + "* Hardcoded IP Addresses (excluding loopback):" + f" {unfriendly.get('hardcoded_ips_count', 0)}" + ) + md.append( + "* Hardcoded Absolute File Paths:" + f" {unfriendly.get('absolute_paths_count', 0)}" + ) + md.append( + "* Raw Thread Creations (`new Thread()`):" + f" {unfriendly.get('raw_threads_count', 0)}" + ) + md.append( + "* Native OS Command Executions (`exec`/`ProcessBuilder`):" + f" {unfriendly.get('native_processes_count', 0)}" + ) + md.append( + "* Direct Socket Connections (`new Socket`):" + f" {unfriendly.get('direct_sockets_count', 0)}" + ) + md.append( + "* JAAS/Custom Security Modules (`LoginModule`):" + f" {unfriendly.get('jaas_login_modules_count', 0)}" + ) + md.append( + "* Servlet Proxies" + " (`HttpClusterServlet`/`HttpProxyServlet`):" + f" {unfriendly.get('specific_proxies_count', 0)}\n" + ) + + wls_api = static["weblogic_api"] + md.append("### WebLogic Specific APIs & Middleware") + md.append(f"* Total WebLogic API Usages: {wls_api['imports_count']}") + md.append(f" * Logging: {wls_api.get('logging_count', 0)}") + md.append(f" * Security: {wls_api.get('security_count', 0)}") + md.append( + f" * Transactions (Total): {wls_api.get('transaction_count', 0)}" + ) + md.append( + " * Transaction Manager / TxHelper:" + f" {wls_api.get('wls_transaction_manager_count', 0)}" + ) + md.append( + " * UserTransaction:" + f" {wls_api.get('wls_user_transaction_count', 0)}" + ) + md.append(f" * Coherence Caching: {wls_api.get('coherence_count', 0)}") + md.append(f" * Work Manager APIs: {wls_api.get('workmanager_count', 0)}") + md.append( + f" * JWS Web Services APIs: {wls_api.get('jws_api_count', 0)}\n" + ) + + md.append("## 4. Shared Utility Evaluation") + # Utility evaluation is resolution-independent, grab it from top-level report + # or first candidate + eval_data = report.get("utility_evaluation", {}) + if not eval_data: + first_res = list(clustering_dict.keys())[0] if clustering_dict else None + first_clustering = clustering_dict.get(first_res, {}) if first_res else {} + eval_data = first_clustering.get("utility_evaluation", {}) + excluded = eval_data.get("excluded_utilities", []) + candidates = eval_data.get("candidates", []) + + if excluded: + md.append("### Excluded Shared Utilities") + md.append( + "The following classes were identified as utilities (via heuristics or" + " configuration) and excluded from the microservice clustering graph:" + ) + for ex in excluded: + # Find the candidate to show reason if available + reason = "" + for c in candidates: + if c["class_name"] == ex: + reason = f" ({c['reason']})" + break + md.append(f"* `{ex}`{reason}") + md.append("") + + other_candidates = [c for c in candidates if not c["is_confirmed_utility"]] + if other_candidates: + md.append( + "### High Fan-In Candidates for Review (LLM Semantic Evaluation" + " Required)" + ) + md.append( + "The following classes have high coupling (fan-in) but were not" + " automatically classified as utilities. You MUST semantically evaluate" + " the source code of these candidates to distinguish true business" + " aggregate roots from domain-agnostic utilities. Register the" + " domain-agnostic utilities using `--confirmed-utilities`:" + ) + for cand in other_candidates: + md.append( + f"* `{cand['class_name']}` (In-Degree: {cand['in_degree']}," + f" Out-Degree: {cand['out_degree']})" + ) + md.append("") + + if not excluded and not other_candidates: + md.append("*No high fan-in utility candidates detected.*\n") + + md.append("## 5. Web & Presentation Modernization Recommendation") + if web_tier["jsp_files_count"] > 0: + md.append("### Legacy JSP Migration Strategy") + md.append( + f"The application contains **{web_tier['jsp_files_count']} JSP files**," + " indicating a server-rendered coupled user interface." + ) + md.append( + "We recommend modernizing the presentation layer to align with" + " cloud-native practices:" + ) + md.append( + "* **Decoupled Frontend**: Extract presentation layout and user" + " interactions from JSP files and re-write them into a modern SPA" + " framework (e.g. **Angular** or **React**)." + ) + md.append( + "* **REST API Layer**: Migrate server-side web components (Struts" + " Action classes, JSF Backing beans, or legacy Servlets) into stateless" + " REST API endpoints (Spring Boot `@RestController` or Quarkus JAX-RS)." + ) + md.append( + "* **Stateless Transition**: Ensure JSP session variables are mapped" + " to token-based authorization (JWT) or persistent distributed session" + " stores (Redis) to allow independent microservice scaling." + ) + md.append("") + elif web_tier["servlets_count"] > 0: + md.append("### Servlet Modernization Strategy") + md.append( + f"The application contains **{web_tier['servlets_count']} legacy" + " Servlets**." + ) + md.append( + "We recommend mapping servlet request-handling logic to modern Spring" + " Boot or Quarkus REST controllers inside the target services." + ) + md.append("") + else: + md.append( + "No server-rendered front-end files (JSPs) were detected. Modernization" + " is focused exclusively on back-end APIs and services.\n" + ) + + md.append("## 6. Proposed Decomposition Candidates (Topological Analysis)") + + if len(clustering_dict) > 1: + md.append( + "Multiple resolutions were requested. Below is a high-level summary of" + " the generated candidates:\n" + ) + md.append( + "| Resolution | Services Count | Total Classes | Total Tables |" + " Excluded Utilities | Service Names |" + ) + md.append( + "|------------|----------------|---------------|--------------|--------------------|---------------|" + ) + for res_val, c_report in clustering_dict.items(): + if "error" in c_report: + md.append(f"| {res_val} | ERROR: {c_report['error']} | - | - | - | - |") + continue + summary = c_report.get("summary", {}) + services = c_report.get("proposed_services", []) + service_names = ", ".join([f"`{s['service_id']}`" for s in services]) + md.append( + f"| {res_val} | {summary.get('proposed_services_count', 0)} |" + f" {summary.get('total_classes', 0)} |" + f" {summary.get('total_tables', 0)} |" + f" {summary.get('excluded_utilities_count', 0)} | {service_names} |" + ) + md.append("\n> [!NOTE]") + md.append( + "> Detailed package/class boundaries and Mermaid diagrams are omitted" + " for multi-resolution runs to prevent report bloat. Please re-run the" + " analyzer with a single resolution parameter (e.g. `--resolution 1.0`)" + " to generate the full markdown report with diagrams." + ) + else: + md.append( + "Based on class-level dependencies and shared database table access" + " (excluding utility classes), we have partitioned the application into" + " the following candidate boundaries:\n" + ) + res_val = list(clustering_dict.keys())[0] + c_report = clustering_dict[res_val] + + if "error" in c_report: + md.append(f"Error executing clustering algorithm: {c_report['error']}\n") + else: + md.append(f"### Selected Resolution: {res_val}\n") + md.append("#### Proposed Boundaries (Mermaid Diagram)") + md.append("```mermaid") + md.append(c_report["mermaid_clusters"]) + md.append("```\n") + + for s in c_report.get("proposed_services", []): + md.append(f"##### Service: `{s['service_id']}`") + md.append("**Packages Represented:**") + for pkg in s["packages"]: + md.append(f"* `{pkg}`") + md.append("**Classes:**") + for cls in s["classes"]: + md.append(f"* `{cls}`") + md.append("**Database Tables:**") + for tbl in s["tables"]: + md.append(f"* `{tbl}`") + return "\n".join(md) + + +# ===================================================================== +# CLI ENTRY POINT +# ===================================================================== +def main(): + """Main entry point for CLI execution. + + Parses command-line arguments and routes commands to appropriate analysis + processes. + """ + parser = argparse.ArgumentParser( + description="WebLogic Monolith Migration Tool" + ) + subparsers = parser.add_subparsers(dest="command") + + # 1. 'analyze' subcommand + analyze_parser = subparsers.add_parser( + "analyze", + help=( + "Run the full static analysis and microservice decomposition" + " pipeline." + ), + ) + analyze_parser.add_argument( + "directory", help="The target project root directory to analyze." + ) + analyze_parser.add_argument( + "--base-package", + help=( + "The base package prefix (e.g. com.example) to filter internal" + " dependencies. Auto-detected if omitted." + ), + ) + analyze_parser.add_argument( + "--format", + choices=["json", "markdown"], + default="json", + help="Output format. Default is json.", + ) + analyze_parser.add_argument( + "--confirmed-utilities", + help=( + "Comma-separated list of FQCNs to exclude from clustering as" + " utilities." + ), + ) + analyze_parser.add_argument( + "--exclude-patterns", + help=( + "Comma-separated list of regex patterns (applied to FQCNs) to" + " aggressively exclude from the clustering graph (e.g." + " '.*\\.beans\\..*,.*Base.*')." + ), + ) + analyze_parser.add_argument( + "--resolution", + nargs="+", + type=float, + default=[1.0], + help=( + "List of resolution parameters for Louvain clustering (e.g. 0.5 1.0" + " 1.5)." + ), + ) + analyze_parser.add_argument( + "--presentation-package-pattern", + nargs="+", + default=DEFAULT_PRESENTATION_PATTERNS, + help=( + "Regex patterns matching UI/presentation layer classes. Uses smart" + " defaults for Struts, Spring MVC, JSF, Servlets, and Swing." + ), + ) + analyze_parser.add_argument( + "--infrastructure-package-pattern", + nargs="+", + default=DEFAULT_INFRASTRUCTURE_PATTERNS, + help=( + "Regex patterns matching cross-cutting infrastructure noise. Uses" + " smart defaults for filters, logging, and utilities." + ), + ) + analyze_parser.add_argument( + "--presentation-mode", + choices=["extract_spa", "vertical_slice", "ignore"], + default="extract_spa", + help=( + "UI/presentation classes mode: extract_spa (default), vertical_slice," + " or ignore." + ), + ) + + # Weights for coupling and cohesion heuristics + analyze_parser.add_argument( + "--weight-domain-anchor", + type=float, + default=5.0, + help=( + "Weight for strong domain anchor bonds (e.g. Session -> Entity" + " matches)." + ), + ) + analyze_parser.add_argument( + "--weight-entry-exclusive", + type=float, + default=3.0, + help=( + "Weight for exclusive entry point bonds (e.g. Action calling only 1" + " Session)." + ), + ) + analyze_parser.add_argument( + "--weight-entry-glue", + type=float, + default=0.1, + help=( + "Weight for orchestrator/glue entry points calling multiple Sessions." + ), + ) + analyze_parser.add_argument( + "--weight-db-penalty", + type=float, + default=0.01, + help=( + "Penalty weight for cross-entity foreign keys to break monolithic" + " databases." + ), + ) + analyze_parser.add_argument( + "--weight-class-table", + type=float, + default=5.0, + help="Weight for class-to-database-table state associations.", + ) + analyze_parser.add_argument( + "--utility-threshold", + type=float, + default=0.07, + help=( + "Fan-in ratio threshold (0.0 to 1.0) for automatically detecting" + " shared utility candidates." + ), + ) + + analyze_parser.add_argument( + "--output", help="Write report to file instead of stdout." + ) + + args = parser.parse_args() + + if args.command == "analyze": + confirmed_utils = None + if args.confirmed_utilities: + confirmed_utils = [x.strip() for x in args.confirmed_utilities.split(",")] + + exclude_patterns = ( + args.exclude_patterns.split(",") if args.exclude_patterns else None + ) + + weight_kwargs = { + "weight_domain_anchor": args.weight_domain_anchor, + "weight_entry_exclusive": args.weight_entry_exclusive, + "weight_entry_glue": args.weight_entry_glue, + "weight_db_penalty": args.weight_db_penalty, + "weight_class_table": args.weight_class_table, + "utility_threshold": args.utility_threshold, + } + + report = run_full_analysis( + args.directory, + args.base_package, + confirmed_utils, + exclude_patterns, + args.resolution, + args.presentation_package_pattern, + args.infrastructure_package_pattern, + args.presentation_mode, + weight_kwargs, + ) + + if args.format == "markdown": + output_content = format_markdown_report(report) + else: + output_content = json.dumps(report, indent=2) + + if args.output: + with open(args.output, "w") as f: + f.write(output_content) + print(f"Report written to {args.output}") + else: + print(output_content) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/clusterer.py b/skills/cloud/google-cloud-weblogic-migration/scripts/clusterer.py new file mode 100644 index 0000000000..7291ba7507 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/clusterer.py @@ -0,0 +1,644 @@ +"""Graph-based mathematical community detection engine for partitioning WebLogic monoliths into microservices. + +This module constructs a class-to-class and class-to-table dependency graph, +assigns weights based on domain cohesion and coupling penalty heuristics, and +runs the Louvain community detection algorithm (via NetworkX) to find clean +service boundaries. + +It incorporates specific architectural guidelines: +1. "God Glue" (Shared Utility) Detection & Universal Infrastructure Pruning: + Identifies framework base classes, horizontal helpers, or data objects with + high fan-in (in-degree), as well as cross-cutting infrastructure noise + (filters, listeners, logging). If not excluded, these bridge-like classes + will cause the algorithm to fuse completely distinct domains into a single + monolith. The module automatically computes degree thresholds and applies + smart default regexes to flag utility candidates. +2. Presentation Tier Extraction (SPA vs. Vertical Slice): + Supports isolating server-side UI controllers, servlets, and frames (Struts, + Spring MVC, JSF, Swing) into a unified `ui_spa_and_api_gateway` service + (`extract_spa` mode), or grouping them directly into their underlying backend + business domains (`vertical_slice` mode). +3. Coupling & Cohesion Edge Weighting: + - Domain Anchors (Session EJBs -> Entity EJBs of similar naming concepts): + 5.0 (keep core domain transactional classes together) + - Database Tables (bound strongly to querying classes): 5.0 (keep tables + grouped with accessing code) + - Exclusive Entry Points (Actions importing exactly 1 Session): 3.0 (merge + vertical UI controllers with backend) + - Orchestrator/Glue Entry Points (Actions calling multiple Sessions): 0.1 + (penalize to prevent fusing domains) + - Cross-Entity DB Penalty (Entity EJBs calling other entities or tables): + 0.01 (sever legacy monolithic joins) +""" + +import collections +import os +import re + +import networkx as nx +from networkx.algorithms import community + +Counter = collections.Counter +louvain_communities = community.louvain_communities + + +# ===================================================================== +# UTILITY DETECTION AND BASE GRAPH CONSTRUCTION +# ===================================================================== + + +def rollup_class_to_package(class_name): + """Rolls up a Fully Qualified Class Name (FQCN) to its package namespace. + + Identifies if the last segment starts with an uppercase letter to strip the + class name, otherwise treats it as a package/namespace itself. + + Args: + class_name (str): The FQCN (e.g. + 'com.acme.medimed.patient.PatientSessionEJB'). + + Returns: + str: The rolled-up package namespace (e.g. 'com.acme.medimed.patient'). + """ + parts = class_name.split(".") + if len(parts) > 1: + if parts[-1][0].isupper(): + return ".".join(parts[:-1]) + return ".".join(parts) + return "default" + + +def build_directed_graph(class_deps, db_data): + """Builds a directed graph of class imports and database table usages. + + Directed edges point from the dependent class to the dependency class or + database table. This graph is primarily used to analyze class degree metrics + (Fan-in/Fan-out) to discover utility classes. + + Args: + class_deps (dict): A dictionary mapping FQCN to a list of + imported/dependent FQCNs. + db_data (dict): Database mappings containing 'class_to_tables' mappings. + + Returns: + networkx.DiGraph: The directed dependency graph. + """ + gr = nx.DiGraph() + + # Add class-to-class dependency edges + for fqcn, deps in class_deps.items(): + gr.add_node(fqcn, type="class") + for dep in deps: + gr.add_node(dep, type="class") + gr.add_edge(fqcn, dep) # fqcn depends on dep + + # Add class-to-table edges + class_to_tables = db_data.get("class_to_tables", {}) + for fqcn, tables in class_to_tables.items(): + gr.add_node(fqcn, type="class") + for table in tables: + gr.add_node(table, type="table") + gr.add_edge(fqcn, table) # class queries table + + return gr + + +def identify_utility_candidates( + gr, class_to_file, utility_threshold=0.07, infrastructure_patterns=None +): + """Scans the directed dependency graph to identify high fan-in classes that act as shared utilities. + + Scans the directed dependency graph to identify high fan-in classes that act + as shared utilities, framework base classes, or data transfer objects. + Applies standard naming-pattern heuristics and structural coupling metrics. + + Args: + gr (networkx.DiGraph): The directed dependency graph of the monolith. + class_to_file (dict): Mapping from FQCN to its physical file path on disk. + utility_threshold (float): Ratio threshold (0.0 to 1.0) of total classes + to qualify as high fan-in utility. + infrastructure_patterns (list[str], optional): Custom regex patterns to + aggressively match infrastructure/utility classes. + + Returns: + list[dict]: A list of candidate utility dictionaries containing metadata, + degree counts, auto-exclusion verdicts, and classification + reasons. + """ + candidates = [] + total_classes = sum(1 for n in gr.nodes if gr.nodes[n].get("type") == "class") + if total_classes == 0: + return [] + + # Compile infrastructure pattern regexes with smart defaults + default_infra_patterns = [ + r".*\.(util|utils|common|helpers|constants|config|logging|exceptions|filters|security)\..*", + r".*(Exception|Error|Utils|Util|Constants|Helper|Factory|Locator|Filter|Listener|Log4jInit|Home|HomeFactory|Properties)$", + ] + if infrastructure_patterns: + default_infra_patterns.extend(infrastructure_patterns) + infra_regexes = [re.compile(p, re.IGNORECASE) for p in default_infra_patterns] + + # Threshold: in-degree >= utility_threshold ratio of total classes, + # minimum of 4 references + threshold = max(4, int(total_classes * utility_threshold)) + + for node in gr.nodes: + if gr.nodes[node].get("type") != "class": + continue + + in_deg = gr.in_degree(node) + out_deg = gr.out_degree(node) + + file_path = class_to_file.get(node) + is_util = False + reason = "" + + name = node.split(".")[-1] + + # Heuristic 1: Naming patterns & Custom regexes + is_infra_match = False + if infra_regexes: + is_infra_match = any(r.match(node) for r in infra_regexes) + + if is_infra_match: + is_util = True + reason = "Custom Infrastructure/Lifecycle pattern match" + elif name.endswith("Exception") or name.endswith("Error"): + is_util = True + reason = "Exception Class (by name)" + elif ( + name.endswith("Utils") + or name.endswith("Util") + or name.endswith("Constants") + or name.endswith("Helper") + or name.endswith("Factory") + or name.endswith("Locator") + or name.endswith("Filter") + or name.endswith("Log4jInit") + or name.endswith("Init") + or name.endswith("Listener") + or name.endswith("MessageProperties") + or name.endswith("Home") + or name.endswith("HomeFactory") + or name == "StartBrowser" + ): + is_util = True + reason = "Utility/Factory/Locator/Infrastructure/Filter Class (by name)" + elif ( + name.endswith("VO") + or name.endswith("DTO") + or ".value." in node + or ".dto." in node + or ".beans." in node + or ( + name.endswith("Bean") + and not name.endswith("SessionBean") + and not name.endswith("MessageDrivenBean") + ) + ): + is_util = True + reason = "Value Object / DTO / Bean (by name/package)" + elif ( + name.startswith("Base") + or name.startswith("Abstract") + or name.endswith("Base") + ): + is_util = True + reason = "Base/Abstract Class (by name)" + + # Fallback to high fan-in heuristics + if not is_util and in_deg >= threshold: + if file_path and os.path.exists(file_path): + try: + with open(file_path, "r", errors="ignore") as f: + content = f.read() + # Heuristic 2: Exception extension + if ( + "extends Exception" in content + or "extends RuntimeException" in content + or "extends Throwable" in content + ): + is_util = True + reason = "Exception Class (extends Exception)" + # Heuristic 3: Simple POJO / Value Object / DTO + # High in-degree but has no outgoing dependencies to other classes + # in our graph + elif out_deg <= 1: + is_util = True + reason = ( + "Data Transfer Object / Value Object (Low Out-Degree:" + f" {out_deg})" + ) + except OSError: + pass + + if not is_util: + reason = ( + f"High Fan-In Candidate (In-Degree: {in_deg}, Out-Degree:" + f" {out_deg})" + ) + + # Track if it's confirmed or if it hits the high fan-in threshold + if is_util or in_deg >= threshold: + candidates.append({ + "class_name": node, + "in_degree": in_deg, + "out_degree": out_deg, + "is_confirmed_utility": is_util, + "reason": reason, + }) + + return candidates + + +# ===================================================================== +# WEIGHTED GRAPH BUILDING & HEURISTIC EDGE WEIGHTING +# ===================================================================== + + +def build_undirected_weighted_graph( + class_deps, db_data, utilities_to_exclude, **weight_kwargs +): + """Builds the filtered, undirected, and weighted graph representing class and database table interactions. + + Excludes specified shared utilities and applies coupling and cohesion edge + weight heuristics to adjust the mathematical coupling of various layers (UI, + Backend Services, JPA Entities, Database Tables). + + Args: + class_deps (dict): A dictionary mapping FQCN to a list of + imported/dependent FQCNs. + db_data (dict): Database mappings containing 'class_to_tables' mappings. + utilities_to_exclude (set): A set of FQCNs representing utilities and base + classes to be filtered out. + **weight_kwargs: Optional edge weighting override parameters. + + Returns: + networkx.Graph: The undirected weighted graph prepared for Louvain + clustering. + """ + gr = nx.Graph() + + # Extract configurable weights with defaults + weight_domain_anchor = weight_kwargs.get("weight_domain_anchor", 5.0) + weight_entry_exclusive = weight_kwargs.get("weight_entry_exclusive", 3.0) + weight_entry_glue = weight_kwargs.get("weight_entry_glue", 0.1) + weight_db_penalty = weight_kwargs.get("weight_db_penalty", 0.01) + weight_class_table = weight_kwargs.get("weight_class_table", 5.0) + + # Add class-to-class edges + for fqcn, deps in class_deps.items(): + if fqcn in utilities_to_exclude: + continue + gr.add_node(fqcn, type="class") + for dep in deps: + if dep in utilities_to_exclude: + continue + gr.add_node(dep, type="class") + + # Determine edge weight based on naming conventions + # (vertical vs horizontal) + weight = 1.0 + fqcn_name = fqcn.split(".")[-1] + dep_name = dep.split(".")[-1] + + # Count how many Sessions this class imports to determine Exclusivity + session_deps = [ + d + for d in deps + if "Session" in d.split(".")[-1] and d not in utilities_to_exclude + ] + + # Heuristic: Penalize cross-entity foreign keys to force domain separation + if ("EJB" in fqcn_name and "Session" not in fqcn_name) and ( + "EJB" in dep_name and "Session" not in dep_name + ): + weight = weight_db_penalty + # Heuristic: Penalize horizontal UI coupling to prevent horizontal UI + # monoliths + elif ("Action" in fqcn_name and "Action" in dep_name) or ( + "WS" in fqcn_name and "WS" in dep_name + ): + weight = weight_db_penalty + # Heuristic: Mathematical Exclusivity for Entry Points + elif ( + "Action" in fqcn_name + or "Controller" in fqcn_name + or "WS" in fqcn_name + ) and "Session" in dep_name: + if len(session_deps) == 1: + # Exclusive Entry Point -> Bind to its backend (but weaker than DB) + weight = weight_entry_exclusive + else: + # Orchestrator / Glue -> Penalize to avoid merging domains + weight = weight_entry_glue + # Heuristic: Strongly bind Backend Business Logic (Sessions to Entities) + elif "Session" in fqcn_name and ( + "EJB" in dep_name or "Entity" in dep_name + ): + session_core = fqcn_name.replace("SessionEJB", "").replace( + "Session", "" + ) + entity_core = dep_name.replace("EJB", "").replace("Entity", "") + if ( + session_core + and entity_core + and (session_core in entity_core or entity_core in session_core) + ): + weight = weight_domain_anchor # Domain Anchor -> STRONGEST BOND + else: + weight = weight_db_penalty + + gr.add_edge(fqcn, dep, weight=weight) + + # Add class-to-table edges + class_to_tables = db_data.get("class_to_tables", {}) + for fqcn, tables in class_to_tables.items(): + if fqcn in utilities_to_exclude: + continue + gr.add_node(fqcn, type="class") + for table in tables: + gr.add_node(table, type="table") + # Tables represent state; bind highly to class + # (weight = weight_class_table) + if gr.has_edge(fqcn, table): + gr[fqcn][table]["weight"] = weight_class_table + else: + gr.add_edge(fqcn, table, weight=weight_class_table) + + return gr + + +# ===================================================================== +# LOUVAIN COMMUNITY DETECTION & BOUNDARY CLUSTERING +# ===================================================================== +def find_communities(gr, resolution=1.0): + """Executes the Louvain community detection algorithm via NetworkX. + + Executes the Louvain community detection algorithm (via NetworkX native + module) to partition the graph nodes into cohesive microservice boundaries. + Automatically assigns service names based on dominant domain naming patterns + within each cluster. + + Args: + gr (networkx.Graph): The undirected weighted graph. + resolution (float): The Louvain resolution parameter. Higher values yield + more, smaller communities. + + Returns: + list[dict]: A list of dictionaries representing the proposed services, + containing the service_id, + associated classes, rolled-up packages, and queryable database + tables. + """ + # Execute native NetworkX Louvain community detection + communities = louvain_communities( + gr, weight="weight", resolution=resolution, seed=42 + ) + + clusters = [] + for i, comm in enumerate(communities): + classes = [] + tables = [] + for node in comm: + node_type = gr.nodes[node].get("type", "unknown") + if node_type == "class": + classes.append(node) + elif node_type == "table": + tables.append(node) + + packages = sorted(list(set(rollup_class_to_package(c) for c in classes))) + + # Calculate a meaningful service name based on dominant class concepts + def get_domain_words(class_name): + simple_name = class_name.split(".")[-1] + # Strip common technical suffixes + for suffix in [ + "EJB", + "Action", + "WS", + "DAO", + "Bean", + "Service", + "Impl", + "Factory", + "Controller", + "Session", + "BaseLookupDispatchAction", + "BaseAction", + "Log4jInit", + ]: + if simple_name.endswith(suffix): + simple_name = simple_name[: -len(suffix)] + # Split camel case into words + words = [ + w for w in re.split(r"(?<=[a-z])(?=[A-Z])", simple_name) if len(w) > 2 + ] + return words + + concept_counts = Counter() + for c in classes: + for w in get_domain_words(c): + concept_counts[w] += 1 + + if concept_counts: + # Get the most common domain concept (e.g. 'Patient', 'Admin', 'Record') + dominant_concept = concept_counts.most_common(1)[0][0].lower() + service_name = f"{dominant_concept}_service_{i+1}" + else: + # Fallback to package if no concepts found + pkg_counts = Counter(rollup_class_to_package(c) for c in classes) + if pkg_counts: + dominant_pkg = pkg_counts.most_common(1)[0][0] + pkg_name = dominant_pkg.split(".")[-1] + service_name = f"{pkg_name}_service_{i+1}" + else: + service_name = f"core_service_{i+1}" + + clusters.append({ + "service_id": service_name, + "classes": sorted(classes), + "packages": packages, + "tables": sorted(tables), + }) + return clusters + + +def generate_mermaid_clusters(clusters): + """Generates a Mermaid graph TD flowchart representing the microservice boundaries. + + Renders subgraphs for each microservice containing class and table nodes. + + Args: + clusters (list[dict]): List of generated service boundary dictionaries. + + Returns: + str: A multi-line Mermaid diagram string. + """ + lines = ["graph TD"] + for cluster in clusters: + service_id = cluster["service_id"] + lines.append(f" subgraph {service_id}") + for cls in cluster["classes"]: + simple_name = cls.split(".")[-1] + node_id = cls.replace(".", "_") + lines.append(f' {node_id}["{simple_name} (Class)"]') + for table in cluster["tables"]: + lines.append(f' {table}["{table} (DB Table)"]') + lines.append(" end") + return "\n".join(lines) + + +def find_service_clusters( + imports_report, + db_report, + confirmed_utilities=None, + exclude_patterns=None, + resolution=1.0, + presentation_package_pattern=None, + infrastructure_package_pattern=None, + presentation_mode="extract_spa", + **weight_kwargs, +): + """Orchestrates microservice boundary recommendation via Louvain clustering. + + Orchestrates directed dependency evaluation, utility extraction, undirected + weighted graph construction, and Louvain community detection to output a + fully packaged microservice boundary recommendation. + + Args: + imports_report (dict): JSON report from import_analyzer containing package + class dependencies. + db_report (dict): JSON report from db_mapper containing + class-to-database-table mappings. + confirmed_utilities (list[str]): List of FQCNs explicitly confirmed as + utilities to be excluded. + exclude_patterns (list[str]): Regex strings matching FQCNs to be + aggressively excluded. + resolution (float): The Louvain parameter resolution. + presentation_package_pattern (list[str], optional): Regex patterns + matching presentation classes. + infrastructure_package_pattern (list[str], optional): Regex patterns + matching infrastructure classes. + presentation_mode (str): Mode for UI/presentation classes partitioning + (extract_spa|vertical_slice|ignore). + **weight_kwargs: Configurable edge weighting configurations. + + Returns: + dict: A dictionary mapping of results containing summary stats, utility + evaluations, + proposed service lists, and Mermaid graphs. + """ + class_deps = imports_report.get("class_dependencies", {}) + class_to_file = imports_report.get("class_to_file", {}) + + if not class_deps: + return {"error": "No class dependency data available."} + + # 1. Build directed graph to evaluate fan-in + di_graph = build_directed_graph(class_deps, db_report) + + # 2. Identify candidate utilities + utility_threshold = weight_kwargs.get("utility_threshold", 0.07) + candidates = identify_utility_candidates( + di_graph, + class_to_file, + utility_threshold=utility_threshold, + infrastructure_patterns=infrastructure_package_pattern, + ) + + # 3. Determine exclusions + # (confirmed utilities from heuristics + externally confirmed) + utilities_to_exclude = set() + for cand in candidates: + if cand["is_confirmed_utility"]: + utilities_to_exclude.add(cand["class_name"]) + + if confirmed_utilities: + for fqcn in confirmed_utilities: + utilities_to_exclude.add(fqcn) + + if exclude_patterns: + patterns = [re.compile(p) for p in exclude_patterns] + for fqcn in class_deps.keys(): + for p in patterns: + if p.match(fqcn): + utilities_to_exclude.add(fqcn) + break + + # 4. Extract Presentation Tier (UI SPA / API Gateway) classes before backend + # domain clustering + # Default covers Struts (*Action, *Form, .actions.), + # JSF (*Page, *Bean, .jsf.), Spring MVC (*Controller, .mvc.), + # desktop Swing/AWT (*Frame, *Client, .swing.) + default_prep_patterns = [ + r".*\.(web|ui|actions|controllers|views|presentation|swing|client|struts|jsf|faces|mvc)\..*", + r".*(Action|Servlet|Controller|Presenter|View|PageBean|SwingClient|Frame)$", + ] + if presentation_package_pattern: + default_prep_patterns.extend(presentation_package_pattern) + prep_regexes = [re.compile(p, re.IGNORECASE) for p in default_prep_patterns] + + ui_spa_classes = [] + backend_class_deps = {} + for fqcn, deps in class_deps.items(): + if fqcn in utilities_to_exclude: + continue + is_presentation = any(r.match(fqcn) for r in prep_regexes) + if presentation_mode == "extract_spa" and is_presentation: + ui_spa_classes.append(fqcn) + else: + backend_class_deps[fqcn] = deps + + # 5. Build weighted undirected graph for backend domain partitioning + undir_graph = build_undirected_weighted_graph( + backend_class_deps, db_report, utilities_to_exclude, **weight_kwargs + ) + + if len(undir_graph) == 0 and not ui_spa_classes: + return {"error": "The filtered dependency graph is empty."} + + # 6. Run Louvain partitioning on backend domain + clusters = [] + if len(undir_graph) > 0: + clusters = find_communities(undir_graph, resolution=resolution) + + # 7. Prepend the UI SPA & API Gateway presentation service if UI classes exist + if ui_spa_classes: + ui_spa_service = { + "service_id": "ui_spa_and_api_gateway", + "service_type": "Presentation Tier (UI SPA & API Gateway)", + "classes": sorted(ui_spa_classes), + "packages": sorted( + list(set(rollup_class_to_package(c) for c in ui_spa_classes)) + ), + "tables": [], + } + clusters.insert(0, ui_spa_service) + + return { + "summary": { + "total_classes": ( + sum( + 1 + for n in undir_graph.nodes + if undir_graph.nodes[n].get("type") == "class" + ) + + len(ui_spa_classes) + ), + "total_tables": sum( + 1 + for n in undir_graph.nodes + if undir_graph.nodes[n].get("type") == "table" + ), + "proposed_services_count": len(clusters), + "excluded_utilities_count": len(utilities_to_exclude), + }, + "utility_evaluation": { + "candidates": candidates, + "excluded_utilities": sorted(list(utilities_to_exclude)), + }, + "proposed_services": clusters, + "mermaid_clusters": generate_mermaid_clusters(clusters), + } diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/config_parser.py b/skills/cloud/google-cloud-weblogic-migration/scripts/config_parser.py new file mode 100644 index 0000000000..5be351553a --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/config_parser.py @@ -0,0 +1,332 @@ +"""XML descriptor parser for mapping Java EE and WebLogic-specific configurations in a monolith. + +This module scans a legacy WebLogic application root to discover and parse +standard Java EE and WebLogic deployment descriptors. Specifically, +it parses: +- web.xml & weblogic.xml (servlets, filters, mapping routes, session +configurations, and security constraints). +- ejb-jar.xml & weblogic-ejb-jar.xml (Enterprise JavaBeans definitions, JNDI +bindings, and security roles). + +It strips XML namespace headers dynamically and parses tag-trees into python +dictionaries, making configuration metadata readily available for microservice +boundary mapping and refactoring. +""" + +import os +import xml.etree.ElementTree as ET + +# ===================================================================== +# GENERAL XML PARSING AND UTILITY FUNCTIONS +# ===================================================================== + + +def strip_ns(tag): + """Strips the namespace prefix from an XML tag. + + Args: + tag (str): The XML element tag string (possibly containing namespace). + + Returns: + str: The raw tag name without namespace. + """ + if "}" in tag: + return tag.split("}", 1)[1] + return tag + + +def parse_xml_to_dict(element): + """Converts an XML ElementTree Element into a python dictionary. + + Recursively converts an XML ElementTree Element into a python dictionary + representation, simplifying leaf node elements into plain strings. + + Args: + element (xml.etree.ElementTree.Element): The XML element. + + Returns: + dict | str: A dictionary map representing the XML node and its children, + or a string value. + """ + res = {} + tag = strip_ns(element.tag) + children = list(element) + if children: + child_dicts = {} + for child in children: + child_tag = strip_ns(child.tag) + child_val = parse_xml_to_dict(child) + if child_tag in child_dicts: + if not isinstance(child_dicts[child_tag], list): + child_dicts[child_tag] = [child_dicts[child_tag]] + child_dicts[child_tag].append(child_val) + else: + child_dicts[child_tag] = child_val + res = child_dicts + else: + res = element.text.strip() if element.text else "" + return res + + +# ===================================================================== +# SPECIFIC DEPLOYMENT DESCRIPTOR PARSERS +# ===================================================================== +def parse_web_xml(path): + """Parses a Java EE standard web.xml file. + + Parses a Java EE standard web.xml file to extract servlets, filters, resource + references, and security constraints (URL patterns & authorization roles). + + Args: + path (str): File path to web.xml. + + Returns: + dict: A structured summary of servlets, filters, resource refs, and + security parameters. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = { + "servlets": [], + "filters": [], + "security_constraints": [], + "resource_refs": [], + } + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + for s in get_as_list(data, "servlet"): + if isinstance(s, dict): + summary["servlets"].append({ + "name": s.get("servlet-name", ""), + "class": s.get("servlet-class", ""), + }) + for f in get_as_list(data, "filter"): + if isinstance(f, dict): + summary["filters"].append({ + "name": f.get("filter-name", ""), + "class": f.get("filter-class", ""), + }) + for r in get_as_list(data, "resource-ref"): + if isinstance(r, dict): + summary["resource_refs"].append({ + "name": r.get("res-ref-name", ""), + "type": r.get("res-type", ""), + "auth": r.get("res-auth", ""), + }) + for c in get_as_list(data, "security-constraint"): + if isinstance(c, dict): + web_resource = c.get("web-resource-collection", {}) + auth_constraint = c.get("auth-constraint", {}) + summary["security_constraints"].append({ + "url_patterns": ( + get_as_list(web_resource, "url-pattern") + if isinstance(web_resource, dict) + else [] + ), + "roles": ( + get_as_list(auth_constraint, "role-name") + if isinstance(auth_constraint, dict) + else [] + ), + }) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse web.xml: {str(e)}"} + + +def parse_weblogic_xml(path): + """Parses a weblogic.xml file. + + Parses a weblogic.xml file to extract security role assignments + and mapping descriptions for EJB JNDI names. + + Args: + path (str): File path to weblogic.xml. + + Returns: + dict: A structured summary of assignments and descriptions. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = { + "security_role_assignments": [], + "ejb_reference_descriptions": [], + } + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + for ra in get_as_list(data, "security-role-assignment"): + if isinstance(ra, dict): + summary["security_role_assignments"].append({ + "role_name": ra.get("role-name", ""), + "principals": get_as_list(ra, "principal-name"), + }) + for ejb_ref in get_as_list(data, "ejb-reference-description"): + if isinstance(ejb_ref, dict): + summary["ejb_reference_descriptions"].append({ + "ejb_ref_name": ejb_ref.get("ejb-ref-name", ""), + "jndi_name": ejb_ref.get("jndi-name", ""), + }) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse weblogic.xml: {str(e)}"} + + +def parse_ejb_jar_xml(path): + """Parses a Java EE standard ejb-jar.xml file. + + Parses a Java EE standard ejb-jar.xml descriptor to extract stateless, + stateful, and message-driven enterprise beans (EJB) configuration details. + + Args: + path (str): File path to ejb-jar.xml. + + Returns: + dict: A dictionary list containing parsed enterprise beans. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = {"enterprise_beans": []} + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + ejb_container = data.get("enterprise-beans", {}) + if isinstance(ejb_container, dict): + for sb in get_as_list(ejb_container, "session"): + if isinstance(sb, dict): + summary["enterprise_beans"].append({ + "name": sb.get("ejb-name", ""), + "type": "Session (" + sb.get("session-type", "Stateless") + ")", + "class": sb.get("ejb-class", ""), + "business_local": sb.get("local", ""), + "business_remote": sb.get("remote", ""), + }) + for mdb in get_as_list(ejb_container, "message-driven"): + if isinstance(mdb, dict): + summary["enterprise_beans"].append({ + "name": mdb.get("ejb-name", ""), + "type": "Message-Driven", + "class": mdb.get("ejb-class", ""), + "destination_type": mdb.get( + "messaging-type", "javax.jms.MessageListener" + ), + }) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse ejb-jar.xml: {str(e)}"} + + +def parse_weblogic_ejb_jar_xml(path): + """Parses a weblogic-ejb-jar.xml file. + + Parses a weblogic-ejb-jar.xml descriptor to map EJBs to their + actual JNDI names. + + Args: + path (str): File path to weblogic-ejb-jar.xml. + + Returns: + dict: A mapping of enterprise beans to their JNDI descriptors. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = {"weblogic_enterprise_beans": []} + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + wles = get_as_list(data, "weblogic-enterprise-bean") + for wle in wles: + if isinstance(wle, dict): + summary["weblogic_enterprise_beans"].append({ + "name": wle.get("ejb-name", ""), + "jndi_name": wle.get("jndi-name", ""), + "local_jndi_name": wle.get("local-jndi-name", ""), + }) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse weblogic-ejb-jar.xml: {str(e)}"} + + +# ===================================================================== +# CONFIGURATION SCANNING AND ORCHESTRATION +# ===================================================================== +def find_files(target_dir, filenames): + """Helper to recursively find specific configuration file names in a repository. + + Args: + target_dir (str): The directory to search. + filenames (list[str]): The names of files to match (case-insensitive). + + Returns: + list[str]: Absolute paths to matching files. + """ + found = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.lower() in [name.lower() for name in filenames]: + found.append(os.path.join(root, f)) + return found + + +def parse_configurations(target_dir): + """Orchestrates the parsing of standard and descriptors across a repository directory. + + Args: + target_dir (str): The target repository directory path. + + Returns: + dict: A dictionary containing lists of parsed descriptors mapped to their + relative file paths. + """ + report = { + "web_xml": [], + "weblogic_xml": [], + "ejb_jar_xml": [], + "weblogic_ejb_jar_xml": [], + } + + for path in find_files(target_dir, ["web.xml"]): + report["web_xml"].append( + {"path": os.path.relpath(path, target_dir), "data": parse_web_xml(path)} + ) + + for path in find_files(target_dir, ["weblogic.xml"]): + report["weblogic_xml"].append({ + "path": os.path.relpath(path, target_dir), + "data": parse_weblogic_xml(path), + }) + + for path in find_files(target_dir, ["ejb-jar.xml"]): + report["ejb_jar_xml"].append({ + "path": os.path.relpath(path, target_dir), + "data": parse_ejb_jar_xml(path), + }) + + for path in find_files(target_dir, ["weblogic-ejb-jar.xml"]): + report["weblogic_ejb_jar_xml"].append({ + "path": os.path.relpath(path, target_dir), + "data": parse_weblogic_ejb_jar_xml(path), + }) + + return report diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/db_mapper.py b/skills/cloud/google-cloud-weblogic-migration/scripts/db_mapper.py new file mode 100644 index 0000000000..86e3f1bdd2 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/db_mapper.py @@ -0,0 +1,348 @@ +"""Database usage mapper for extracting table references from JPA/ORM descriptors and legacy EJB/JDBC code. + +This module parses persistence and ORM mappings to map classes to database +tables: +- Parses WebLogic CMP descriptors (`weblogic-cmp-rdbms-jar.xml`) to extract +EJB-to-table bindings. +- Inspects Java source files for JPA annotations (`@Table`, `@SecondaryTable`) +and parses SQL statement regexes. + +This class-to-table lookup is used by the clustering engine to calculate +database access profiles and ensure cohesive database boundaries are mapped to +individual microservices. +""" + +import os +import re +import xml.etree.ElementTree as ET +import lexical_normalizer + +# ===================================================================== +# GENERAL XML UTILITIES +# ===================================================================== + + +def strip_ns(tag): + """Strips XML namespaces from elements. + + Args: + tag (str): The raw tag name. + + Returns: + str: The tag name without namespace. + """ + if "}" in tag: + return tag.split("}", 1)[1] + return tag + + +def parse_xml_to_dict(element): + """Recursively converts an XML tree element to a nested dictionary representation. + + Args: + element (xml.etree.ElementTree.Element): The XML element. + + Returns: + dict | str: Parsed dictionary or text value. + """ + res = {} + tag = strip_ns(element.tag) + children = list(element) + if children: + child_dicts = {} + for child in children: + child_tag = strip_ns(child.tag) + child_val = parse_xml_to_dict(child) + if child_tag in child_dicts: + if not isinstance(child_dicts[child_tag], list): + child_dicts[child_tag] = [child_dicts[child_tag]] + child_dicts[child_tag].append(child_val) + else: + child_dicts[child_tag] = child_val + res = child_dicts + else: + res = element.text.strip() if element.text else "" + return res + + +# ===================================================================== +# ORM DESCRIPTOR AND JAVA SOURCE PARSERS +# ===================================================================== +def parse_weblogic_cmp_maps(path): + """Parses weblogic-cmp-rdbms-jar.xml files to map entity beans to database tables. + + Args: + path (str): File path to weblogic-cmp-rdbms-jar.xml. + + Returns: + dict: Mapping of EJB names to their queryable database tables. + """ + mappings = {} + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + beans = get_as_list(data, "weblogic-rdbms-bean") + for bean in beans: + if isinstance(bean, dict): + ejb_name = bean.get("ejb-name") + table_map = bean.get("table-map") + tables = [] + if isinstance(table_map, dict): + tables.append(table_map.get("table-name")) + elif isinstance(table_map, list): + for tm in table_map: + if isinstance(tm, dict): + tables.append(tm.get("table-name")) + if ejb_name and tables: + mappings[ejb_name] = [t for t in tables if t] + except (ET.ParseError, OSError): + pass + return mappings + + +def extract_db_info_from_java(file_path): + """Extracts database table references from a Java source file. + + Statically analyzes a Java source file using lexical normalization to identify + package, class name, entity status, and database table references in JPA + annotations and SQL queries. + Safely handles inline comments, multi-line attributes, string concatenations, + and comma-separated FROM clauses. + + Args: + file_path (str): File path to the Java source file. + + Returns: + tuple[str, str, bool, list[str]]: package, class name, is_entity boolean, + and lists of referenced tables. + """ + tables = set() + is_entity = False + class_name = ( + os.path.basename(file_path).replace(".java", "").replace(".ejb", "") + ) + package = "" + + try: + tokens = lexical_normalizer.tokenize_java_file(file_path) + package = lexical_normalizer.get_package_name(file_path) or "" + code_clean = tokens["code_text_with_strings"] + code_no_str = tokens["code_text_no_strings"] + + # 1. Check entity status and JPA table annotations on comment-stripped, + # whitespace-collapsed code + if re.search( + r"@(?:Entity|Table|SecondaryTable)\b|\bGenericEntityBean\b", code_no_str + ): + is_entity = True + + for match in re.finditer( + r'@(?:SecondaryTable|Table|JoinTable)\s*\(\s*(?:[^)]*?\bname\s*=\s*)?"([a-zA-Z0-9_]+)"', + code_clean, + ): + tables.add(match.group(1).upper()) + is_entity = True + + if is_entity and not tables and re.search(r"@Entity\b", code_no_str): + tables.add(class_name.upper()) + + for javadoc in tokens.get("javadocs", []): + for match in re.finditer(r"table-name\s*=\s*([a-zA-Z0-9_]+)", javadoc): + tables.add(match.group(1).upper()) + is_entity = True + + for match in re.finditer(r"table-name\s*=\s*([a-zA-Z0-9_]+)", code_clean): + tables.add(match.group(1).upper()) + is_entity = True + + # 2. Extract tables from SQL queries in string literals (concatenations + # already merged by normalizer) + sql_keywords = { + "SELECT", + "INSERT", + "UPDATE", + "DELETE", + "WHERE", + "AND", + "OR", + "JOIN", + "LEFT", + "RIGHT", + "INNER", + "OUTER", + "CROSS", + "FULL", + "NATURAL", + "ON", + "USING", + "GROUP", + "ORDER", + "BY", + "SET", + "AS", + "UNION", + "HAVING", + "LIMIT", + "OFFSET", + "VALS", + "VALUES", + "FROM", + "INTO", + "CREATE", + "DROP", + "ALTER", + "TABLE", + "INDEX", + "VIEW", + } + + for sql_str in tokens["string_literals"]: + if len(sql_str) < 6: + continue + # Match FROM, JOIN, INTO, UPDATE followed by table names/aliases up to + # next SQL keyword or subquery '(' + # Note: 'AS' is intentionally excluded from lookahead so comma-separated + # lists with aliases (e.g. FROM tableA AS a, tableB AS b) are captured in + # full. + for clause_match in re.finditer( + r"(?i)\b(?:FROM|JOIN|INTO|UPDATE)\s+([a-zA-Z0-9_\.\s,]*[a-zA-Z0-9_])\s*(?=\b(?:WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|CROSS|FULL|NATURAL|ON|USING|GROUP|ORDER|BY|SET|SELECT|UNION|HAVING|LIMIT|OFFSET|VALS|VALUES)\b|\(|;|$)", + sql_str, + ): + clause_text = clause_match.group(1) + # Split comma-separated table lists (e.g. "PATIENT p, ADDRESS a" + # or "PATIENT AS p, ADDRESS AS a") + for table_expr in clause_text.split(","): + parts = table_expr.strip().split() + if parts: + table_name = parts[0].upper() + if ( + table_name not in sql_keywords + and not table_name.startswith("JAVA:") + and len(table_name) > 1 + ): + tables.add(table_name) + except OSError: + pass + + sql_keywords = { + "SELECT", + "INSERT", + "UPDATE", + "DELETE", + "WHERE", + "AND", + "OR", + "JOIN", + "LEFT", + "RIGHT", + "INNER", + "OUTER", + "CROSS", + "FULL", + "NATURAL", + "ON", + "USING", + "GROUP", + "ORDER", + "BY", + "SET", + "AS", + "UNION", + "HAVING", + "LIMIT", + "OFFSET", + "VALS", + "VALUES", + "FROM", + "INTO", + "CREATE", + "DROP", + "ALTER", + "TABLE", + "INDEX", + "VIEW", + } + cleaned_tables = [ + t for t in tables if t not in sql_keywords and not t.startswith("JAVA:") + ] + return package, class_name, is_entity, cleaned_tables + + +# ===================================================================== +# RUNTIME FILE DISCOVERY AND DATABASE ORCHESTRATION +# ===================================================================== +def find_files(target_dir, filenames): + """Recursively scans the directory to find files matching given file names. + + Args: + target_dir (str): Directory path to scan. + filenames (list[str]): File names to search. + + Returns: + list[str]: Absolute paths to files found. + """ + found = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.lower() in [name.lower() for name in filenames]: + found.append(os.path.join(root, f)) + return found + + +def map_db_usage(target_dir): + """Extracts database usage mappings from a repository. + + Scans the repository to map class-to-table and table-to-class interaction + mappings, extracting relations from both XML CMP mappings and Java code. + + Args: + target_dir (str): The repository root path to scan. + + Returns: + dict: A nested mapping report representing classes and tables. + """ + class_to_tables = {} + table_to_classes = {} + + ejb_to_tables = {} + cmp_maps = find_files(target_dir, ["weblogic-cmp-rdbms-jar.xml"]) + for cmap in cmp_maps: + maps = parse_weblogic_cmp_maps(cmap) + ejb_to_tables.update(maps) + + java_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.endswith(".java") or f.endswith(".ejb"): + java_files.append(os.path.join(root, f)) + + for path in java_files: + package, class_name, is_entity, tables = extract_db_info_from_java(path) + if class_name in ejb_to_tables: + tables = list(set(tables + ejb_to_tables[class_name])) + for ejb_name, cmp_tables in ejb_to_tables.items(): + if class_name.startswith(ejb_name): + tables = list(set(tables + cmp_tables)) + if tables: + fq_class_name = f"{package}.{class_name}" if package else class_name + class_to_tables[fq_class_name] = sorted(tables) + for t in tables: + if t not in table_to_classes: + table_to_classes[t] = [] + table_to_classes[t].append(fq_class_name) + + for t in table_to_classes: + table_to_classes[t] = sorted(table_to_classes[t]) + + return { + "cmp_ejb_mappings": ejb_to_tables, + "class_to_tables": class_to_tables, + "table_to_classes": table_to_classes, + } diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/domain_parser.py b/skills/cloud/google-cloud-weblogic-migration/scripts/domain_parser.py new file mode 100644 index 0000000000..a2825eb67e --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/domain_parser.py @@ -0,0 +1,294 @@ +"""WebLogic domain configuration parser for extracting database connection pools, JMS settings, and WLST automation scripts. + +This module parses WebLogic domain configurations (like `config.xml` and its +sub-descriptors) to identify infrastructure and middleware setups: +- JDBC connection configurations (database drivers, connection pool sizes, +database URLs, JNDI names). +- JMS System resources (queues, topics, connection factories). +- WLST scripting setups (discovering `.py` scripts containing automation +routines). + +It provides structured dictionaries mapped to the original WebLogic configs to +assist in provisioning matching GCP services like Cloud SQL, Cloud Memorystore, +or Cloud Pub/Sub. +""" + +import os +import xml.etree.ElementTree as ET + +# ===================================================================== +# GENERAL XML UTILITIES +# ===================================================================== + + +def strip_ns(tag): + """Strips namespace tags from XML elements. + + Args: + tag (str): The raw XML tag name. + + Returns: + str: The tag name without namespace. + """ + if "}" in tag: + return tag.split("}", 1)[1] + return tag + + +def parse_xml_to_dict(element): + """Converts XML elements to simplified nested dictionaries. + + Args: + element (xml.etree.ElementTree.Element): The XML element to parse. + + Returns: + dict | str: Parsed configuration dictionary or element value. + """ + res = {} + tag = strip_ns(element.tag) + children = list(element) + if children: + child_dicts = {} + for child in children: + child_tag = strip_ns(child.tag) + child_val = parse_xml_to_dict(child) + if child_tag in child_dicts: + if not isinstance(child_dicts[child_tag], list): + child_dicts[child_tag] = [child_dicts[child_tag]] + child_dicts[child_tag].append(child_val) + else: + child_dicts[child_tag] = child_val + res = child_dicts + else: + res = element.text.strip() if element.text else "" + return res + + +# ===================================================================== +# WEBLOGIC DOMAIN RESOURCE DESCRIPTOR PARSERS +# ===================================================================== +def parse_jdbc_descriptor(path): + """Parses a WebLogic JDBC system resource descriptor XML file to extract JNDI name and URL details. + + Args: + path (str): File path to the JDBC descriptor XML. + + Returns: + dict: A dictionary of parsed JDBC configurations. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = {} + if isinstance(data, dict): + ds = data.get("jdbc-data-source-params", {}) + if isinstance(ds, dict): + summary["jndi_names"] = ds.get("jndi-name", "") + driver = data.get("jdbc-driver-params", {}) + if isinstance(driver, dict): + summary["url"] = driver.get("url", "") + summary["driver_name"] = driver.get("driver-name", "") + props = driver.get("properties", {}) + if isinstance(props, dict): + prop_list = props.get("property", []) + if not isinstance(prop_list, list): + prop_list = [prop_list] + for p in prop_list: + if isinstance(p, dict) and p.get("name") == "user": + summary["username"] = p.get("value", "") + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse JDBC descriptor {path}: {str(e)}"} + + +def parse_jms_descriptor(path): + """Parses a WebLogic JMS system resource descriptor XML file to extract queues, topics, and connection factories. + + Args: + path (str): File path to the JMS descriptor XML. + + Returns: + dict: A dictionary listing queues, topics, and connection factories. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = {"queues": [], "topics": [], "connection_factories": []} + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + for q in get_as_list(data, "queue"): + if isinstance(q, dict): + summary["queues"].append( + {"name": q.get("name"), "jndi_name": q.get("jndi-name")} + ) + for t in get_as_list(data, "topic"): + if isinstance(t, dict): + summary["topics"].append( + {"name": t.get("name"), "jndi_name": t.get("jndi-name")} + ) + for cf in get_as_list(data, "connection-factory"): + if isinstance(cf, dict): + summary["connection_factories"].append( + {"name": cf.get("name"), "jndi_name": cf.get("jndi-name")} + ) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse JMS descriptor {path}: {str(e)}"} + + +def resolve_descriptor_path(config_path, desc_file): + """Resolves a descriptor file path relative to config.xml directory or domain root. + + Handles cases where descriptor-file-name includes or excludes the 'config/' + prefix. + + Args: + config_path (str): File path to config.xml. + desc_file (str): Descriptor file name. + + Returns: + str: Resolved file path to the descriptor or empty string if not found. + """ + if not desc_file: + return "" + candidates = [ + os.path.join(os.path.dirname(config_path), desc_file), + os.path.join(os.path.dirname(os.path.dirname(config_path)), desc_file), + ] + if desc_file.startswith("config/"): + candidates.append(os.path.join(os.path.dirname(config_path), desc_file[7:])) + for cand in candidates: + if os.path.exists(cand): + return cand + return candidates[0] + + +def parse_config_xml(path): + """Parses WebLogic domain config.xml to extract JDBC, JMS, and deployment configurations. + + Parses the domain configuration config.xml file to identify defined JDBC/JMS + resources and deployments, automatically loading referenced system resource + descriptors. + + Args: + path (str): File path to config.xml. + + Returns: + dict: Summary of defined domain configurations. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + data = parse_xml_to_dict(root) + summary = { + "domain_name": root.attrib.get("name", "Unknown"), + "jdbc_resources": [], + "jms_resources": [], + "deployments": [], + } + + def get_as_list(d, key): + val = d.get(key, []) + return val if isinstance(val, list) else [val] + + if isinstance(data, dict): + for jdbc in get_as_list(data, "jdbc-system-resource"): + if isinstance(jdbc, dict): + name = jdbc.get("name") + desc_file = jdbc.get("descriptor-file-name") + desc_path = resolve_descriptor_path(path, desc_file) + details = {} + if desc_path and os.path.exists(desc_path): + details = parse_jdbc_descriptor(desc_path) + else: + details = {"warning": f"Descriptor file {desc_file} not found"} + summary["jdbc_resources"].append( + {"name": name, "descriptor_file": desc_file, "details": details} + ) + for jms in get_as_list(data, "jms-system-resource"): + if isinstance(jms, dict): + name = jms.get("name") + desc_file = jms.get("descriptor-file-name") + desc_path = resolve_descriptor_path(path, desc_file) + details = {} + if desc_path and os.path.exists(desc_path): + details = parse_jms_descriptor(desc_path) + else: + details = {"warning": f"Descriptor file {desc_file} not found"} + summary["jms_resources"].append( + {"name": name, "descriptor_file": desc_file, "details": details} + ) + for app in get_as_list(data, "app-deployment"): + if isinstance(app, dict): + summary["deployments"].append({ + "name": app.get("name"), + "source_path": app.get("source-path"), + "targets": app.get("target"), + }) + return summary + except (ET.ParseError, OSError) as e: + return {"error": f"Failed to parse config.xml: {str(e)}"} + + +# ===================================================================== +# AUTOMATION SCRIPT SCANNERS AND DOMAIN ORCHESTRATION +# ===================================================================== +def scan_for_wlst(target_dir): + """Scans the repository recursively to locate Python WLST administration scripts. + + Args: + target_dir (str): The repository root. + + Returns: + list[str]: Relative paths of identified WLST scripts. + """ + wlst_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.endswith(".py"): + path = os.path.join(root, f) + try: + with open(path, "r", errors="ignore") as file: + content = file.read() + if ( + "connect(" in content + or "cmo." in content + or "wlst" in content.lower() + ): + wlst_files.append(os.path.relpath(path, target_dir)) + except OSError: + pass + return wlst_files + + +def parse_domain_config(target_dir): + """Scans the directory for WebLogic config/config.xml configurations and maps WLST scripts. + + Args: + target_dir (str): Target repository directory path. + + Returns: + dict: A compiled report mapping domains and WLST scripts. + """ + report = {"domains": [], "wlst_scripts": []} + + config_files = [] + for root, dirs, files in os.walk(target_dir): + if "config.xml" in files: + if os.path.basename(root) == "config": + config_files.append(os.path.join(root, "config.xml")) + + for config in config_files: + report["domains"].append({ + "path": os.path.relpath(config, target_dir), + "analysis": parse_config_xml(config), + }) + + report["wlst_scripts"] = scan_for_wlst(target_dir) + return report diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/generate_temp_pom.py b/skills/cloud/google-cloud-weblogic-migration/scripts/generate_temp_pom.py new file mode 100644 index 0000000000..6815881214 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/generate_temp_pom.py @@ -0,0 +1,196 @@ +"""Dynamic Maven POM builder for generating temporary pom.xml files to support OpenRewrite refactoring. + +This module scans a target legacy directory recursively to calculate Java source +roots by mapping packages. It then writes a temporary, minimal `pom.xml` +pointing to those roots. +This allows modern AST/refactoring toolkits (like OpenRewrite) to parse and +compile the codebase, even if the monolith originally used Ant or lacked +structured dependency files. +""" + +import os +import sys +import xml.etree.ElementTree as ET +import build_parser +import jar_analyzer +import lexical_normalizer + +# ===================================================================== +# SOURCE DIRECTORY RESOLUTION UTILITIES +# ===================================================================== + + +def extract_package(file_path): + """Extracts package declaration from a Java source file using lexical normalization. + + Immune to commented-out package statements or multi-line comments. + + Args: + file_path (str): File path to the Java source file. + + Returns: + str | None: Package name or None. + """ + return lexical_normalizer.get_package_name(file_path) + + +def detect_source_roots(target_dir): + """Detects Java source roots by comparing file paths with package declarations. + + Args: + target_dir (str): Monolith repository folder to scan. + + Returns: + list[str]: Relative paths of identified source roots. + """ + java_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.endswith(".java"): + java_files.append(os.path.join(root, f)) + + source_roots = set() + + for path in java_files: + pkg = extract_package(path) + if pkg: + # Calculate package depth (number of dots + 1) + depth = len(pkg.split(".")) + + # Go up 'depth' times from the file's parent directory + current = os.path.dirname(path) + for _ in range(depth): + current = os.path.dirname(current) + + source_roots.add(os.path.relpath(current, target_dir)) + else: + # No package declaration, class is in default package. + # Source root is the parent directory of the file. + source_roots.add(os.path.relpath(os.path.dirname(path), target_dir)) + + return list(source_roots) if source_roots else ["."] + + +# ===================================================================== +# POM GENERATION AND WRITE OPERATIONS +# ===================================================================== +def generate_pom(target_dir, source_roots): + """Generates a minimal pom.xml inside target_dir pointing to the best source root. + + Injects local JARs and existing Maven dependencies to support OpenRewrite AST + parsing. + + Args: + target_dir (str): Directory where pom.xml will be written. + source_roots (list[str]): List of detected source roots. + """ + src_dir = "src" + if source_roots: + # Prefer roots containing 'src' + src_roots = [r for r in source_roots if "src" in r] + if src_roots: + src_dir = src_roots[0] + else: + src_dir = source_roots[0] + + # Find existing dependencies and Java version + java_version = "1.8" + maven_deps = [] + + poms = build_parser.find_files(target_dir, ["pom.xml"]) + for pom in poms: + # Avoid reading the pom we are about to overwrite if it's already there + if os.path.abspath(pom) == os.path.abspath( + os.path.join(target_dir, "pom.xml") + ): + continue + try: + tree = ET.parse(pom) + root = tree.getroot() + deps = build_parser.parse_pom_dependencies(root) + maven_deps.extend(deps) + props = build_parser.parse_pom_properties(root) + jv = build_parser.get_java_version(root, props) + if jv != "Unknown": + java_version = jv + except (ET.ParseError, OSError): + pass + + # Find local JARs + jars_report = jar_analyzer.analyze_local_jars(target_dir) + all_jars = ( + jars_report.get("weblogic_specific", []) + + jars_report.get("java_ee_legacy", []) + + jars_report.get("others", []) + ) + + deps_xml = "" + if all_jars or maven_deps: + deps_xml += " \n" + + # Inject Maven dependencies + for dep in maven_deps: + g = dep.get("groupId", "unknown") + a = dep.get("artifactId", "unknown") + v = dep.get("version", "1.0") + deps_xml += f""" + {g} + {a} + {v} + \n""" + + # Inject local JARs as system scope + for idx, jar_meta in enumerate(all_jars): + jar_path = jar_meta["relative_path"] + artifact_id = jar_meta["filename"].replace(".jar", "").replace(".", "-") + jar_path = jar_path.replace("\\", "/") + deps_xml += f""" + local.legacy + {artifact_id}_{idx} + 1.0 + system + ${{project.basedir}}/{jar_path} + \n""" + + deps_xml += " \n" + + pom_content = f""" + + 4.0.0 + temp-migration + temp-project + 1.0-SNAPSHOT + + {java_version} + {java_version} + UTF-8 + +{deps_xml} + {src_dir} + + +""" + pom_path = os.path.join(target_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write(pom_content) + print(f"Generated temporary pom.xml pointing to source directory: {src_dir}") + + +# ===================================================================== +# CLI ENTRY POINT +# ===================================================================== +def main(): + """CLI main entry point.""" + if len(sys.argv) < 2: + print("Usage: generate_temp_pom.py ") + sys.exit(1) + + target_dir = sys.argv[1] + roots = detect_source_roots(target_dir) + generate_pom(target_dir, roots) + + +if __name__ == "__main__": + main() diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/import_analyzer.py b/skills/cloud/google-cloud-weblogic-migration/scripts/import_analyzer.py new file mode 100644 index 0000000000..c8a4ca9912 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/import_analyzer.py @@ -0,0 +1,254 @@ +"""Java import statement parser for mapping class-to-class dependency connections in a monolith. + +This module statically processes Java files recursively to construct a directed +dependency graph. +It extracts: +- Package namespaces and class names. +- Explicit import dependencies (including wildcard imports and static imports). +- WebLogic Javadoc annotations mapping target EJBs (e.g. `@ejbgen:local-link` or +`@target-ejb`). + +It resolves dependencies to identify internal-only coupling, filtering out +standard JDK and external libraries based on a common base package prefix to +yield a clean internal class graph. +""" + +import os +import re +import lexical_normalizer + +# ===================================================================== +# INDIVIDUAL JAVA FILE PARSING UTILITIES +# ===================================================================== + + +def get_class_name_from_path(file_path): + """Extracts the simple class name from a file path. + + Args: + file_path (str): File path of the Java source file. + + Returns: + str: The class name (e.g. 'PatientSessionEJB'). + """ + return os.path.splitext(os.path.basename(file_path))[0] + + +def extract_package_and_imports(file_path): + """Extracts package and imports from a Java file. + + Parses a Java file using lexical normalization to extract its package + declaration, import statements, and WebLogic EJBGen target links safely across + line breaks. + + Args: + file_path (str): File path to Java source file. + + Returns: + tuple[str | None, list[str]]: The package FQCN prefix, and list of + imported FQCNs. + """ + try: + tokens = lexical_normalizer.tokenize_java_file(file_path) + package = lexical_normalizer.get_package_name(file_path) + + # Extract imports from normalized code text (no string literals or comments + # to trigger false positives) + imports = re.findall( + r"\bimport\s+(?:static\s+)?([a-zA-Z0-9_\.\*]+)\s*;", + tokens["code_text_no_strings"], + ) + + # Extract WebLogic target-ejb and local-link annotations from preserved + # Javadocs + for javadoc in tokens["javadocs"]: + for match in re.finditer( + r"(?:target-ejb|@ejbgen:local-link)\s*(?:=\s*|\s+target-ejb\s*=\s*|\s+)([a-zA-Z0-9_]+)", + javadoc, + ): + imports.append(match.group(1)) + for match in re.finditer(r"target-ejb\s*=\s*([a-zA-Z0-9_]+)", javadoc): + imports.append(match.group(1)) + + return package, list(set(imports)) + except OSError: + return None, [] + + +# ===================================================================== +# BASE PACKAGE PREFIX RESOLUTION AND ORCHESTRATION +# ===================================================================== +def get_longest_common_prefix(packages): + """Determines the longest common package prefix for a list of Java packages. + + Determines the root base package namespace (e.g., 'com.medimed') of the + monolith by calculating the longest common prefix of all scanned Java + packages. + + Args: + packages (list[str]): List of packages. + + Returns: + str: The common base package prefix. + """ + if not packages: + return "" + split_packages = [p.split(".") for p in packages if p] + if not split_packages: + return "" + min_len = min(len(p) for p in split_packages) + common_parts = [] + for i in range(min_len): + part = split_packages[0][i] + if all(p[i] == part for p in split_packages): + common_parts.append(part) + else: + break + return ".".join(common_parts) + + +def analyze_imports(target_dir, base_package=None): + """Scans Java files to build a complete, resolved dependency graph. + + Builds a complete, resolved dependency graph of internal classes by scanning + package scopes, wildcard imports, static nested classes, implicit imports, and + FQCN references in code bodies. + Filters the output to include only packages under the root base_package. + + Args: + target_dir (str): Monolith repository directory. + base_package (str, optional): Package prefix. Auto-detected if omitted. + + Returns: + dict: Summary containing detected base package, class-to-class dependency + graph, and FQCN class-to-file path maps. + """ + java_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.endswith(".java") or f.endswith(".ejb"): + java_files.append(os.path.join(root, f)) + + fqcn_to_file = {} + package_to_class_names = {} + class_data = {} + all_packages = set() + + # Pass 1: Gather all FQCNs and package structures + for path in java_files: + pkg, imps = extract_package_and_imports(path) + if not pkg: + continue + + class_name = get_class_name_from_path(path) + fqcn = f"{pkg}.{class_name}" + fqcn_to_file[fqcn] = path + all_packages.add(pkg) + + if pkg not in package_to_class_names: + package_to_class_names[pkg] = [] + package_to_class_names[pkg].append(class_name) + + try: + content = lexical_normalizer.clean_java_code(path) + except OSError: + content = "" + + class_data[fqcn] = { + "package": pkg, + "class_name": class_name, + "imports": imps, + "content": content, + } + + if not base_package: + base_package = get_longest_common_prefix(list(all_packages)) + + # Pass 2: Resolve references + class_graph = {} + for fqcn in class_data.keys(): + class_graph[fqcn] = set() + + for fqcn, data in class_data.items(): + pkg = data["package"] + class_name = data["class_name"] + content = data["content"] + imports = data["imports"] + + def is_referenced(target_class_name): + pattern = re.compile(r"\b" + re.escape(target_class_name) + r"\b") + if bool(pattern.search(content)): + return True + # Also check if the code references EJBGen interfaces of this class + if target_class_name.endswith("EJB"): + base = target_class_name[:-3] + for suffix in ["", "Home", "Local", "LocalHome"]: + if bool(re.search(r"\b" + re.escape(base + suffix) + r"\b", content)): + return True + return False + + # A. Resolve imports + for imp in imports: + if imp.endswith(".*"): + # Wildcard imports + imp_package = imp[:-2] + if imp_package in package_to_class_names: + for target_c in package_to_class_names[imp_package]: + target_fqcn = f"{imp_package}.{target_c}" + if target_fqcn != fqcn and is_referenced(target_c): + class_graph[fqcn].add(target_fqcn) + else: + # Direct FQCN imports + if imp in fqcn_to_file: + class_graph[fqcn].add(imp) + else: + # In case of static imports or nested classes + resolved = False + for potential_fqcn in fqcn_to_file.keys(): + if imp.startswith(potential_fqcn + "."): + class_graph[fqcn].add(potential_fqcn) + resolved = True + break + + # EJBGen interface resolution (Foo, FooHome, FooLocal -> FooEJB) + if not resolved: + for suffix in ["", "Home", "Local", "LocalHome"]: + if imp.endswith(suffix): + base_imp = imp if suffix == "" else imp[: -len(suffix)] + ejb_fqcn = base_imp + "EJB" + if ejb_fqcn in fqcn_to_file: + class_graph[fqcn].add(ejb_fqcn) + break + + # B. Resolve same-package classes (implicit import) + if pkg in package_to_class_names: + for target_c in package_to_class_names[pkg]: + if target_c != class_name: + target_fqcn = f"{pkg}.{target_c}" + if is_referenced(target_c): + class_graph[fqcn].add(target_fqcn) + + # C. Resolve fully qualified name references in code body + for target_fqcn in fqcn_to_file.keys(): + if target_fqcn != fqcn and target_fqcn in content: + class_graph[fqcn].add(target_fqcn) + + # Filter by base package + filtered_graph = {} + filtered_class_to_file = {} + for fqcn, deps in class_graph.items(): + if base_package and not fqcn.startswith(base_package): + continue + filtered_class_to_file[fqcn] = fqcn_to_file[fqcn] + filtered_deps = set() + for dep in deps: + if base_package and not dep.startswith(base_package): + continue + filtered_deps.add(dep) + filtered_graph[fqcn] = sorted(list(filtered_deps)) + + return { + "detected_base_package": base_package, + "class_dependencies": filtered_graph, + "class_to_file": filtered_class_to_file, + } diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/jar_analyzer.py b/skills/cloud/google-cloud-weblogic-migration/scripts/jar_analyzer.py new file mode 100644 index 0000000000..1900d39668 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/jar_analyzer.py @@ -0,0 +1,186 @@ +"""Third-party dependency scanner for extracting metadata and versions from local checked-in JAR files. + +This module inspects static `.jar` binary files stored inside the monolith +folder. +It extracts: +- Attributable manifest properties (`META-INF/MANIFEST.MF`) resolving wrapped +line breaks. +- Specification titles, vendor details, bundle names, and version fields. + +It runs categorization checks to tag jars as WebLogic-specific helpers or +standard legacy Java EE modules, facilitating their replacement with modern +Maven/Gradle dependencies. +""" + +import os +import zipfile + +# ===================================================================== +# JAR ARCHIVE MANIFEST PARSERS +# ===================================================================== + + +def parse_manifest(manifest_content): + """Parses manifest content lines into key-value property pairs, accounting for line wraps. + + Args: + manifest_content (str): The raw manifest.mf content text. + + Returns: + dict: A dictionary of manifest attributes and their values. + """ + properties = {} + current_key = None + current_val = "" + for line in manifest_content.splitlines(): + if not line: + continue + if line.startswith(" "): + current_val += line[1:] + else: + if current_key: + properties[current_key] = current_val.strip() + if ":" in line: + current_key, current_val = line.split(":", 1) + current_key = current_key.strip() + else: + current_key = None + if current_key: + properties[current_key] = current_val.strip() + return properties + + +def analyze_jar(path): + """Opens a zip-compressed JAR archive and extracts vendor, version, and package title from its META-INF/MANIFEST.MF. + + Args: + path (str): File path to JAR archive. + + Returns: + dict: A dictionary containing implementation details, versions, or errors. + """ + try: + with zipfile.ZipFile(path, "r") as jar: + manifest_path = "META-INF/MANIFEST.MF" + if manifest_path in jar.namelist(): + with jar.open(manifest_path) as mf: + content = mf.read().decode("utf-8", errors="ignore") + props = parse_manifest(content) + return { + "filename": os.path.basename(path), + "title": ( + props.get("Implementation-Title") + or props.get("Specification-Title") + or props.get("Bundle-Name") + or "Unknown" + ), + "version": ( + props.get("Implementation-Version") + or props.get("Specification-Version") + or props.get("Bundle-Version") + or "Unknown" + ), + "vendor": ( + props.get("Implementation-Vendor") + or props.get("Specification-Vendor") + or props.get("Bundle-Vendor") + or "Unknown" + ), + "symbolic_name": props.get("Bundle-SymbolicName") or "Unknown", + } + else: + return { + "filename": os.path.basename(path), + "warning": "No MANIFEST.MF found", + } + except Exception as e: + return { + "filename": os.path.basename(path), + "error": f"Failed to read JAR: {str(e)}", + } + + +# ===================================================================== +# LIBRARY CLASSIFICATION AND DIRECTORY SCANNING +# ===================================================================== +def categorize_jar(metadata): + """Categorizes JAR libraries into WebLogic-specific, legacy Java EE standard, or others. + + Args: + metadata (dict): The parsed Implementation/Bundle attributes dictionary. + + Returns: + str: Category string ('weblogic_specific', 'java_ee_legacy', 'others', or + 'error'). + """ + if "error" in metadata: + return "error" + title = (metadata.get("title") or "").lower() + vendor = (metadata.get("vendor") or "").lower() + symbolic_name = (metadata.get("symbolic_name") or "").lower() + filename = metadata["filename"].lower() + + wls_indicators = ["weblogic", "oracle", "wlfullclient", "wlthint3client"] + legacy_indicators = [ + "javax.", + "jakarta.servlet", + "ejb", + "jms", + "transaction", + "jboss", + "glassfish", + ] + + is_wls = any( + ind in title or ind in vendor or ind in symbolic_name or ind in filename + for ind in wls_indicators + ) + is_legacy = any( + ind in title or ind in vendor or ind in symbolic_name or ind in filename + for ind in legacy_indicators + ) + + if is_wls: + return "weblogic_specific" + elif is_legacy: + return "java_ee_legacy" + else: + return "others" + + +def analyze_local_jars(target_dir): + """Recursively scans a target directory for check-in libraries (*.jar) and categorizes them. + + Args: + target_dir (str): Target directory to scan. + + Returns: + dict: Summary categorizing checked-in libraries. + """ + report = { + "weblogic_specific": [], + "java_ee_legacy": [], + "others": [], + "errors": [], + } + + jar_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if f.endswith(".jar"): + jar_files.append(os.path.join(root, f)) + + for jar_path in jar_files: + meta = analyze_jar(jar_path) + meta["relative_path"] = os.path.relpath(jar_path, target_dir) + category = categorize_jar(meta) + if category == "weblogic_specific": + report["weblogic_specific"].append(meta) + elif category == "java_ee_legacy": + report["java_ee_legacy"].append(meta) + elif category == "error": + report["errors"].append(meta) + else: + report["others"].append(meta) + + return report diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/lexical_normalizer.py b/skills/cloud/google-cloud-weblogic-migration/scripts/lexical_normalizer.py new file mode 100644 index 0000000000..a8ddaf7644 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/lexical_normalizer.py @@ -0,0 +1,218 @@ +"""Lexical normalizer and token-classified scanner for Java source code analysis. + +This module implements an in-memory, dependency-free lexical tokenizer and +normalizer for legacy Java and WebLogic source files (`.java`, `.ejb`, `.jws`). +It solves regex parsing vulnerabilities caused by multi-line formatting, line +breaks, string literal traps, and comment contamination without requiring heavy +AST parsers. + +Specifically, it scans code in a single pass to separate text into isolated +buckets: +- Code text (comments removed, whitespace collapsed, safe for keyword and +annotation scanning). +- Javadoc comments (preserved intact to extract WebLogic `@ejbgen` and +`@target-ejb` annotations). +- String literals (extracted and automatically merging multi-line string +concatenations `+` for SQL query scanning). +- Ordinary comments (`//` and `/*...*/` stripped safely to prevent false +positive imports). +""" + +import os +import re + +# ===================================================================== +# LEXICAL REGEX PATTERNS AND CONSTANTS +# ===================================================================== + +# Master regex to tokenize Java code into Javadoc, block comments, line +# comments, text blocks, strings, and chars. +JAVA_LEXER_REGEX = re.compile( + r'(?P/\*\*.*?\*/)|' + r'(?P/\*.*?\*/)|' + r'(?P//[^\r\n]*)|' + r'(?P""".*?""")|' + r'(?P"(?:\\.|[^"\\])*")|' + r'(?P\'(?:\\.|[^\'\\])*\')', + re.DOTALL, +) + + +# ===================================================================== +# CORE TOKENIZER AND NORMALIZATION ENGINE +# ===================================================================== + + +def tokenize_java_content(content): + """Tokenizes raw Java source content into isolated lexical buckets (code, javadocs, strings). + + Collapses multi-line whitespace and automatically merges concatenated string + literals. + + Args: + content (str): Raw Java source file text. + + Returns: + dict: A dictionary containing: + - 'code_text_with_strings': Clean code with comments stripped and + whitespace collapsed, strings kept. + - 'code_text_no_strings': Clean code with comments and strings + stripped, whitespace collapsed. + - 'javadocs': List of Javadoc comment contents (with /** and */ + stripped). + - 'string_literals': List of extracted string literals, merging + multi-line '+' concatenations. + """ + code_with_strings_parts = [] + code_no_strings_parts = [] + javadocs = [] + string_literals = [] + + last_idx = 0 + last_token_was_string = False + + for match in JAVA_LEXER_REGEX.finditer(content): + # 1. Process unmatched intervening text as ordinary code + intervening_code = content[last_idx : match.start()] + if intervening_code: + code_with_strings_parts.append(intervening_code) + code_no_strings_parts.append(intervening_code) + + # Check if the intervening code was strictly whitespace and a string + # concatenation '+' + is_concat = last_token_was_string and bool( + re.match(r'^\s*\+\s*$', intervening_code) + ) + last_token_was_string = False + + # 2. Process the matched token bucket + if match.group('javadoc'): + raw_javadoc = match.group('javadoc') + # Strip /** and */ and clean leading asterisks on wrapped lines + inner = raw_javadoc[3:-2] + cleaned_lines = [ + re.sub(r'^\s*\*\s?', '', line) for line in inner.splitlines() + ] + javadocs.append('\n'.join(cleaned_lines).strip()) + # Replace comment with space in code text to prevent token merging + code_with_strings_parts.append(' ') + code_no_strings_parts.append(' ') + + elif match.group('block_comment') or match.group('line_comment'): + # Strip ordinary comments, replace with space + code_with_strings_parts.append(' ') + code_no_strings_parts.append(' ') + + elif match.group('text_block') or match.group('string'): + raw_str = match.group(0) + if raw_str.startswith('"""'): + val = raw_str[3:-3] + else: + # Strip quotes and unescape basic escape sequences + val = raw_str[1:-1].replace(r'\"', '"').replace(r'\\', '\\') + + if is_concat and string_literals: + string_literals[-1] += val + else: + string_literals.append(val) + + last_token_was_string = True + code_with_strings_parts.append(raw_str) + code_no_strings_parts.append('""') + + elif match.group('char'): + code_with_strings_parts.append(match.group(0)) + code_no_strings_parts.append("''") + + last_idx = match.end() + + # Append remaining trailing code + trailing_code = content[last_idx:] + if trailing_code: + code_with_strings_parts.append(trailing_code) + code_no_strings_parts.append(trailing_code) + + # Collapse consecutive whitespace and line breaks into single spaces for + # robust regex matching + code_with_strings = re.sub( + r'\s+', ' ', ''.join(code_with_strings_parts) + ).strip() + code_no_strings = re.sub(r'\s+', ' ', ''.join(code_no_strings_parts)).strip() + + return { + 'code_text_with_strings': code_with_strings, + 'code_text_no_strings': code_no_strings, + 'javadocs': javadocs, + 'string_literals': string_literals, + } + + +# ===================================================================== +# FILE HELPERS AND CONVENIENCE WRAPPERS +# ===================================================================== + + +def tokenize_java_file(file_path): + """Reads a Java source file from disk and returns its tokenized lexical buckets. + + Args: + file_path (str): Absolute or relative path to the Java source file. + + Returns: + dict: The tokenized buckets dictionary, or an empty default dict on read + failure. + """ + try: + with open(file_path, 'r', errors='ignore') as f: + content = f.read() + return tokenize_java_content(content) + except Exception: + return { + 'code_text_with_strings': '', + 'code_text_no_strings': '', + 'javadocs': [], + 'string_literals': [], + } + + +def clean_java_code(content): + """Convenience helper that strips comments and collapses whitespace for fast pattern counting. + + Args: + content (str): Raw Java source text or file path. + + Returns: + str: Cleaned code text with string literals preserved and multi-line + formatting normalized. + """ + if os.path.exists(content) and os.path.isfile(content): + tokens = tokenize_java_file(content) + else: + tokens = tokenize_java_content(content) + return tokens['code_text_with_strings'] + + +def get_package_name(content_or_path): + """Extracts the package declaration namespace from a file path or raw Java content string. + + Immune to commented-out package statements or multi-line formatting. + + Args: + content_or_path (str): File path ending in .java/.ejb/.jws or raw source + string. + + Returns: + str | None: The extracted package FQCN (e.g. 'com.medimed.service') or + None. + """ + if os.path.exists(content_or_path) and os.path.isfile(content_or_path): + tokens = tokenize_java_file(content_or_path) + else: + tokens = tokenize_java_content(content_or_path) + + match = re.search( + r'\bpackage\s+([a-zA-Z0-9_\.]+)\s*;', tokens['code_text_no_strings'] + ) + if match: + return match.group(1) + return None diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/requirements.txt b/skills/cloud/google-cloud-weblogic-migration/scripts/requirements.txt new file mode 100644 index 0000000000..4d07dfe2f8 --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/requirements.txt @@ -0,0 +1 @@ +networkx diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/run_openrewrite.sh b/skills/cloud/google-cloud-weblogic-migration/scripts/run_openrewrite.sh new file mode 100644 index 0000000000..6a12983c6b --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/run_openrewrite.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# OpenRewrite Runner Script +# This script executes OpenRewrite recipes on Maven projects to automate code refactoring. + +set -euo pipefail + +TARGET_DIR="${1:-.}" +RECIPE="${2:-jakarta}" # Options: jakarta, java17, spring3, quarkus + +if [ ! -d "$TARGET_DIR" ]; then + echo "Error: Directory $TARGET_DIR does not exist." >&2 + exit 1 +fi + +# Get the directory of this script to find helper scripts +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TEMP_POM_CREATED=false + +# Cleanup function to remove temp pom if created +cleanup() { + if [ "$TEMP_POM_CREATED" = true ]; then + echo "Cleaning up temporary pom.xml..." + rm -f "$TARGET_DIR/pom.xml" + fi +} +trap cleanup EXIT + +# Check if pom.xml exists in target directory, if not, generate a temporary one +if [ ! -f "$TARGET_DIR/pom.xml" ]; then + echo "No pom.xml found. Attempting to generate a temporary pom.xml for refactoring..." + python3 "$SCRIPT_DIR/generate_temp_pom.py" "$TARGET_DIR" + TEMP_POM_CREATED=true +fi + +echo "====================================================" +echo " Running OpenRewrite Refactoring" +echo " Target Directory: $TARGET_DIR" +echo " Recipe Selected: $RECIPE" +echo "====================================================" + +# Determine active recipes and dependencies +case "$RECIPE" in + jakarta) + ACTIVE_RECIPES="org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta" + # We need the rewrite-migrate-java artifact for Jakarta migration recipes + RECIPE_COORDS="org.openrewrite.recipe:rewrite-migrate-java:2.9.0" + ;; + java17) + ACTIVE_RECIPES="org.openrewrite.java.migrate.UpgradeToJava17" + RECIPE_COORDS="org.openrewrite.recipe:rewrite-migrate-java:2.9.0" + ;; + spring3) + ACTIVE_RECIPES="org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0" + RECIPE_COORDS="org.openrewrite.recipe:rewrite-spring:5.5.0" + ;; + quarkus) + ACTIVE_RECIPES="org.openrewrite.java.quarkus.quarkus3.Quarkus3Migration" + RECIPE_COORDS="org.openrewrite.recipe:rewrite-quarkus:2.1.0" + ;; + *) + echo "Error: Unknown recipe '$RECIPE'. Supported: jakarta, java17, spring3, quarkus" >&2 + exit 1 + ;; +esac + +# Run Maven command +# Note: This will download the plugin and recipes if not already in local m2 cache. +( + cd "$TARGET_DIR" + mvn org.openrewrite.maven:rewrite-maven-plugin:5.15.0:run \ + -DactiveRecipes="$ACTIVE_RECIPES" \ + -Drewrite.recipeArtifactCoordinates="$RECIPE_COORDS" \ + -Drewrite.exportDatatable=true +) + +echo "====================================================" +echo " Refactoring Run Complete." +echo " Review changes using git diff." +echo "====================================================" diff --git a/skills/cloud/google-cloud-weblogic-migration/scripts/static_analyzer.py b/skills/cloud/google-cloud-weblogic-migration/scripts/static_analyzer.py new file mode 100644 index 0000000000..799164d28f --- /dev/null +++ b/skills/cloud/google-cloud-weblogic-migration/scripts/static_analyzer.py @@ -0,0 +1,698 @@ +"""Static code analyzer for mapping Java source files and detecting cloud-unfriendly patterns in WebLogic monoliths. + +This module performs recursive source code parsing on target repository folders +using regular expressions to build a quantitative inventory of the application +assets. +Specifically, it: +- Categorizes file types (Java classes, EJB descriptors, JSPs, JSF, web +configurations). +- Identifies Enterprise JavaBean categories (Stateless, Stateful, +Message-Driven, CMP/BMP Entity Beans). +- Flags cloud-unfriendly patterns (local File I/O, HTTP Session dependencies, +legacy RMI remoting, JavaMail, JMX MBeans). +- Identifies dependencies on WebLogic-specific API imports (e.g. +`weblogic.*` classes). +- Finds SOAP WebServices, JAX-RS REST endpoints, JMS resources, JNDI Lookups, +and Security role constraints. +""" + +import json +import os +import re +import subprocess +import sys +import lexical_normalizer + +# ===================================================================== +# REGEX SCROLLING AND PATTERN MATCHING UTILITIES +# ===================================================================== + + +def count_pattern( + target_dir, pattern_str, extensions=None +): + """Scans files in a directory recursively to count occurrences of a regex pattern. + + For Java/EJB/JWS files, scans against normalized code with comments stripped. + + Args: + target_dir (str): The target directory to scan. + pattern_str (str): The regex pattern to count. + extensions (list[str]): File extensions to include in the scan. + + Returns: + int: Total number of pattern occurrences found. + """ + if extensions is None: + extensions = [".xml", ".properties", ".ejb"] + pattern = re.compile(pattern_str) + count = 0 + for root, _, files in os.walk(target_dir): + for f in files: + if any(f.endswith(ext) for ext in extensions): + path = os.path.join(root, f) + try: + if any(f.endswith(ext) for ext in [".java", ".ejb", ".jws"]): + content = lexical_normalizer.clean_java_code(path) + else: + with open(path, "r", errors="ignore") as file: + content = file.read() + count += len(pattern.findall(content)) + except OSError: + pass + return count + + +def get_files_with_pattern( + target_dir, pattern_str, extensions=None +): + """Locates files in a directory recursively that contain at least one occurrence of a regex pattern. + + For Java/EJB/JWS files, scans against normalized code with comments stripped. + + Args: + target_dir (str): The target directory to scan. + pattern_str (str): The regex pattern to search for. + extensions (list[str]): File extensions to include. + + Returns: + list[str]: Relative paths of matching files from the target directory. + """ + if extensions is None: + extensions = [".java", ".ejb", ".jws"] + pattern = re.compile(pattern_str) + found_files = [] + for root, _, files in os.walk(target_dir): + for f in files: + if any(f.endswith(ext) for ext in extensions): + path = os.path.join(root, f) + try: + if any(f.endswith(ext) for ext in [".java", ".ejb", ".jws"]): + content = lexical_normalizer.clean_java_code(path) + else: + with open(path, "r", errors="ignore") as file: + content = file.read() + if pattern.search(content): + found_files.append(os.path.relpath(path, target_dir)) + except OSError: + pass + return found_files + + +# ===================================================================== +# UNIFIED STATIC STRUCTURE AND BLOCKERS ANALYSIS +# ===================================================================== +def combine_metric(regex_val, ast_val): + """Returns authoritative AST metric if available and greater than 0, otherwise falls back to regex count. + + Prevents double-counting when both regex matching and AST analysis detect + occurrences. + + Args: + regex_val (int): The count from regex matching. + ast_val (int): The count from AST analysis. + + Returns: + int: The combined metric value. + """ + return ast_val if ast_val and ast_val > 0 else regex_val + + +def get_compatible_jdk_env(): + """Checks the default Java version and returns an env dict with JAVA_HOME set to a compatible JDK (11, 17, or 21) if needed.""" + env = os.environ.copy() + if "JAVA_HOME" in env: + return env + + try: + version_res = subprocess.run( + ["java", "-version"], capture_output=True, text=True, check=True + ) + version_output = version_res.stderr or version_res.stdout + match = re.search(r'(?:version\s+"([^"]+)")', version_output) + if match: + version_str = match.group(1) + if version_str.startswith("1."): + major = int(version_str.split(".")[1]) + else: + major = int(version_str.split(".")[0]) + + if 11 <= major <= 21: + return env + except (subprocess.SubprocessError, OSError): + pass + + common_paths = [ + "/usr/lib/jvm/java-21-openjdk-amd64", + "/usr/lib/jvm/openjdk-21", + "/usr/lib/jvm/java-17-openjdk-amd64", + "/usr/lib/jvm/java-11-openjdk-amd64", + ] + + for path in common_paths: + if os.path.exists(path) and os.path.exists( + os.path.join(path, "bin", "java") + ): + env["JAVA_HOME"] = path + env["PATH"] = ( + os.path.join(path, "bin") + os.path.pathsep + env.get("PATH", "") + ) + print( + f"Note: Overriding JAVA_HOME to compatible JDK: {path}", + file=sys.stderr, + ) + return env + + print( + "Warning: Could not find a compatible JDK (11, 17, or 21) in common" + " paths. AST parsing may fail.", + file=sys.stderr, + ) + return env + + +def get_ast_metrics(target_dir): + """Executes the OpenRewrite AST analyzer to extract true AST metrics from Java files.""" + ast_parser_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "ast_parser" + ) + env = get_compatible_jdk_env() + + # 1. Compile the ast_parser project + try: + subprocess.run( + ["mvn", "-q", "compile"], + cwd=ast_parser_dir, + capture_output=True, + text=True, + check=True, + env=env, + ) + except FileNotFoundError as e: + raise RuntimeError( + "Maven ('mvn') executable not found. Please install Maven to use the " + + "AST parser." + ) from e + except subprocess.CalledProcessError as e: + print( + f"Error: Maven compilation failed with exit status {e.returncode}.", + file=sys.stderr, + ) + if e.stdout: + print(f"Maven compilation stdout:\n{e.stdout}", file=sys.stderr) + if e.stderr: + print(f"Maven compilation stderr:\n{e.stderr}", file=sys.stderr) + raise RuntimeError( + "AST parser compilation failed. Cannot proceed with static analysis." + ) from e + + # 2. Run the analyzer + try: + exec_cmd = [ + "mvn", + "-q", + "exec:java", + "-Dexec.mainClass=OpenRewriteAstAnalyzer", + f"-Dexec.args={os.path.abspath(target_dir)}", + ] + result = subprocess.run( + exec_cmd, + cwd=ast_parser_dir, + capture_output=True, + text=True, + check=True, + env=env, + ) + except subprocess.CalledProcessError as e: + print( + f"Error: AST parser execution failed with exit status {e.returncode}.", + file=sys.stderr, + ) + if e.stdout: + print(f"AST parser execution stdout:\n{e.stdout}", file=sys.stderr) + if e.stderr: + print(f"AST parser execution stderr:\n{e.stderr}", file=sys.stderr) + raise RuntimeError( + "AST parser execution failed. Cannot proceed with static analysis." + ) from e + + # 3. Parse JSON output + output = result.stdout.strip() + json_start = output.find("{") + json_end = output.rfind("}") + 1 + if json_start != -1 and json_end != -1: + try: + return json.loads(output[json_start:json_end]) + except json.JSONDecodeError as e: + print( + "Error: Failed to parse AST parser JSON output. Raw output" + f" was:\n{output}", + file=sys.stderr, + ) + raise RuntimeError("AST parser generated invalid JSON output.") from e + else: + raise RuntimeError( + f"AST parser did not output a valid JSON block. Raw output:\n{output}" + ) + + +def analyze_static(target_dir): + """Runs the full static analysis scanning suite on the target monolith repository. + + Aggregates metrics for files, weblogic APIs, EJBs, JMS, JNDI, transactions, + databases, security roles, MVC frameworks, and cloud-unfriendly patterns (e.g. + raw Thread/Socket). + + Args: + target_dir (str): The target monolith repository directory path. + + Returns: + dict: A nested dictionary containing structural and inventory metrics. + """ + report = {} + + # 1. General Metrics + java_files = 0 + xml_files = 0 + jsp_files = 0 + jsf_files = 0 + jws_files = 0 + html_files = 0 + properties_files = 0 + total_files = 0 + for _, _, files in os.walk(target_dir): + for f in files: + total_files += 1 + ext = os.path.splitext(f)[1].lower() + if ext in [".java", ".ejb"]: + java_files += 1 + elif ext == ".jws": + jws_files += 1 + elif ext == ".xml": + xml_files += 1 + elif ext in [".jsp", ".jspx"]: + jsp_files += 1 + elif ext in [".xhtml", ".jsf", ".faces"]: + jsf_files += 1 + elif ext in [".html", ".htm"]: + html_files += 1 + elif ext == ".properties": + properties_files += 1 + + report["general"] = { + "total_files": total_files, + "java_files": java_files, + "xml_files": xml_files, + "jsp_files": jsp_files, + "jsf_files": jsf_files, + "jws_files": jws_files, + "html_files": html_files, + "properties_files": properties_files, + } + + # Fetch AST metrics for .java files + ast = get_ast_metrics(target_dir) + ast_wls = ast.get("weblogic_api", {}) + ast_ejb = ast.get("ejb", {}) + ast_jms = ast.get("jms", {}) + ast_jndi = ast.get("jndi", {}) + ast_tx = ast.get("transactions", {}) + ast_da = ast.get("data_access", {}) + ast_sec = ast.get("security", {}) + ast_web = ast.get("web_tier", {}) + ast_adv = ast.get("advanced_features", {}) + ast_cloud = ast.get("cloud_unfriendly_patterns", {}) + + # 2. WebLogic Specific APIs + report["weblogic_api"] = { + "imports_count": combine_metric( + count_pattern( + target_dir, + r"import weblogic\.|GenericSessionBean|GenericMessageDrivenBean", + ), + ast_wls.get("imports_count", 0), + ), + "logging_count": combine_metric( + count_pattern(target_dir, r"weblogic\.logging|NonCatalogLogger"), + ast_wls.get("logging_count", 0), + ), + "security_count": combine_metric( + count_pattern(target_dir, r"weblogic\.security"), + ast_wls.get("security_count", 0), + ), + "transaction_count": combine_metric( + count_pattern(target_dir, r"weblogic\.transaction"), + ast_wls.get("transaction_count", 0), + ), + "wls_transaction_manager_count": combine_metric( + count_pattern( + target_dir, + r"weblogic\.transaction\.(TransactionManager|TxHelper)", + ), + ast_wls.get("wls_transaction_manager_count", 0), + ), + "wls_user_transaction_count": combine_metric( + count_pattern(target_dir, r"weblogic\.transaction\.UserTransaction"), + ast_wls.get("wls_user_transaction_count", 0), + ), + "coherence_count": combine_metric( + count_pattern(target_dir, r"com\.tangosol\.|coherence\.xml"), + ast_wls.get("coherence_count", 0), + ), + "workmanager_count": combine_metric( + count_pattern( + target_dir, + r"weblogic\.work\.WorkManager|weblogic\.work\.WorkManagerFactory", + ), + ast_wls.get("workmanager_count", 0), + ), + "jws_api_count": combine_metric( + count_pattern(target_dir, r"weblogic\.jws"), + ast_wls.get("jws_api_count", 0), + ), + "files_with_imports": get_files_with_pattern( + target_dir, + r"import weblogic\.|GenericSessionBean|GenericMessageDrivenBean", + )[:10], + } + + # 3. EJB + report["ejb"] = { + "stateless_count": combine_metric( + count_pattern(target_dir, r"@Stateless|type\s*=\s*Stateless"), + ast_ejb.get("stateless_count", 0), + ), + "stateful_count": combine_metric( + count_pattern(target_dir, r"@Stateful|type\s*=\s*Stateful"), + ast_ejb.get("stateful_count", 0), + ), + "singleton_count": combine_metric( + count_pattern(target_dir, r"@Singleton"), + ast_ejb.get("singleton_count", 0), + ), + "mdb_count": combine_metric( + count_pattern( + target_dir, + r"@MessageDriven|GenericMessageDrivenBean|@ejbgen:message-driven", + ), + ast_ejb.get("mdb_count", 0), + ), + "entity_bean_count": combine_metric( + count_pattern( + target_dir, + r"implements EntityBean|extends EntityBean|@ejbgen:entity", + ), + ast_ejb.get("entity_bean_count", 0), + ), + "ejb_2x_home_interfaces_count": combine_metric( + count_pattern( + target_dir, + r"extends EJBHome|extends EJBLocalHome|extends EJBObject|extends" + r" EJBLocalObject", + ), + ast_ejb.get("ejb_2x_home_interfaces_count", 0), + ), + "ejb_descriptors_count": len( + get_files_with_pattern(target_dir, r"ejb-jar\.xml", [".xml"]) + ), + } + + # 4. JMS + report["jms"] = { + "imports_count": combine_metric( + count_pattern(target_dir, r"import (javax|jakarta)\.jms\."), + ast_jms.get("imports_count", 0), + ), + "jndi_lookups_count": combine_metric( + count_pattern(target_dir, r"jms/"), ast_jndi.get("lookups_count", 0) + ), + "producer_count": combine_metric( + count_pattern( + target_dir, + r"(javax|jakarta)\.jms\.(MessageProducer|QueueSender|TopicPublisher)", + ), + ast_jms.get("producer_count", 0), + ), + "consumer_count": combine_metric( + count_pattern( + target_dir, + r"(javax|jakarta)\.jms\.(MessageConsumer|QueueReceiver|TopicSubscriber)", + ), + ast_jms.get("consumer_count", 0), + ), + "queue_count": combine_metric( + count_pattern(target_dir, r"(javax|jakarta)\.jms\.Queue\b"), + ast_jms.get("queue_count", 0), + ), + "topic_count": combine_metric( + count_pattern(target_dir, r"(javax|jakarta)\.jms\.Topic\b"), + ast_jms.get("topic_count", 0), + ), + } + + # 5. JNDI + report["jndi"] = { + "initial_context_count": combine_metric( + count_pattern(target_dir, r"new InitialContext\("), + ast_jndi.get("initial_context_count", 0), + ), + "lookups_count": combine_metric( + count_pattern(target_dir, r"\.lookup\("), + ast_jndi.get("lookups_count", 0), + ), + } + + # 6. Transactions + report["transactions"] = { + "user_transaction_count": combine_metric( + count_pattern(target_dir, r"UserTransaction"), + ast_tx.get("user_transaction_count", 0), + ), + "xa_datasource_count": combine_metric( + count_pattern(target_dir, r"XADataSource|javax\.sql\.XA"), + ast_tx.get("xa_datasource_count", 0), + ), + "xa_resource_count": combine_metric( + count_pattern( + target_dir, r"XAResource|(javax|jakarta)\.transaction\.xa" + ), + ast_tx.get("xa_resource_count", 0), + ), + "declarative_transaction_count": combine_metric( + count_pattern(target_dir, r"@Transactional|@TransactionAttribute"), + ast_tx.get("declarative_transaction_count", 0), + ), + "standard_transaction_manager_count": combine_metric( + count_pattern( + target_dir, r"(javax|jakarta)\.transaction\.TransactionManager" + ), + ast_tx.get("standard_transaction_manager_count", 0), + ), + } + + # 7. Data Access + report["data_access"] = { + "jdbc_connections_count": combine_metric( + count_pattern( + target_dir, r"java\.sql\.Connection|javax\.sql\.DataSource" + ), + ast_da.get("jdbc_connections_count", 0), + ), + "jpa_entities_count": combine_metric( + count_pattern(target_dir, r"@Entity"), + ast_da.get("jpa_entities_count", 0), + ), + "hibernate_sessions_count": combine_metric( + count_pattern(target_dir, r"org\.hibernate\.Session"), + ast_da.get("hibernate_sessions_count", 0), + ), + "spring_data_repositories_count": combine_metric( + count_pattern(target_dir, r"org\.springframework\.data\.repository"), + ast_da.get("spring_data_repositories_count", 0), + ), + "persistence_xml_count": len( + get_files_with_pattern(target_dir, r"persistence\.xml", [".xml"]) + ), + } + + # 8. Security + report["security"] = { + "roles_allowed_count": combine_metric( + count_pattern(target_dir, r"@RolesAllowed"), + ast_sec.get("roles_allowed_count", 0), + ), + "run_as_count": combine_metric( + count_pattern(target_dir, r"@RunAs"), ast_sec.get("run_as_count", 0) + ), + "user_in_role_count": combine_metric( + count_pattern(target_dir, r"\.isUserInRole\("), + ast_sec.get("user_in_role_count", 0), + ), + "user_principal_count": combine_metric( + count_pattern(target_dir, r"\.getUserPrincipal\("), + ast_sec.get("user_principal_count", 0), + ), + } + + # 9. Web Tier + report["web_tier"] = { + "jsp_files_count": len( + get_files_with_pattern(target_dir, r".*", [".jsp", ".jspx"]) + ), + "servlets_count": combine_metric( + count_pattern(target_dir, r"extends HttpServlet|@WebServlet"), + ast_web.get("servlets_count", 0), + ), + "filters_count": combine_metric( + count_pattern(target_dir, r"implements Filter|@WebFilter"), + ast_web.get("filters_count", 0), + ), + "listeners_count": combine_metric( + count_pattern(target_dir, r"ServletContextListener|@WebListener"), + ast_web.get("listeners_count", 0), + ), + "jax_rs_endpoints_count": combine_metric( + count_pattern(target_dir, r"@Path|(javax|jakarta)\.ws\.rs\."), + ast_web.get("jax_rs_endpoints_count", 0), + ), + "spring_controllers_count": combine_metric( + count_pattern(target_dir, r"@Controller|@RestController"), + ast_web.get("spring_controllers_count", 0), + ), + "struts_count": combine_metric( + count_pattern(target_dir, r"org\.apache\.struts"), + ast_web.get("struts_count", 0), + ), + "jsf_count": combine_metric( + count_pattern( + target_dir, r"javax\.faces|jakarta\.faces|@ManagedBean" + ), + ast_web.get("jsf_count", 0), + ), + "web_xml_count": len( + get_files_with_pattern(target_dir, r"web\.xml", [".xml"]) + ), + "weblogic_xml_count": len( + get_files_with_pattern(target_dir, r"weblogic\.xml", [".xml"]) + ), + "http_sessions_count": combine_metric( + count_pattern(target_dir, r"HttpSession|request\.getSession\("), + ast_web.get("http_sessions_count", 0), + ), + } + + # 10. Advanced Features + report["advanced_features"] = { + "work_managers_count": combine_metric( + count_pattern(target_dir, r"work-manager|commonj\.work"), + ast_adv.get("work_managers_count", 0), + ), + "timers_count": combine_metric( + count_pattern( + target_dir, r"javax\.ejb\.TimerService|commonj\.timers" + ), + ast_adv.get("timers_count", 0), + ), + "resource_adapters_count": len( + get_files_with_pattern( + target_dir, r"weblogic-ra\.xml|ra\.xml", [".xml"] + ) + ), + "classloading_custom_count": count_pattern( + target_dir, + r"prefer-application-packages|prefer-application-resources", + ), + "web_services_count": combine_metric( + count_pattern( + target_dir, r"weblogic-webservices\.xml|@WebService|@WebMethod" + ), + ast_adv.get("web_services_count", 0), + ), + "jax_rpc_count": combine_metric( + count_pattern(target_dir, r"javax\.xml\.rpc"), + ast_adv.get("jax_rpc_count", 0), + ), + "jax_ws_count": combine_metric( + count_pattern(target_dir, r"javax\.jws|jakarta\.jws"), + ast_adv.get("jax_ws_count", 0), + ), + "batch_processing_count": combine_metric( + count_pattern( + target_dir, r"javax\.batch\.api|org\.springframework\.batch" + ), + ast_adv.get("batch_processing_count", 0), + ), + "jmx_mbeans_count": combine_metric( + count_pattern( + target_dir, r"javax\.management\.MBeanServer|weblogic\.management" + ), + ast_adv.get("jmx_mbeans_count", 0), + ), + } + + # 11. Cloud-Unfriendly Patterns + ip_regex = r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b" + total_ips = count_pattern(target_dir, ip_regex) + loopback_ips = count_pattern(target_dir, r"\b127\.0\.0\.1\b|\b0\.0\.0\.0\b") + hardcoded_ips = max(0, total_ips - loopback_ips) + + path_regex = r"\"/(opt|var|usr|etc|bin|srv)/|\"[a-zA-Z]:\\\\" + absolute_paths = count_pattern(target_dir, path_regex) + raw_threads = combine_metric( + count_pattern(target_dir, r"new Thread\("), + ast_cloud.get("raw_threads_count", 0), + ) + native_processes = combine_metric( + count_pattern( + target_dir, r"Runtime\.getRuntime\(\)\.exec\b|ProcessBuilder\b" + ), + ast_cloud.get("native_processes_count", 0), + ) + direct_sockets = combine_metric( + count_pattern(target_dir, r"new\s+(Server)?Socket\("), + ast_cloud.get("direct_sockets_count", 0), + ) + jaas_login_modules = combine_metric( + count_pattern( + target_dir, + r"implements\s+LoginModule|extends\s+UsernamePasswordLoginModule", + ), + ast_cloud.get("jaas_login_modules_count", 0), + ) + specific_proxies = combine_metric( + count_pattern(target_dir, r"HttpClusterServlet|HttpProxyServlet"), + ast_cloud.get("specific_proxies_count", 0), + ) + file_io = combine_metric( + count_pattern( + target_dir, + r"java\.io\.FileOutputStream|java\.io\.FileWriter|java\.nio\.file\.Files\.write|java\.io\.RandomAccessFile", + ), + ast_cloud.get("file_io_count", 0), + ) + rmi_corba = combine_metric( + count_pattern( + target_dir, + r"java\.rmi\.|javax\.rmi\.|UnicastRemoteObject|Naming\.lookup", + ), + ast_cloud.get("rmi_corba_count", 0), + ) + java_mail = combine_metric( + count_pattern(target_dir, r"javax\.mail\.|jakarta\.mail\."), + ast_cloud.get("java_mail_count", 0), + ) + + report["cloud_unfriendly_patterns"] = { + "hardcoded_ips_count": hardcoded_ips, + "absolute_paths_count": absolute_paths, + "raw_threads_count": raw_threads, + "native_processes_count": native_processes, + "direct_sockets_count": direct_sockets, + "jaas_login_modules_count": jaas_login_modules, + "specific_proxies_count": specific_proxies, + "file_io_count": file_io, + "rmi_corba_count": rmi_corba, + "java_mail_count": java_mail, + } + + return report