package org.gcube.datatransfer.resolver.services; import java.io.InputStream; import java.net.URISyntaxException; import java.util.LinkedHashMap; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; import org.gcube.application.geoportalcommon.GeoportalCommon; import org.gcube.application.geoportalcommon.shared.GeoportalItemReferences; import org.gcube.application.geoportalcommon.shared.GeoportalItemReferences.SHARE_LINK_TO; import org.gcube.application.geoportaldatamapper.exporter.Geoportal_PDF_Exporter; import org.gcube.application.geoportaldatamapper.shared.ExporterProjectSource; import org.gcube.application.geoportaldatamapper.shared.FileReference; import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; import org.gcube.common.authorization.utils.manager.SecretManager; import org.gcube.common.authorization.utils.manager.SecretManagerProvider; import org.gcube.common.authorization.utils.user.User; import org.gcube.datatransfer.resolver.ConstantsResolver; import org.gcube.datatransfer.resolver.geoportal.exporter.FetchPDF; import org.gcube.datatransfer.resolver.geoportal.exporter.GeoportalJsonResponse; import org.gcube.datatransfer.resolver.geoportal.exporter.HTML_Page; import org.gcube.datatransfer.resolver.services.error.ExceptionManager; import org.gcube.datatransfer.resolver.services.exceptions.BadRequestException; import org.gcube.datatransfer.resolver.util.SingleFileStreamingOutput; import org.gcube.datatransfer.resolver.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Class GeoportalExporter. * * @author Francesco Mangiacrapa at ISTI-CNR francesco.mangiacrapa@isti.cnr.it * * Apr 16, 2024 */ @Path("geoportal") public class GeoportalExporter { private static final String JOB_CODE = "jobCode"; public static final String EXPORT_TYPE = "type"; public static final String PATH_PROJECT_ID = GeoportalResolver.PATH_PROJECT_ID; public static final String PATH_USECASE_ID = GeoportalResolver.PATH_USECASE_ID; public static final String PATH_VRE_NAME = GeoportalResolver.PATH_VRE_NAME; public static final String QUERY_PARAMETER_AS_URL = "as-url"; private static final Logger LOG = LoggerFactory.getLogger(GeoportalExporter.class); private static String helpURI = "https://wiki.gcube-system.org/gcube/URI_Resolver#Geoportal_Resolver"; private static LinkedHashMap map = new LinkedHashMap(); /** * The Enum ACCEPTED_EXPORT_TYPE. * * @author Francesco Mangiacrapa at ISTI-CNR francesco.mangiacrapa@isti.cnr.it * * Apr 22, 2024 */ public enum ACCEPTED_EXPORT_TYPE { pdf } /** * Export. * * @param req the req * @param export_type the export type * @param ucdID the ucd ID * @param projectID the project ID * @param asURL the as URL * @return the response * @throws WebApplicationException the web application exception */ @GET @Path("/export/{type}/{usecase_id}/{project_id}") @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN, MediaType.APPLICATION_OCTET_STREAM }) public Response export(@Context HttpServletRequest req, @PathParam(EXPORT_TYPE) String export_type, @PathParam(PATH_USECASE_ID) String ucdID, @PathParam(PATH_PROJECT_ID) String projectID, @QueryParam(QUERY_PARAMETER_AS_URL) @Nullable String asURL) throws WebApplicationException { String userAgentName = req.getHeader("User-Agent"); LOG.info(this.getClass().getSimpleName() + " export - GET starts..."); LOG.info("Params: [" + EXPORT_TYPE + ": {}, " + PATH_USECASE_ID + ": {}, " + PATH_PROJECT_ID + ": {}, " + QUERY_PARAMETER_AS_URL + ": {}]", export_type, ucdID, projectID, asURL); checkPathParameterNotNull(req, EXPORT_TYPE, export_type); checkPathParameterNotNull(req, PATH_USECASE_ID, ucdID); checkPathParameterNotNull(req, PATH_PROJECT_ID, projectID); boolean getAsURL = false; try { getAsURL = Boolean.parseBoolean(asURL); } catch (Exception e) { } LOG.info("getAsURL is: {}", getAsURL); checkExportType(req, export_type); boolean checked = false; SecretManager cm = SecretManagerProvider.instance.get(); final String context = cm.getContext(); if (context == null) { throw ExceptionManager.forbiddenException(req, "Cannot determine context (the scope)", this.getClass(), helpURI); } final org.gcube.common.authorization.utils.user.User user = cm.getUser(); if (user == null) { throw ExceptionManager.forbiddenException(req, "Cannot determine user", this.getClass(), helpURI); } LOG.info("Identified caller {} in context {}", user.getUsername(), context); try { Geoportal_PDF_Exporter pdfExporter = new Geoportal_PDF_Exporter(); checked = pdfExporter.checkConfig(); } catch (Exception e) { LOG.error("Error on performing export", e); throw ExceptionManager .internalErrorException(req, "Error the " + Geoportal_PDF_Exporter.class.getSimpleName() + " seems to be not configured in the context: " + context, this.getClass(), helpURI); } if (checked) { if (getAsURL) { try { LOG.info("Serving request as getAsURL..."); FileReference pdfRef = exportAsPDF(req, ucdID, projectID, null, context, user); String theURL = pdfRef.getStorageVolatileURL().toString(); LOG.info("returning URL {}", theURL); return Response.ok(theURL).build(); } catch (Exception e) { LOG.error("Error on performing export by url", e); throw ExceptionManager.internalErrorException(req, "Sorry, error occurred when generating the project URL. Error is: " + e.getMessage(), this.getClass(), helpURI); } } try { if (userAgentName != null) { LOG.info("Serving request as User-Agent {}", userAgentName); final String jobCode = ucdID + "_" + projectID + "_" + System.currentTimeMillis(); final FetchPDF fetchPDF = new FetchPDF(null, jobCode, 0); map.put(jobCode, fetchPDF); try { Thread t = new Thread() { @Override public void run() { LOG.info("exportAsPDF called in thread..."); FileReference pdfRef = exportAsPDF(req, ucdID, projectID, null, context, user); LOG.info("exportAsPDF setFileRef in thread for code: " + jobCode); fetchPDF.setFileRef(pdfRef); map.put(jobCode, fetchPDF); } }; t.start(); } catch (Exception e) { LOG.error("Error on performing export in thread", e); throw e; } String serviceViewPDF_URL = String.format("%s/%s/view/%s", Util.getServerURL(req), "geoportal", jobCode); String entity = HTML_Page.entityHTMLMessage("Exporting as PDF...", "The project with id: " + projectID, true, serviceViewPDF_URL); LOG.info("returning waiting HTML page"); return Response.ok(entity).encoding("UTF-8").header(ConstantsResolver.CONTENT_TYPE, "text/html") .build(); } else { LOG.info("Serving request as client..."); FileReference pdfRef = exportAsPDF(req, ucdID, projectID, null, context, user); // returning as stream InputStream input = pdfRef.getStorageVolatileURL().openStream(); StreamingOutput so = new SingleFileStreamingOutput(input); LOG.info("returning project streaming..."); return Response.ok(so) .header(ConstantsResolver.CONTENT_DISPOSITION, "inline; filename=\"" + pdfRef.getFileName() + "\"") .header("Content-Type", pdfRef.getContentType()).build(); } } catch (Exception e) { LOG.error("Error on performing export", e); throw ExceptionManager.internalErrorException(req, "Sorry, error occurred when exporting the project", this.getClass(), helpURI); } } else { return Response .status(Status.NOT_FOUND).entity(GeoportalExporter.class.getSimpleName() + "seems to be not configured in the context: " + context) .type(MediaType.TEXT_PLAIN).build(); } } /** * Healthcheck. * * @param req the req * @param export_type the export type * @return the response * @throws WebApplicationException the web application exception */ @GET @Path("/export/{type}/healthcheck") @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN, MediaType.TEXT_HTML }) public Response healthcheck(@Context HttpServletRequest req, @PathParam(EXPORT_TYPE) String export_type) throws WebApplicationException { LOG.info(this.getClass().getSimpleName() + " healthcheck - GET starts..."); LOG.debug("param is: exportType: {}", export_type); checkPathParameterNotNull(req, EXPORT_TYPE, export_type); checkExportType(req, export_type); try { SecretManager cm = SecretManagerProvider.instance.get(); String context = cm.getContext(); if (context == null) { throw ExceptionManager.forbiddenException(req, "Cannot determine context (the scope)", this.getClass(), helpURI); } org.gcube.common.authorization.utils.user.User user = cm.getUser(); LOG.info("Identified caller {} in context {}", user.getUsername(), context); Geoportal_PDF_Exporter pdfExporter = new Geoportal_PDF_Exporter(); boolean checked = pdfExporter.checkConfig(); GeoportalJsonResponse theJson = new GeoportalJsonResponse(); if (checked) { theJson.setState("OK"); theJson.setMessage(GeoportalExporter.class.getSimpleName() + " Config OK in the context: " + context); String jsonResponse = responseToString(theJson); return Response.ok(jsonResponse).build(); } else { theJson.setState("KO"); theJson.setMessage(GeoportalExporter.class.getSimpleName() + " Config " + Status.NOT_FOUND.getReasonPhrase() + " in the context: " + context); String jsonResponse = responseToString(theJson); return Response.status(Status.NOT_FOUND).entity(jsonResponse).build(); } } catch (Exception e) { LOG.error("Error on performing healthcheck", e); throw ExceptionManager.internalErrorException(req, "Error when performing " + GeoportalExporter.class.getSimpleName() + " healthcheck", this.getClass(), helpURI); } } /** * View job. * * @param req the req * @param jobCode the job code * @return the response * @throws WebApplicationException the web application exception */ @GET @Path("/view/{jobCode}") @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) public Response viewJob(@Context HttpServletRequest req, @PathParam(JOB_CODE) String jobCode) throws WebApplicationException { LOG.info(this.getClass().getSimpleName() + " viewPDF - GET starts..."); LOG.info("viewJob param " + JOB_CODE + ": {}", jobCode); FetchPDF fetchedPDF = map.get(jobCode); LOG.info("viewJob FileReference at code {} is {}", jobCode, fetchedPDF); String theURL = null; String messagge = null; String state = null; GeoportalJsonResponse theJson = new GeoportalJsonResponse(); theJson.setState(state); theJson.setUrl(theURL); theJson.setMessage(messagge); String jsonReponse = null; if (fetchedPDF == null) { theJson.setState(Status.NOT_FOUND.getReasonPhrase()); theJson.setMessage("No job found"); try { jsonReponse = responseToString(theJson); LOG.info("viewJob returning not found: " + jsonReponse); return Response.status(Status.NOT_FOUND).entity(jsonReponse).build(); } catch (JsonProcessingException e) { throw ExceptionManager.internalErrorException(req, "Error when returning " + GeoportalExporter.class.getSimpleName() + " not found job for " + jobCode, this.getClass(), helpURI); } } try { // File PDF is not available if (fetchedPDF.getFileRef() == null) { int attempt = fetchedPDF.incrementAttempt(); if (fetchedPDF.getAttempt() < fetchedPDF.getMAX_RETRY()) { state = "OK"; messagge = "Attempt #" + attempt; } else { state = "ERROR"; messagge = "Sorry, an error occurred tryng to create the PDF. Max retries reached"; theJson.setState(state); } // updating map status map.put(jobCode, fetchedPDF); } else { // File PDF is available state = "OK"; theURL = fetchedPDF.getFileRef().getStorageVolatileURL().toString(); messagge = "PDF created correclty"; // removing from map map.put(jobCode, null); } theJson.setState(state); theJson.setUrl(theURL); theJson.setMessage(messagge); try { jsonReponse = responseToString(theJson); LOG.info("viewJob returning OK: " + jsonReponse); return Response.ok(jsonReponse).build(); } catch (JsonProcessingException e) { throw ExceptionManager.internalErrorException(req, "Error when returning " + GeoportalExporter.class.getSimpleName() + " response for " + jobCode, this.getClass(), helpURI); } } catch (Exception e) { LOG.error("Error on checking job", e); throw ExceptionManager.internalErrorException(req, "Error when checking " + GeoportalExporter.class.getSimpleName() + " job view for " + jobCode, this.getClass(), helpURI); } } public String responseToString(GeoportalJsonResponse jsonResponse) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(jsonResponse); } /** * Check path parameter not null. * * @param req the req * @param parameter the parameter * @param value the value * @throws BadRequestException the bad request exception */ public void checkPathParameterNotNull(HttpServletRequest req, String parameter, String value) throws BadRequestException { if (value == null || value.isEmpty()) { LOG.error("The path parameter {} not found or empty in the path", parameter); throw ExceptionManager.badRequestException(req, "Mandatory path parameter " + parameter + " not found or empty", this.getClass(), helpURI); } } /** * Check export type. * * @param req the req * @param export_type the export type * @return the accepted export type * @throws BadRequestException the bad request exception */ public ACCEPTED_EXPORT_TYPE checkExportType(HttpServletRequest req, String export_type) throws BadRequestException { ACCEPTED_EXPORT_TYPE exportType; try { exportType = ACCEPTED_EXPORT_TYPE.valueOf(export_type); } catch (Exception e) { throw ExceptionManager .wrongParameterException(req, "The path parameter " + EXPORT_TYPE + " has a bad value: " + export_type + ". It must be value of " + ACCEPTED_EXPORT_TYPE.values(), this.getClass(), helpURI); } return exportType; } /** * Export as PDF. * * @param req the req * @param profileID the profile ID * @param projectID the project ID * @param profileTitle the profile title * @param context the context * @param user the user * @return the file reference * @throws WebApplicationException the web application exception */ public FileReference exportAsPDF(HttpServletRequest req, String profileID, String projectID, String profileTitle, String context, User user) throws WebApplicationException { LOG.info("exportAsPDF for profileID: " + profileID + ", projectID: " + projectID + "called"); LOG.info("exportAsPDF context is {}, user is {}", context, user); FileReference pdfRef = null; try { Geoportal_PDF_Exporter gpdfe = new Geoportal_PDF_Exporter(); ExporterProjectSource exportSource = new ExporterProjectSource(); exportSource.setProfileID(profileID); exportSource.setProfileTitle(profileTitle); exportSource.setProjectID(projectID); exportSource.setScope(context); GeoportalItemReferences geoportalItemReferences = new GeoportalItemReferences(projectID, profileID, SHARE_LINK_TO.DATA_VIEWER); GeoportalItemReferences gir = getPublicLinksFor(geoportalItemReferences, context); if (user.isApplication()) { exportSource.setGisLink(gir.getOpenLink().getShortURL()); exportSource.setAccountname(null); } else { exportSource.setGisLink(gir.getRestrictedLink().getShortURL()); exportSource.setAccountname(user.getUsername()); } pdfRef = gpdfe.createPDFFile(exportSource); } catch (Exception e1) { LOG.error("Error occurred when exporting the project", e1); throw ExceptionManager.internalErrorException(req, "Sorry, an error occurred when exporting the project", this.getClass(), helpURI); } return pdfRef; } /** * Gets the public links for. * * @param item the item * @param context the context * @return the public links for * @throws Exception the exception */ public GeoportalItemReferences getPublicLinksFor(GeoportalItemReferences item, String context) throws Exception { LOG.info("getPublicLinksFor called for: " + item); try { if (item == null) throw new Exception("Bad request, the item is null"); if (item.getProjectID() == null) throw new Exception("Bad request, the projectID is null"); if (item.getProfileID() == null) throw new Exception("Bad request, the profileID is null"); GeoportalCommon gc = new GeoportalCommon(); return gc.getPublicLinksFor(context, item, true); } catch (Exception e) { LOG.error("Error on getPublicLinksFor for: " + item, e); throw new Exception("Share link not available for this item. Try later or contact the support. Error: " + e.getMessage()); } } }