From db4a75ab50dd461a6ae0c6569b373c24077c629a Mon Sep 17 00:00:00 2001
From: joshwanf <17016446+joshwanf@users.noreply.github.com>
Date: Sat, 17 Jan 2026 18:10:47 -0500
Subject: [PATCH 1/2] added validation/serialization to Create Screener
Endpoint
---
builder-api/pom.xml | 13 +-
.../error/UnknownFieldExceptionMapper.java | 25 +
.../org/acme/controller/ScreenerResource.java | 768 +++++++++---------
.../java/org/acme/model/domain/Screener.java | 37 +-
.../dto/Screener/CreateScreenerRequest.java | 8 +
.../src/main/resources/application.properties | 3 +
6 files changed, 462 insertions(+), 392 deletions(-)
create mode 100644 builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
create mode 100644 builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java
diff --git a/builder-api/pom.xml b/builder-api/pom.xml
index d0cf3dd8..6e7daaed 100644
--- a/builder-api/pom.xml
+++ b/builder-api/pom.xml
@@ -1,5 +1,7 @@
-
+
4.0.0
org.acme
builder-api
@@ -50,6 +52,10 @@
io.quarkus
quarkus-arc
+
+ io.quarkus
+ quarkus-hibernate-validator
+
io.quarkiverse.googlecloudservices
quarkus-google-cloud-firebase-admin
@@ -168,7 +174,8 @@
- ${project.build.directory}/${project.build.finalName}-runner
+
+ ${project.build.directory}/${project.build.finalName}-runner
org.jboss.logmanager.LogManager
${maven.home}
@@ -191,4 +198,4 @@
-
+
\ No newline at end of file
diff --git a/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
new file mode 100644
index 00000000..0aa26b25
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
@@ -0,0 +1,25 @@
+package org.acme.api.error;
+
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+
+import java.util.Map;
+
+// Global error handler for providing extra fields in the request body
+
+@Provider
+public class UnknownFieldExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(UnrecognizedPropertyException e) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(Map.of(
+ "error", true,
+ "message", "Unknown field " + e.getPropertyName()))
+ .build();
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
index 9e42abb8..0e5abc72 100644
--- a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
+++ b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
@@ -5,433 +5,451 @@
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
+import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import org.acme.auth.AuthUtils;
import org.acme.model.domain.*;
import org.acme.model.dto.PublishScreenerRequest;
import org.acme.model.dto.SaveSchemaRequest;
+import org.acme.model.dto.Screener.CreateScreenerRequest;
import org.acme.persistence.EligibilityCheckRepository;
-import org.acme.persistence.ScreenerRepository;
import org.acme.persistence.PublishedScreenerRepository;
+import org.acme.persistence.ScreenerRepository;
import org.acme.persistence.StorageService;
import org.acme.service.DmnService;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
@Path("/api")
public class ScreenerResource {
- @Inject
- ScreenerRepository screenerRepository;
+ @Inject ScreenerRepository screenerRepository;
- @Inject
- PublishedScreenerRepository publishedScreenerRepository;
+ @Inject PublishedScreenerRepository publishedScreenerRepository;
- @Inject
- EligibilityCheckRepository eligibilityCheckRepository;
+ @Inject EligibilityCheckRepository eligibilityCheckRepository;
- @Inject
- StorageService storageService;
+ @Inject StorageService storageService;
- @Inject
- DmnService dmnService;
+ @Inject DmnService dmnService;
- @GET
- @Path("/screeners")
- public Response getScreeners(@Context SecurityIdentity identity) {
- String userId = AuthUtils.getUserId(identity);
- if (userId == null){
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
- Log.info("Fetching screeners for user: " + userId);
- List screeners = screenerRepository.getWorkingScreeners(userId);
-
- return Response.ok(screeners, MediaType.APPLICATION_JSON).build();
+ @GET
+ @Path("/screeners")
+ public Response getScreeners(@Context SecurityIdentity identity) {
+ String userId = AuthUtils.getUserId(identity);
+ if (userId == null) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
}
+ Log.info("Fetching screeners for user: " + userId);
+ List screeners = screenerRepository.getWorkingScreeners(userId);
- @GET
- @Path("/screener/{screenerId}")
- public Response getScreener(@Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) {
- String userId = AuthUtils.getUserId(identity);
- Log.info("Fetching screener " + screenerId + " for user " + userId);
+ return Response.ok(screeners, MediaType.APPLICATION_JSON).build();
+ }
- Optional screenerOptional = screenerRepository.getWorkingScreener(screenerId);
+ @GET
+ @Path("/screener/{screenerId}")
+ public Response getScreener(
+ @Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) {
+ String userId = AuthUtils.getUserId(identity);
+ Log.info("Fetching screener " + screenerId + " for user " + userId);
- if (screenerOptional.isEmpty()){
- throw new NotFoundException();
- }
+ Optional screenerOptional = screenerRepository.getWorkingScreener(screenerId);
- Screener screener = screenerOptional.get();
- if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
+ if (screenerOptional.isEmpty()) {
+ throw new NotFoundException();
+ }
- return Response.ok(screener, MediaType.APPLICATION_JSON).build();
+ Screener screener = screenerOptional.get();
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
}
- @GET
- @Path("/published/screener/{screenerId}")
- @PermitAll // This endpoint is accessible without authentication
- public Response getPublishedScreener(@PathParam("screenerId") String screenerId) {
- Optional screenerOptional = publishedScreenerRepository.getScreener(screenerId);
- if (screenerOptional.isEmpty()){
- throw new NotFoundException();
- }
+ return Response.ok(screener, MediaType.APPLICATION_JSON).build();
+ }
- return Response.ok(screenerOptional.get(), MediaType.APPLICATION_JSON).build();
+ @GET
+ @Path("/published/screener/{screenerId}")
+ @PermitAll // This endpoint is accessible without authentication
+ public Response getPublishedScreener(@PathParam("screenerId") String screenerId) {
+ Optional screenerOptional = publishedScreenerRepository.getScreener(screenerId);
+ if (screenerOptional.isEmpty()) {
+ throw new NotFoundException();
}
- @POST
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/screener")
- public Response postScreener(@Context SecurityIdentity identity, Screener newScreener){
- String userId = AuthUtils.getUserId(identity);
-
- newScreener.setOwnerId(userId);
- try {
- String screenerId = screenerRepository.saveNewWorkingScreener(newScreener);
- newScreener.setId(screenerId);
- return Response.ok(newScreener, MediaType.APPLICATION_JSON).build();
- } catch (Exception e){
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not save Screener"))
- .build();
- }
+ return Response.ok(screenerOptional.get(), MediaType.APPLICATION_JSON).build();
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Path("/screener")
+ public Response postScreener(
+ @Context SecurityIdentity identity, @Valid CreateScreenerRequest request) {
+ String userId = AuthUtils.getUserId(identity);
+
+ Screener newScreener = Screener.create(userId, request.screenerName(), request.description());
+
+ try {
+ String screenerId = screenerRepository.saveNewWorkingScreener(newScreener);
+ newScreener.setId(screenerId);
+ return Response.ok(newScreener, MediaType.APPLICATION_JSON).build();
+ } catch (Exception e) {
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not save Screener"))
+ .build();
}
-
- @PUT
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/screener")
- public Response updateScreener(@Context SecurityIdentity identity, Screener screener){
- String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, screener.getId())) return Response.status(Response.Status.UNAUTHORIZED).build();
-
- //add user info to the update data
- screener.setOwnerId(userId);
-
- try {
- screenerRepository.updateWorkingScreener(screener);
-
- return Response.ok().build();
- } catch (Exception e){
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not update Screener"))
- .build();
- }
+ }
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Path("/screener")
+ public Response updateScreener(@Context SecurityIdentity identity, Screener screener) {
+ String userId = AuthUtils.getUserId(identity);
+ if (!isUserAuthorizedToAccessScreener(userId, screener.getId()))
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+
+ // add user info to the update data
+ screener.setOwnerId(userId);
+
+ try {
+ screenerRepository.updateWorkingScreener(screener);
+
+ return Response.ok().build();
+ } catch (Exception e) {
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not update Screener"))
+ .build();
}
-
- @POST
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/save-form-schema")
- public Response saveFormSchema(@Context SecurityIdentity identity, SaveSchemaRequest saveSchemaRequest){
-
- String screenerId = saveSchemaRequest.screenerId;
- if (screenerId == null || screenerId.isBlank()){
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required required data in request body: screenerId")
- .build();
- }
-
- String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, saveSchemaRequest.screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build();
-
- JsonNode schema = saveSchemaRequest.schema;
- if (schema == null){
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required required data in request body: screenerId")
- .build();
- }
- try {
- String filePath = storageService.getScreenerWorkingFormSchemaPath(screenerId);
- storageService.writeJsonToStorage(filePath, schema);
- Log.info("Saved form schema of screener " + screenerId + " to storage");
- return Response.ok().build();
- } catch (Exception e){
- Log.info(("Failed to save form for screener " + screenerId));
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
- }
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Path("/save-form-schema")
+ public Response saveFormSchema(
+ @Context SecurityIdentity identity, SaveSchemaRequest saveSchemaRequest) {
+
+ String screenerId = saveSchemaRequest.screenerId;
+ if (screenerId == null || screenerId.isBlank()) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Error: Missing required required data in request body: screenerId")
+ .build();
}
- @POST
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/publish")
- public Response publishScreener(@Context SecurityIdentity identity, PublishScreenerRequest publishScreenerRequest){
-
- String screenerId = publishScreenerRequest.screenerId;
- if (screenerId == null || screenerId.isBlank()){
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required query parameter: screenerId")
- .build();
- }
-
- String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build();
-
- try {
- Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
- if (screenerOpt.isEmpty()) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
- Screener screener = screenerOpt.get();
- screenerRepository.publishScreener(screener);
- return Response.ok().build();
- } catch (Exception e) {
- Log.error("Error: Error updating screener to published. Screener: " + screenerId);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
- }
- }
+ String userId = AuthUtils.getUserId(identity);
+ if (!isUserAuthorizedToAccessScreener(userId, saveSchemaRequest.screenerId))
+ return Response.status(Response.Status.UNAUTHORIZED).build();
- @DELETE
- @Path("/screener/delete")
- public Response deleteScreener(@Context SecurityIdentity identity, @QueryParam("screenerId") String screenerId){
- if (screenerId == null || screenerId.isBlank()){
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required query parameter: screenerId")
- .build();
- }
-
- String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build();
-
- try {
- screenerRepository.deleteWorkingScreener(screenerId);
- return Response.ok().build();
- } catch (Exception e){
- Log.error("Error: error deleting screener " + screenerId);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
- }
+ JsonNode schema = saveSchemaRequest.schema;
+ if (schema == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Error: Missing required required data in request body: screenerId")
+ .build();
+ }
+ try {
+ String filePath = storageService.getScreenerWorkingFormSchemaPath(screenerId);
+ storageService.writeJsonToStorage(filePath, schema);
+ Log.info("Saved form schema of screener " + screenerId + " to storage");
+ return Response.ok().build();
+ } catch (Exception e) {
+ Log.info(("Failed to save form for screener " + screenerId));
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Path("/publish")
+ public Response publishScreener(
+ @Context SecurityIdentity identity, PublishScreenerRequest publishScreenerRequest) {
+
+ String screenerId = publishScreenerRequest.screenerId;
+ if (screenerId == null || screenerId.isBlank()) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Error: Missing required query parameter: screenerId")
+ .build();
}
- private boolean isUserAuthorizedToAccessScreener(String userId, String screenerId) {
- Optional screenerOptional = screenerRepository.getWorkingScreenerMetaDataOnly(screenerId);
- if (screenerOptional.isEmpty()){
- return false;
- }
- Screener screener = screenerOptional.get();
- return isUserAuthorizedToAccessScreenerByScreener(userId, screener);
+ String userId = AuthUtils.getUserId(identity);
+ if (!isUserAuthorizedToAccessScreener(userId, screenerId))
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+
+ try {
+ Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
+ if (screenerOpt.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ Screener screener = screenerOpt.get();
+ screenerRepository.publishScreener(screener);
+ return Response.ok().build();
+ } catch (Exception e) {
+ Log.error("Error: Error updating screener to published. Screener: " + screenerId);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
+ }
+
+ @DELETE
+ @Path("/screener/delete")
+ public Response deleteScreener(
+ @Context SecurityIdentity identity, @QueryParam("screenerId") String screenerId) {
+ if (screenerId == null || screenerId.isBlank()) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Error: Missing required query parameter: screenerId")
+ .build();
+ }
+
+ String userId = AuthUtils.getUserId(identity);
+ if (!isUserAuthorizedToAccessScreener(userId, screenerId))
+ return Response.status(Response.Status.UNAUTHORIZED).build();
- private boolean isUserAuthorizedToAccessScreenerByScreener(String userId, Screener screener) {
- String ownerId = screener.getOwnerId();
- if (userId.equals(ownerId)){
- return true;
- }
- return false;
+ try {
+ screenerRepository.deleteWorkingScreener(screenerId);
+ return Response.ok().build();
+ } catch (Exception e) {
+ Log.error("Error: error deleting screener " + screenerId);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
+ }
- @GET
- @Path("/screener/{screenerId}/benefit")
- public Response getScreenerBenefits(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId){
- String userId = AuthUtils.getUserId(identity);
-
- Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
- if (screenerOpt.isEmpty()){
- throw new NotFoundException();
- }
- Screener screener = screenerOpt.get();
-
- if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- try{
- List benefits = screenerRepository.getBenefitsInScreener(screener);
- return Response.ok().entity(benefits).build();
- } catch (Exception e){
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not fetch benefits"))
- .build();
- }
+ private boolean isUserAuthorizedToAccessScreener(String userId, String screenerId) {
+ Optional screenerOptional =
+ screenerRepository.getWorkingScreenerMetaDataOnly(screenerId);
+ if (screenerOptional.isEmpty()) {
+ return false;
+ }
+ Screener screener = screenerOptional.get();
+ return isUserAuthorizedToAccessScreenerByScreener(userId, screener);
+ }
+
+ private boolean isUserAuthorizedToAccessScreenerByScreener(String userId, Screener screener) {
+ String ownerId = screener.getOwnerId();
+ if (userId.equals(ownerId)) {
+ return true;
}
+ return false;
+ }
+
+ @GET
+ @Path("/screener/{screenerId}/benefit")
+ public Response getScreenerBenefits(
+ @Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) {
+ String userId = AuthUtils.getUserId(identity);
+
+ Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
+ if (screenerOpt.isEmpty()) {
+ throw new NotFoundException();
+ }
+ Screener screener = screenerOpt.get();
- @GET
- @Path("/screener/{screenerId}/benefit/{benefitId}")
- public Response getScreenerBenefit(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId,
- @PathParam("benefitId") String benefitId){
- String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, screenerId)){
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- try{
- Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
- if (benefitOpt.isEmpty()){
- return Response.status(Response.Status.NOT_FOUND).build();
- }
- return Response.ok().entity(benefitOpt.get()).build();
- } catch (Exception e){
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not fetch benefit"))
- .build();
- }
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
}
- @GET
- @Path("/screener/{screenerId}/benefit/{benefitId}/check")
- public Response getScreenerCustomBenefitChecks(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId,
- @PathParam("benefitId") String benefitId){
- try {
- String userId = AuthUtils.getUserId(identity);
-
- Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
- if (screenerOpt.isEmpty()){
- throw new NotFoundException();
- }
- Screener screener = screenerOpt.get();
-
- if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
- if (benefitOpt.isEmpty()) {
- throw new NotFoundException();
- }
-
- List checks = eligibilityCheckRepository.getChecksInBenefit(benefitOpt.get());
- return Response.ok().entity(checks).build();
- } catch (Exception e){
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not fetch checks"))
- .build();
- }
+ try {
+ List benefits = screenerRepository.getBenefitsInScreener(screener);
+ return Response.ok().entity(benefits).build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not fetch benefits"))
+ .build();
+ }
+ }
+
+ @GET
+ @Path("/screener/{screenerId}/benefit/{benefitId}")
+ public Response getScreenerBenefit(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ @PathParam("benefitId") String benefitId) {
+ String userId = AuthUtils.getUserId(identity);
+ if (!isUserAuthorizedToAccessScreener(userId, screenerId)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
}
- @POST
- @Path("/screener/{screenerId}/benefit")
- public Response addCustomBenefit(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId,
- Benefit newBenefit) {
- String userId = AuthUtils.getUserId(identity);
-
- newBenefit.setOwnerId(userId);
- newBenefit.setChecks(Collections.emptyList());
-
- BenefitDetail benefitDetail = new BenefitDetail();
- benefitDetail.setId(newBenefit.getId());
- benefitDetail.setName(newBenefit.getName());
- benefitDetail.setDescription(newBenefit.getDescription());
- benefitDetail.setPublic(newBenefit.getPublic());
- try {
- // Check to make sure not introducing duplicates
- Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
- if (screenerOpt.isEmpty()){
- Log.error("Screener not found. Screener ID:" + screenerId);
- throw new NotFoundException();
- }
-
- Screener screener = screenerOpt.get();
-
- // Authorise action
- if (userId != null && !isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- List benefits = screenerOpt.get().getBenefits();
- if (benefits == null) {
- benefits = Collections.emptyList();
- }
- Boolean benefitIdExists = !benefits.stream().filter(benefit -> benefit.getId().equals(benefitDetail.getId())).toList().isEmpty();
-
- if (benefitIdExists){
- return Response.status(
- Response.Status.CONFLICT.getStatusCode(),
- "Benefit with provided ID already exists on screener."
- ).build();
- }
-
- String benefitId = screenerRepository.saveNewCustomBenefit(screenerId, newBenefit);
- screenerRepository.addBenefitDetailToWorkingScreener(screenerId, benefitDetail);
- newBenefit.setId(benefitId);
- return Response.ok(newBenefit, MediaType.APPLICATION_JSON).build();
- } catch (Exception e) {
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not save benefit"))
- .build();
- }
+ try {
+ Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
+ if (benefitOpt.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ return Response.ok().entity(benefitOpt.get()).build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not fetch benefit"))
+ .build();
+ }
+ }
+
+ @GET
+ @Path("/screener/{screenerId}/benefit/{benefitId}/check")
+ public Response getScreenerCustomBenefitChecks(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ @PathParam("benefitId") String benefitId) {
+ try {
+ String userId = AuthUtils.getUserId(identity);
+
+ Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
+ if (screenerOpt.isEmpty()) {
+ throw new NotFoundException();
+ }
+ Screener screener = screenerOpt.get();
+
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+ }
+
+ Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
+ if (benefitOpt.isEmpty()) {
+ throw new NotFoundException();
+ }
+
+ List checks =
+ eligibilityCheckRepository.getChecksInBenefit(benefitOpt.get());
+ return Response.ok().entity(checks).build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not fetch checks"))
+ .build();
+ }
+ }
+
+ @POST
+ @Path("/screener/{screenerId}/benefit")
+ public Response addCustomBenefit(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ Benefit newBenefit) {
+ String userId = AuthUtils.getUserId(identity);
+
+ newBenefit.setOwnerId(userId);
+ newBenefit.setChecks(Collections.emptyList());
+
+ BenefitDetail benefitDetail = new BenefitDetail();
+ benefitDetail.setId(newBenefit.getId());
+ benefitDetail.setName(newBenefit.getName());
+ benefitDetail.setDescription(newBenefit.getDescription());
+ benefitDetail.setPublic(newBenefit.getPublic());
+ try {
+ // Check to make sure not introducing duplicates
+ Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
+ if (screenerOpt.isEmpty()) {
+ Log.error("Screener not found. Screener ID:" + screenerId);
+ throw new NotFoundException();
+ }
+
+ Screener screener = screenerOpt.get();
+
+ // Authorise action
+ if (userId != null && !isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+ }
+
+ List benefits = screenerOpt.get().getBenefits();
+ if (benefits == null) {
+ benefits = Collections.emptyList();
+ }
+ Boolean benefitIdExists =
+ !benefits.stream()
+ .filter(benefit -> benefit.getId().equals(benefitDetail.getId()))
+ .toList()
+ .isEmpty();
+
+ if (benefitIdExists) {
+ return Response.status(
+ Response.Status.CONFLICT.getStatusCode(),
+ "Benefit with provided ID already exists on screener.")
+ .build();
+ }
+
+ String benefitId = screenerRepository.saveNewCustomBenefit(screenerId, newBenefit);
+ screenerRepository.addBenefitDetailToWorkingScreener(screenerId, benefitDetail);
+ newBenefit.setId(benefitId);
+ return Response.ok(newBenefit, MediaType.APPLICATION_JSON).build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not save benefit"))
+ .build();
}
+ }
- @PUT
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/screener/{screenerId}/benefit")
- public Response updateCustomBenefit(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId,
- Benefit updatedBenefit) {
- String userId = AuthUtils.getUserId(identity);
-
- // TODO: Add validations for user provided data
-
- if (!isUserAuthorizedToAccessScreener(userId, screenerId)) {
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- try {
- Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, updatedBenefit.getId());
- if (benefitOpt.isEmpty()) {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
-
- screenerRepository.updateCustomBenefit(screenerId, updatedBenefit);
- return Response.ok(updatedBenefit, MediaType.APPLICATION_JSON).build();
- } catch (Exception e) {
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not update custom benefit"))
- .build();
- }
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Path("/screener/{screenerId}/benefit")
+ public Response updateCustomBenefit(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ Benefit updatedBenefit) {
+ String userId = AuthUtils.getUserId(identity);
+
+ // TODO: Add validations for user provided data
+
+ if (!isUserAuthorizedToAccessScreener(userId, screenerId)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
}
- @DELETE
- @Path("/screener/{screenerId}/benefit/{benefitId}")
- public Response deleteCustomBenefit(@Context SecurityIdentity identity,
- @PathParam("screenerId") String screenerId,
- @PathParam("benefitId") String benefitId) {
- try {
- // Check if Screener and Benefit exist
- Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
- Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
- if (screenerOpt.isEmpty()){
- throw new NotFoundException();
- }
- if (benefitOpt.isEmpty()) {
- throw new NotFoundException();
- }
-
- // Confirm user is authorized to make the change
- String userId = AuthUtils.getUserId(identity);
- Screener screener = screenerOpt.get();
- if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){
- return Response.status(Response.Status.UNAUTHORIZED).build();
- }
-
- // Delete the benefit and remove the benefitDetail from the screener
- screenerRepository.deleteCustomBenefit(screenerId, benefitId);
- List updatedBenefits = screener.getBenefits()
- .stream()
- .filter(benefitDetail -> !benefitDetail.getId().equals(benefitId))
- .toList();
- screener.setBenefits(updatedBenefits);
- screenerRepository.updateWorkingScreener(screener);
-
- return Response.ok().build();
- } catch (Exception e) {
- Log.error(e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity(Map.of("error", "Could not delete custom benefit"))
- .build();
- }
+ try {
+ Optional benefitOpt =
+ screenerRepository.getCustomBenefit(screenerId, updatedBenefit.getId());
+ if (benefitOpt.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ screenerRepository.updateCustomBenefit(screenerId, updatedBenefit);
+ return Response.ok(updatedBenefit, MediaType.APPLICATION_JSON).build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not update custom benefit"))
+ .build();
+ }
+ }
+
+ @DELETE
+ @Path("/screener/{screenerId}/benefit/{benefitId}")
+ public Response deleteCustomBenefit(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ @PathParam("benefitId") String benefitId) {
+ try {
+ // Check if Screener and Benefit exist
+ Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId);
+ Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId);
+ if (screenerOpt.isEmpty()) {
+ throw new NotFoundException();
+ }
+ if (benefitOpt.isEmpty()) {
+ throw new NotFoundException();
+ }
+
+ // Confirm user is authorized to make the change
+ String userId = AuthUtils.getUserId(identity);
+ Screener screener = screenerOpt.get();
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+ }
+
+ // Delete the benefit and remove the benefitDetail from the screener
+ screenerRepository.deleteCustomBenefit(screenerId, benefitId);
+ List updatedBenefits =
+ screener.getBenefits().stream()
+ .filter(benefitDetail -> !benefitDetail.getId().equals(benefitId))
+ .toList();
+ screener.setBenefits(updatedBenefits);
+ screenerRepository.updateWorkingScreener(screener);
+
+ return Response.ok().build();
+ } catch (Exception e) {
+ Log.error(e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(Map.of("error", "Could not delete custom benefit"))
+ .build();
}
+ }
}
diff --git a/builder-api/src/main/java/org/acme/model/domain/Screener.java b/builder-api/src/main/java/org/acme/model/domain/Screener.java
index b6439548..fcf59f34 100644
--- a/builder-api/src/main/java/org/acme/model/domain/Screener.java
+++ b/builder-api/src/main/java/org/acme/model/domain/Screener.java
@@ -26,7 +26,16 @@ public Screener(Map model) {
this.formSchema = model;
}
- public Screener(){
+ public Screener() {
+ }
+
+ /* Domain creation for POST */
+ public static Screener create(String ownerId, String screenerName, String description) {
+ Screener s = new Screener();
+ s.ownerId = ownerId;
+ s.screenerName = screenerName;
+
+ return s;
}
public Map getFormSchema() {
@@ -37,55 +46,55 @@ public void setFormSchema(Map formSchema) {
this.formSchema = formSchema;
}
- public void setOwnerId(String ownerId){
+ public void setOwnerId(String ownerId) {
this.ownerId = ownerId;
}
- public String getOwnerId(){
+ public String getOwnerId() {
return this.ownerId;
}
- public void setScreenerName(String screenerName){
+ public void setScreenerName(String screenerName) {
this.screenerName = screenerName;
}
- public String getScreenerName(){
+ public String getScreenerName() {
return this.screenerName;
}
- public void setLastPublishDate(String lastPublishDate){
+ public void setLastPublishDate(String lastPublishDate) {
this.lastPublishDate = lastPublishDate;
}
- public void setId(String id){
+ public void setId(String id) {
this.id = id;
}
- public String getId(){
+ public String getId() {
return this.id;
}
- public String getOrganizationName(){
+ public String getOrganizationName() {
return this.organizationName;
}
- public void setOrganizationName(String organizationName){
+ public void setOrganizationName(String organizationName) {
this.organizationName = organizationName;
}
- public void setPublishedScreenerId(String publishedScreenerId){
+ public void setPublishedScreenerId(String publishedScreenerId) {
this.publishedScreenerId = publishedScreenerId;
}
- public String getPublishedScreenerId(){
+ public String getPublishedScreenerId() {
return this.publishedScreenerId;
}
- public void setLastPublishedDate(String lastPublishDate){
+ public void setLastPublishedDate(String lastPublishDate) {
this.lastPublishDate = lastPublishDate;
}
- public String getLastPublishDate(){
+ public String getLastPublishDate() {
return this.lastPublishDate;
}
diff --git a/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java b/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java
new file mode 100644
index 00000000..b0ec9127
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java
@@ -0,0 +1,8 @@
+package org.acme.model.dto.Screener;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record CreateScreenerRequest(
+ @NotBlank(message = "screenerName must be provided.") String screenerName,
+ String description) {
+}
\ No newline at end of file
diff --git a/builder-api/src/main/resources/application.properties b/builder-api/src/main/resources/application.properties
index 5126db2a..4fe60282 100644
--- a/builder-api/src/main/resources/application.properties
+++ b/builder-api/src/main/resources/application.properties
@@ -14,3 +14,6 @@ quarkus.http.auth.permission.secured.policy=authenticated
quarkus.http.auth.permission.public.paths=/api/published/*
quarkus.http.auth.permission.public.policy=permit
+
+# Reject requests if extra properties are sent
+quarkus.jackson.fail-on-unknown-properties=true
\ No newline at end of file
From f81a25b7ec64e28b559e10667aeeb256b3d59a05 Mon Sep 17 00:00:00 2001
From: joshwanf <17016446+joshwanf@users.noreply.github.com>
Date: Wed, 21 Jan 2026 17:18:44 -0500
Subject: [PATCH 2/2] PATCH /api/screener/screenerId, added validation and
serialization error messages to EditScreener and SaveFormSchema endpoints
---
.../java/org/acme/api/error/ApiError.java | 7 +
.../api/error/JsonServerExceptionMappers.java | 60 ++++++
.../error/UnknownFieldExceptionMapper.java | 18 +-
.../api/error/ValidationExceptionMapper.java | 45 ++++
.../api/validation/AtLeastOneProvided.java | 18 ++
.../AtLeastOneProvidedValidator.java | 42 ++++
.../org/acme/api/validation/HasSchema.java | 7 +
.../acme/api/validation/SchemaValidator.java | 45 ++++
.../org/acme/api/validation/ValidSchema.java | 21 ++
.../org/acme/controller/ScreenerResource.java | 94 ++++++---
.../java/org/acme/model/domain/Screener.java | 194 ++++++++----------
.../org/acme/model/dto/SaveSchemaRequest.java | 8 +-
.../dto/Screener/EditScreenerRequest.java | 6 +
.../src/main/resources/application.properties | 2 +-
builder-frontend/src/api/screener.ts | 54 +++--
.../homeScreen/EditScreenerForm.jsx | 3 +-
.../components/homeScreen/ProjectsList.tsx | 12 +-
17 files changed, 462 insertions(+), 174 deletions(-)
create mode 100644 builder-api/src/main/java/org/acme/api/error/ApiError.java
create mode 100644 builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java
create mode 100644 builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java
create mode 100644 builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java
create mode 100644 builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java
create mode 100644 builder-api/src/main/java/org/acme/api/validation/HasSchema.java
create mode 100644 builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java
create mode 100644 builder-api/src/main/java/org/acme/api/validation/ValidSchema.java
create mode 100644 builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java
diff --git a/builder-api/src/main/java/org/acme/api/error/ApiError.java b/builder-api/src/main/java/org/acme/api/error/ApiError.java
new file mode 100644
index 00000000..7a6fba48
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/error/ApiError.java
@@ -0,0 +1,7 @@
+package org.acme.api.error;
+
+public record ApiError(boolean error, String message) {
+ public static ApiError of(String message) {
+ return new ApiError(true, message);
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java b/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java
new file mode 100644
index 00000000..85fe6cdc
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java
@@ -0,0 +1,60 @@
+package org.acme.api.error;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
+
+public class JsonServerExceptionMappers {
+
+ // @ServerExceptionMapper
+ // public Response map(UnrecognizedPropertyException e) {
+ // return Response.status(Response.Status.BAD_REQUEST)
+ // .type(MediaType.APPLICATION_JSON)
+ // .entity(Map.of("error", true, "message", "Unknown fields " + e.getPropertyName()))
+ // .build();
+ // }
+
+ @ServerExceptionMapper
+ public Response map(MismatchedInputException e) {
+ // e.g. screenerName is object but DTO expects String
+ String field =
+ e.getPath() != null && !e.getPath().isEmpty()
+ ? e.getPath().get(e.getPath().size() - 1).getFieldName()
+ : "request body";
+
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of("Invalid type for field '" + field + "'."))
+ .build();
+ }
+
+ @ServerExceptionMapper
+ public Response map(JsonParseException e) {
+ // malformed JSON like { "schema": }
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of("Malformed JSON."))
+ .build();
+ }
+
+ @ServerExceptionMapper
+ public Response map(WebApplicationException e) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of("Malformed JSON."))
+ .build();
+ }
+
+ @ServerExceptionMapper
+ public Response map(JsonMappingException e) {
+ // other mapping errors
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of("Invalid request body."))
+ .build();
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
index 0aa26b25..877d5f51 100644
--- a/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
+++ b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java
@@ -6,20 +6,16 @@
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
-import java.util.Map;
-
// Global error handler for providing extra fields in the request body
@Provider
public class UnknownFieldExceptionMapper implements ExceptionMapper {
- @Override
- public Response toResponse(UnrecognizedPropertyException e) {
- return Response.status(Response.Status.BAD_REQUEST)
- .type(MediaType.APPLICATION_JSON)
- .entity(Map.of(
- "error", true,
- "message", "Unknown field " + e.getPropertyName()))
- .build();
- }
+ @Override
+ public Response toResponse(UnrecognizedPropertyException e) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of("Unknown field '" + e.getPropertyName() + "'"))
+ .build();
+ }
}
diff --git a/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java b/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java
new file mode 100644
index 00000000..f9631fc8
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java
@@ -0,0 +1,45 @@
+package org.acme.api.error;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+import java.util.stream.Collectors;
+import org.jboss.logging.Logger;
+
+@Provider
+public class ValidationExceptionMapper implements ExceptionMapper {
+
+ private static final Logger LOG = Logger.getLogger(ValidationExceptionMapper.class);
+
+ @Override
+ public Response toResponse(ConstraintViolationException e) {
+ // Log all violations (since endpoint method won't run)
+ String detail =
+ e.getConstraintViolations().stream()
+ .map(
+ v ->
+ v.getPropertyPath()
+ + ": "
+ + v.getMessage()
+ + " (invalid="
+ + String.valueOf(v.getInvalidValue())
+ + ")")
+ .collect(Collectors.joining("; "));
+
+ LOG.warn("Validation failed: " + detail);
+
+ String msg =
+ e.getConstraintViolations().stream()
+ .findFirst()
+ .map(ConstraintViolation::getMessage)
+ .orElse("Validation failed.");
+
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(ApiError.of(msg))
+ .build();
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java
new file mode 100644
index 00000000..a5d3c76c
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java
@@ -0,0 +1,18 @@
+package org.acme.api.validation;
+
+import jakarta.validation.*;
+import java.lang.annotation.*;
+import java.lang.annotation.Retention;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = AtLeastOneProvidedValidator.class)
+public @interface AtLeastOneProvided {
+ String message() default "At least one field must be provided";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ String[] fields(); // names of fields to check
+}
diff --git a/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java
new file mode 100644
index 00000000..8d807f14
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java
@@ -0,0 +1,42 @@
+package org.acme.api.validation;
+
+import jakarta.validation.*;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+public class AtLeastOneProvidedValidator
+ implements ConstraintValidator {
+
+ private String[] fields;
+
+ @Override
+ public void initialize(AtLeastOneProvided ann) {
+ this.fields = ann.fields();
+ }
+
+ @Override
+ public boolean isValid(Object value, ConstraintValidatorContext ctx) {
+ if (value == null) return true;
+
+ Class> cls = value.getClass();
+
+ for (String fieldName : fields) {
+ try {
+ // Works for records + POJOs
+ Method accessor = cls.getMethod(fieldName);
+ Object fieldValue = accessor.invoke(value);
+
+ if (fieldValue instanceof String s && !s.isBlank()) return true;
+ if (fieldValue instanceof Map, ?> m && !m.isEmpty()) return true;
+ if (fieldValue != null) return true;
+
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("No accessor '" + fieldName + "()' on " + cls.getName(), e);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/api/validation/HasSchema.java b/builder-api/src/main/java/org/acme/api/validation/HasSchema.java
new file mode 100644
index 00000000..7ce91126
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/validation/HasSchema.java
@@ -0,0 +1,7 @@
+package org.acme.api.validation;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public interface HasSchema {
+ JsonNode schema();
+}
diff --git a/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java b/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java
new file mode 100644
index 00000000..2ef37651
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java
@@ -0,0 +1,45 @@
+package org.acme.api.validation;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+public class SchemaValidator implements ConstraintValidator {
+
+ private boolean required;
+ private boolean mustBeObject;
+
+ @Override
+ public void initialize(ValidSchema ann) {
+ this.required = ann.required();
+ this.mustBeObject = ann.mustBeObject();
+ }
+
+ @Override
+ public boolean isValid(HasSchema value, ConstraintValidatorContext ctx) {
+ if (value == null) return true;
+
+ JsonNode schema = value.schema();
+
+ // Priority 1: required check (null or JSON null)
+ if (required && (schema == null || schema.isNull())) {
+ return fail(ctx, "schema cannot be null", "schema");
+ }
+
+ // If not required and absent, it's valid
+ if (schema == null || schema.isNull()) return true;
+
+ // Priority 2: type check
+ if (mustBeObject && !schema.isObject()) {
+ return fail(ctx, "schema must be a JSON object", "schema");
+ }
+
+ return true;
+ }
+
+ private static boolean fail(ConstraintValidatorContext ctx, String msg, String node) {
+ ctx.disableDefaultConstraintViolation();
+ ctx.buildConstraintViolationWithTemplate(msg).addPropertyNode(node).addConstraintViolation();
+ return false;
+ }
+}
diff --git a/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java b/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java
new file mode 100644
index 00000000..c34c9dee
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java
@@ -0,0 +1,21 @@
+package org.acme.api.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.*;
+
+@Documented
+@Constraint(validatedBy = SchemaValidator.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValidSchema {
+ String message() default "Invalid schema";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ boolean required() default true; // reject null/NullNode
+
+ boolean mustBeObject() default true; // reject non-object
+}
diff --git a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
index 0e5abc72..022e15bc 100644
--- a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
+++ b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java
@@ -1,11 +1,12 @@
package org.acme.controller;
-import com.fasterxml.jackson.databind.JsonNode;
import io.quarkus.logging.Log;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
+import jakarta.validation.Validator;
+import jakarta.validation.constraints.NotBlank;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
@@ -19,6 +20,7 @@
import org.acme.model.dto.PublishScreenerRequest;
import org.acme.model.dto.SaveSchemaRequest;
import org.acme.model.dto.Screener.CreateScreenerRequest;
+import org.acme.model.dto.Screener.EditScreenerRequest;
import org.acme.persistence.EligibilityCheckRepository;
import org.acme.persistence.PublishedScreenerRepository;
import org.acme.persistence.ScreenerRepository;
@@ -28,6 +30,8 @@
@Path("/api")
public class ScreenerResource {
+ @Inject Validator validator;
+
@Inject ScreenerRepository screenerRepository;
@Inject PublishedScreenerRepository publishedScreenerRepository;
@@ -104,21 +108,36 @@ public Response postScreener(
}
}
- @PUT
+ @PATCH
@Consumes(MediaType.APPLICATION_JSON)
- @Path("/screener")
- public Response updateScreener(@Context SecurityIdentity identity, Screener screener) {
+ @Path("/screener/{screenerId}")
+ public Response updateScreener(
+ @Context SecurityIdentity identity,
+ @PathParam("screenerId") String screenerId,
+ @Valid EditScreenerRequest request) {
String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, screener.getId()))
+
+ // Fetch Screener record and confirm user is authorized
+ Optional maybeScreener = screenerRepository.getWorkingScreener(screenerId);
+ if (maybeScreener.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ Screener screener = maybeScreener.get();
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
+ }
- // add user info to the update data
- screener.setOwnerId(userId);
+ Log.info(request.toString());
+
+ // Update Screener fields from request
+ if (request.screenerName() != null) {
+ screener.setScreenerName(request.screenerName());
+ }
try {
screenerRepository.updateWorkingScreener(screener);
-
- return Response.ok().build();
+ return Response.ok(screener, MediaType.APPLICATION_JSON).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Could not update Screener"))
@@ -130,29 +149,50 @@ public Response updateScreener(@Context SecurityIdentity identity, Screener scre
@Consumes(MediaType.APPLICATION_JSON)
@Path("/save-form-schema")
public Response saveFormSchema(
- @Context SecurityIdentity identity, SaveSchemaRequest saveSchemaRequest) {
-
- String screenerId = saveSchemaRequest.screenerId;
- if (screenerId == null || screenerId.isBlank()) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required required data in request body: screenerId")
- .build();
+ @Context SecurityIdentity identity,
+ @QueryParam("screenerId") @NotBlank(message = "Must provide screenerId") String screenerId,
+ @Valid SaveSchemaRequest request) {
+
+ Log.info(
+ "schema node = "
+ + (request == null
+ ? "request=null"
+ : request.schema() == null
+ ? "schema=null"
+ : request.schema().getNodeType() + " : " + request.schema().toString()));
+
+ var violations = validator.validate(request);
+ if (!violations.isEmpty()) {
+ return Response.status(400).entity(violations.toString()).build();
}
+ // // Make sure request.schema is not null
+ // if (request.schema() == null || request.schema().isNull()) {
+ // return Response.status(Response.Status.BAD_REQUEST)
+ // .entity(ApiError.of("schema cannot be null."))
+ // .build();
+ // }
+
String userId = AuthUtils.getUserId(identity);
- if (!isUserAuthorizedToAccessScreener(userId, saveSchemaRequest.screenerId))
- return Response.status(Response.Status.UNAUTHORIZED).build();
- JsonNode schema = saveSchemaRequest.schema;
- if (schema == null) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Error: Missing required required data in request body: screenerId")
+ // Fetch Screener record and confirm user is authorized
+ Optional maybeScreener = screenerRepository.getWorkingScreener(screenerId);
+ if (maybeScreener.isEmpty()) {
+ return Response.status(Response.Status.NOT_FOUND)
+ .entity(Map.of("error", true, "message", "Screener " + screenerId + " cannot be found."))
.build();
}
+
+ Screener screener = maybeScreener.get();
+ if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) {
+ return Response.status(Response.Status.UNAUTHORIZED)
+ .entity(Map.of("error", true, "message", "Unauthorized access to the screener."))
+ .build();
+ }
+
try {
String filePath = storageService.getScreenerWorkingFormSchemaPath(screenerId);
- storageService.writeJsonToStorage(filePath, schema);
- Log.info("Saved form schema of screener " + screenerId + " to storage");
+ storageService.writeJsonToStorage(filePath, request.schema());
return Response.ok().build();
} catch (Exception e) {
Log.info(("Failed to save form for screener " + screenerId));
@@ -225,11 +265,7 @@ private boolean isUserAuthorizedToAccessScreener(String userId, String screenerI
}
private boolean isUserAuthorizedToAccessScreenerByScreener(String userId, Screener screener) {
- String ownerId = screener.getOwnerId();
- if (userId.equals(ownerId)) {
- return true;
- }
- return false;
+ return userId.equals(screener.getOwnerId());
}
@GET
diff --git a/builder-api/src/main/java/org/acme/model/domain/Screener.java b/builder-api/src/main/java/org/acme/model/domain/Screener.java
index fcf59f34..9ee2fed3 100644
--- a/builder-api/src/main/java/org/acme/model/domain/Screener.java
+++ b/builder-api/src/main/java/org/acme/model/domain/Screener.java
@@ -1,116 +1,96 @@
package org.acme.model.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Screener {
- /* Screener metadata */
- private String id;
- private String ownerId;
- private String screenerName;
- private String organizationName;
-
- /* Screener data */
- private Map formSchema;
- private List resultsSchema;
- private List benefits;
-
- /* Publishing properties */
- private String publishedScreenerId;
- private String lastPublishDate;
-
- public Screener(Map model) {
- this.formSchema = model;
- }
-
- public Screener() {
- }
-
- /* Domain creation for POST */
- public static Screener create(String ownerId, String screenerName, String description) {
- Screener s = new Screener();
- s.ownerId = ownerId;
- s.screenerName = screenerName;
-
- return s;
- }
-
- public Map getFormSchema() {
- return formSchema;
- }
-
- public void setFormSchema(Map formSchema) {
- this.formSchema = formSchema;
- }
-
- public void setOwnerId(String ownerId) {
- this.ownerId = ownerId;
- }
-
- public String getOwnerId() {
- return this.ownerId;
- }
-
- public void setScreenerName(String screenerName) {
- this.screenerName = screenerName;
- }
-
- public String getScreenerName() {
- return this.screenerName;
- }
-
- public void setLastPublishDate(String lastPublishDate) {
- this.lastPublishDate = lastPublishDate;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getId() {
- return this.id;
- }
-
- public String getOrganizationName() {
- return this.organizationName;
- }
-
- public void setOrganizationName(String organizationName) {
- this.organizationName = organizationName;
- }
-
- public void setPublishedScreenerId(String publishedScreenerId) {
- this.publishedScreenerId = publishedScreenerId;
- }
-
- public String getPublishedScreenerId() {
- return this.publishedScreenerId;
- }
-
- public void setLastPublishedDate(String lastPublishDate) {
- this.lastPublishDate = lastPublishDate;
- }
-
- public String getLastPublishDate() {
- return this.lastPublishDate;
- }
-
- public List getResultsSchema() {
- return resultsSchema;
- }
-
- public void setResultsSchema(List resultsSchema) {
- this.resultsSchema = resultsSchema;
- }
-
- public List getBenefits() {
- return benefits;
- }
-
- public void setBenefits(List benefits) {
- this.benefits = benefits;
- }
+ /* Screener metadata */
+ private String id;
+ private String ownerId;
+ private String screenerName;
+
+ /* Screener data */
+ private Map formSchema;
+ private List benefits;
+
+ /* Publishing properties */
+ private String publishedScreenerId;
+ private String lastPublishDate;
+
+ public Screener(Map model) {
+ this.formSchema = model;
+ }
+
+ public Screener() {}
+
+ /* Domain creation for POST */
+ public static Screener create(String ownerId, String screenerName, String description) {
+ Screener s = new Screener();
+ s.ownerId = ownerId;
+ s.screenerName = screenerName;
+
+ return s;
+ }
+
+ public Map getFormSchema() {
+ return formSchema;
+ }
+
+ public void setFormSchema(Map formSchema) {
+ this.formSchema = formSchema;
+ }
+
+ public void setOwnerId(String ownerId) {
+ this.ownerId = ownerId;
+ }
+
+ public String getOwnerId() {
+ return this.ownerId;
+ }
+
+ public void setScreenerName(String screenerName) {
+ this.screenerName = screenerName;
+ }
+
+ public String getScreenerName() {
+ return this.screenerName;
+ }
+
+ public void setLastPublishDate(String lastPublishDate) {
+ this.lastPublishDate = lastPublishDate;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setPublishedScreenerId(String publishedScreenerId) {
+ this.publishedScreenerId = publishedScreenerId;
+ }
+
+ public String getPublishedScreenerId() {
+ return this.publishedScreenerId;
+ }
+
+ public void setLastPublishedDate(String lastPublishDate) {
+ this.lastPublishDate = lastPublishDate;
+ }
+
+ public String getLastPublishDate() {
+ return this.lastPublishDate;
+ }
+
+ public List getBenefits() {
+ return benefits;
+ }
+
+ public void setBenefits(List benefits) {
+ this.benefits = benefits;
+ }
}
diff --git a/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java b/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java
index 4724484e..8eacbb95 100644
--- a/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java
+++ b/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java
@@ -1,8 +1,8 @@
package org.acme.model.dto;
import com.fasterxml.jackson.databind.JsonNode;
+import org.acme.api.validation.HasSchema;
+import org.acme.api.validation.ValidSchema;
-public class SaveSchemaRequest {
- public String screenerId;
- public JsonNode schema;
-}
+@ValidSchema(required = true, mustBeObject = true)
+public record SaveSchemaRequest(JsonNode schema) implements HasSchema {}
diff --git a/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java b/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java
new file mode 100644
index 00000000..915b1385
--- /dev/null
+++ b/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java
@@ -0,0 +1,6 @@
+package org.acme.model.dto.Screener;
+
+import org.acme.api.validation.AtLeastOneProvided;
+
+@AtLeastOneProvided(fields = {"screenerName"})
+public record EditScreenerRequest(String screenerName) {}
diff --git a/builder-api/src/main/resources/application.properties b/builder-api/src/main/resources/application.properties
index 4fe60282..37212871 100644
--- a/builder-api/src/main/resources/application.properties
+++ b/builder-api/src/main/resources/application.properties
@@ -1,6 +1,6 @@
quarkus.http.cors.enabled=true
quarkus.http.cors.origins=*
-quarkus.http.cors.methods=GET,POST,PUT,DELETE
+quarkus.http.cors.methods=GET,POST,PUT,PATCH,DELETE
quarkus.http.cors.headers=Authorization,Content-Type
quarkus.datasource.db-kind=sqlite
diff --git a/builder-frontend/src/api/screener.ts b/builder-frontend/src/api/screener.ts
index 8c3fb8f4..26f3906e 100644
--- a/builder-frontend/src/api/screener.ts
+++ b/builder-frontend/src/api/screener.ts
@@ -46,7 +46,10 @@ export const fetchProject = async (screenerId) => {
}
};
-export const createNewScreener = async (screenerData) => {
+export const createNewScreener = async (request: {
+ screenerName: string;
+ description?: string;
+}) => {
const url = apiUrl + "/screener";
try {
const response = await authFetch(url, {
@@ -55,7 +58,7 @@ export const createNewScreener = async (screenerData) => {
"Content-Type": "application/json",
Accept: "application/json",
},
- body: JSON.stringify(screenerData),
+ body: JSON.stringify(request),
});
if (!response.ok) {
@@ -69,20 +72,25 @@ export const createNewScreener = async (screenerData) => {
}
};
-export const updateScreener = async (screenerData) => {
- const url = apiUrl + "/screener";
+export const updateScreener = async (
+ screenerId: string,
+ request: { screenerName: string },
+) => {
+ const url = new URL(`${apiUrl}/screener/${screenerId}`);
+
try {
- const response = await authFetch(url, {
- method: "PUT",
+ const response = await authFetch(url.toString(), {
+ method: "PATCH",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
- body: JSON.stringify(screenerData),
+ body: JSON.stringify(request),
});
if (!response.ok) {
- throw new Error(`Update failed with status: ${response.status}`);
+ const err = await response.json();
+ throw new Error(err);
}
} catch (error) {
console.error("Error updating project:", error);
@@ -110,13 +118,14 @@ export const deleteScreener = async (screenerData) => {
}
};
-export const saveFormSchema = async (screenerId, schema) => {
+export const saveFormSchema = async (screenerId: string, schema) => {
const requestData: any = {};
- requestData.screenerId = screenerId;
requestData.schema = schema;
- const url = apiUrl + "/save-form-schema";
+ const url = new URL(`${apiUrl}/save-form-schema`);
+ url.searchParams.append("screenerId", screenerId);
+
try {
- const response = await authFetch(url, {
+ const response = await authFetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -155,7 +164,10 @@ export const publishScreener = async (screenerId: string): Promise => {
}
};
-export const addCustomBenefit = async (screenerId: string, benefit: BenefitDetail) => {
+export const addCustomBenefit = async (
+ screenerId: string,
+ benefit: BenefitDetail,
+) => {
const url = apiUrl + "/screener/" + screenerId + "/benefit";
try {
const response = await authFetch(url, {
@@ -176,7 +188,10 @@ export const addCustomBenefit = async (screenerId: string, benefit: BenefitDetai
}
};
-export const removeCustomBenefit = async (screenerId: string, benefitId: string) => {
+export const removeCustomBenefit = async (
+ screenerId: string,
+ benefitId: string,
+) => {
const url = apiUrl + "/screener/" + screenerId + "/benefit/" + benefitId;
try {
const response = await authFetch(url, {
@@ -188,7 +203,9 @@ export const removeCustomBenefit = async (screenerId: string, benefitId: string)
});
if (!response.ok) {
- throw new Error(`Delete of benefit failed with status: ${response.status}`);
+ throw new Error(
+ `Delete of benefit failed with status: ${response.status}`,
+ );
}
} catch (error) {
console.error("Error deleting custom benefit:", error);
@@ -196,7 +213,10 @@ export const removeCustomBenefit = async (screenerId: string, benefitId: string)
}
};
-export const evaluateScreener = async (screenerId: string, inputData: any): Promise => {
+export const evaluateScreener = async (
+ screenerId: string,
+ inputData: any,
+): Promise => {
const url = apiUrl + "/decision/v2?screenerId=" + screenerId;
try {
const response = await authFetch(url, {
@@ -211,7 +231,7 @@ export const evaluateScreener = async (screenerId: string, inputData: any): Prom
if (!response.ok) {
throw new Error(`Evaluation failed with status: ${response.status}`);
}
-
+
const data = await response.json();
return data;
} catch (error) {
diff --git a/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx b/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx
index 71256ca3..0aa18ca5 100644
--- a/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx
+++ b/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx
@@ -29,9 +29,8 @@ export default function EditScreenerForm({
setIsLoading(true);
const data = {
screenerName: screenerName(),
- id: screenerData.id,
};
- await handleEditScreener(data);
+ await handleEditScreener(screenerData.id, data);
if (isActive) setIsLoading(false);
} catch (e) {
if (setIsLoading()) {
diff --git a/builder-frontend/src/components/homeScreen/ProjectsList.tsx b/builder-frontend/src/components/homeScreen/ProjectsList.tsx
index 9d038e9a..a8ed1e7d 100644
--- a/builder-frontend/src/components/homeScreen/ProjectsList.tsx
+++ b/builder-frontend/src/components/homeScreen/ProjectsList.tsx
@@ -33,7 +33,10 @@ export default function ProjectsList() {
navigate("/project/" + project.id);
};
- const handleCreateNewScreener = async (screenerData) => {
+ const handleCreateNewScreener = async (screenerData: {
+ screenerName: string;
+ description?: string | undefined;
+ }) => {
try {
const newScreener = await createNewScreener(screenerData);
navigate(`/project/${newScreener.id}`);
@@ -48,9 +51,12 @@ export default function ProjectsList() {
setIsEditgModalVisible(true);
};
- const handleUpdateScreener = async (screenerData) => {
+ const handleUpdateScreener = async (
+ screenerId: string,
+ screenerData: { screenerName: string },
+ ) => {
try {
- await updateScreener(screenerData);
+ await updateScreener(screenerId, screenerData);
refetchProjectList();
setIsEditgModalVisible(false);
} catch (e) {