/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.services; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.curisit.integrity.commons.Utils; import net.curisit.securis.DefaultExceptionHandler; import net.curisit.securis.db.Application; import net.curisit.securis.db.ApplicationMetadata; import net.curisit.securis.db.User.Rol; import net.curisit.securis.ioc.EnsureTransaction; import net.curisit.securis.security.BasicSecurityContext; import net.curisit.securis.security.Securable; import net.curisit.securis.services.exception.SeCurisServiceException; import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes; import net.curisit.securis.services.helpers.MetadataHelper; import net.curisit.securis.utils.TokenHelper; /** * ApplicationResource *

* REST endpoints to list, fetch, create, update and delete {@link Application}s. * Security: *

* Side-effects: * * * Author: roberto <roberto.sanchez@curisit.net>
* Last reviewed by JRA on Oct 5, 2025. */ @Path("/application") public class ApplicationResource { @Inject TokenHelper tokenHelper; @Inject MetadataHelper metadataHelper; @Context EntityManager em; private static final Logger LOG = LogManager.getLogger(ApplicationResource.class); /** * ApplicationResource

* Constructor */ public ApplicationResource() {} /** * index

* List applications visible to the current user. * * @param bsc security context * @return 200 with list (possibly empty) or 200 empty if user has no app scope */ @GET @Path("/") @Produces({ MediaType.APPLICATION_JSON }) @Securable public Response index(@Context BasicSecurityContext bsc) { LOG.info("Getting applications list "); em.clear(); TypedQuery q; if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) { q = em.createNamedQuery("list-applications", Application.class); } else { if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) { return Response.ok().build(); } q = em.createNamedQuery("list-applications-by_ids", Application.class); q.setParameter("list_ids", bsc.getApplicationsIds()); } List list = q.getResultList(); return Response.ok(list).build(); } /** * get

* Fetch a single application by ID. * * @param appid string ID * @return 200 + entity or 404 if not found * @throws SeCurisServiceException when ID is invalid or not found */ @GET @Path("/{appid}") @Produces({ MediaType.APPLICATION_JSON }) @Securable public Response get(@PathParam("appid") String appid) throws SeCurisServiceException { LOG.info("Getting application data for id: {}: ", appid); if (appid == null || "".equals(appid)) { LOG.error("Application ID is mandatory"); return Response.status(Status.NOT_FOUND).build(); } em.clear(); Application app = null; try { LOG.info("READY to GET app: {}", appid); app = em.find(Application.class, Integer.parseInt(appid)); } catch (Exception e) { LOG.info("ERROR GETTING app: {}", e); } if (app == null) { LOG.error("Application with id {} not found in DB", appid); throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Application not found with ID: " + appid); } return Response.ok(app).build(); } /** * create

* Create a new application with optional metadata entries. * * @param app application payload * @param token auth token (audited externally) * @return 200 + persisted entity */ @POST @Path("/") @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) @EnsureTransaction @Securable(roles = Rol.ADMIN) @RolesAllowed(BasicSecurityContext.ROL_ADMIN) public Response create(Application app, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) { LOG.info("Creating new application"); app.setCreationTimestamp(new Date()); em.persist(app); if (app.getApplicationMetadata() != null) { for (ApplicationMetadata md : app.getApplicationMetadata()) { md.setApplication(app); md.setCreationTimestamp(new Date()); em.persist(md); } } LOG.info("Creating application ({}) with date: {}", app.getId(), app.getCreationTimestamp()); return Response.ok(app).build(); } /** * modify

* Update core fields and reconcile metadata set: *

* * @param appid path ID * @param app new state */ @PUT @POST @Path("/{appid}") @EnsureTransaction @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) @Securable(roles = Rol.ADMIN) @RolesAllowed(BasicSecurityContext.ROL_ADMIN) public Response modify(Application app, @PathParam("appid") String appid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) { LOG.info("Modifying application with id: {}", appid); Application currentapp = em.find(Application.class, Integer.parseInt(appid)); if (currentapp == null) { LOG.error("Application with id {} not found in DB", appid); return Response.status(Status.NOT_FOUND) .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid) .build(); } currentapp.setCode(app.getCode()); currentapp.setName(app.getName()); currentapp.setLicenseFilename(app.getLicenseFilename()); currentapp.setDescription(app.getDescription()); Set newMD = app.getApplicationMetadata(); Set oldMD = currentapp.getApplicationMetadata(); boolean metadataChanges = !metadataHelper.match(newMD, oldMD); if (metadataChanges) { Map directOldMD = getMapMD(oldMD); Map directNewMD = getMapMD(newMD); // Remove deleted MD for (ApplicationMetadata currentMd : oldMD) { if (newMD == null || !directNewMD.containsKey(currentMd.getKey())) { em.remove(currentMd); } } // Merge or persist if (newMD != null) { for (ApplicationMetadata md : newMD) { if (directOldMD.containsKey(md.getKey())) { em.merge(md); } else { md.setApplication(currentapp); if (md.getCreationTimestamp() == null) { md.setCreationTimestamp(app.getCreationTimestamp()); } em.persist(md); } } } currentapp.setApplicationMetadata(app.getApplicationMetadata()); } em.merge(currentapp); if (metadataChanges) { metadataHelper.propagateMetadata(em, currentapp); } return Response.ok(currentapp).build(); } /** * getMapMD

* Build a map from metadata key → entity for fast reconciliation. * * @param applicationMetadata * @return mapMD */ private Map getMapMD(Set amd) { Map map = new HashMap<>(); if (amd != null) { for (ApplicationMetadata m : amd) { map.put(m.getKey(), m); } } return map; } /** * delete

* Delete an application by ID. *

Note: deletion is not allowed if there are dependent entities (enforced by DB/cascade).

* * @param appId * @param request */ @DELETE @Path("/{appid}") @EnsureTransaction @Produces({ MediaType.APPLICATION_JSON }) @Securable(roles = Rol.ADMIN) @RolesAllowed(BasicSecurityContext.ROL_ADMIN) public Response delete(@PathParam("appid") String appid, @Context HttpServletRequest request) { LOG.info("Deleting app with id: {}", appid); Application app = em.find(Application.class, Integer.parseInt(appid)); if (app == null) { LOG.error("Application with id {} can not be deleted, It was not found in DB", appid); return Response.status(Status.NOT_FOUND) .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid) .build(); } em.remove(app); return Response.ok(Utils.createMap("success", true, "id", appid)).build(); } }