diff --git a/.github/linters/sun_checks.xml b/.github/linters/sun_checks.xml index d2ec5e6..74766cb 100644 --- a/.github/linters/sun_checks.xml +++ b/.github/linters/sun_checks.xml @@ -118,12 +118,14 @@ - + @@ -178,13 +180,13 @@ - diff --git a/src/main/java/eosc/eu/ActionError.java b/src/main/java/eosc/eu/ActionError.java index 40eefa2..b315a5c 100644 --- a/src/main/java/eosc/eu/ActionError.java +++ b/src/main/java/eosc/eu/ActionError.java @@ -14,6 +14,7 @@ import javax.ws.rs.core.Response.Status; import parser.b2share.B2ShareException; +import parser.esrf.EsrfException; import parser.zenodo.ZenodoException; import egi.fts.FileTransferServiceException; @@ -48,6 +49,7 @@ public class ActionError { /** * Copy constructor does deep copy + * @param error Error to copy */ public ActionError(ActionError error) { @@ -67,6 +69,8 @@ public ActionError(ActionError error) { /** * Copy but change id + * @param error Error to copy + * @param newId New error id */ public ActionError(ActionError error, String newId) { this(error); @@ -75,6 +79,7 @@ public ActionError(ActionError error, String newId) { /** * Construct with error id + * @param id Error id */ public ActionError(String id) { this.id = id; @@ -84,6 +89,8 @@ public ActionError(String id) { /** * Construct with error id and description + * @param id Error id + * @param description Error description */ public ActionError(String id, String description) { this.id = id; @@ -93,6 +100,8 @@ public ActionError(String id, String description) { /** * Construct with error id and detail + * @param id Error id + * @param detail Key-value pair to add to the details of the error */ public ActionError(String id, Tuple2 detail) { this(id, Arrays.asList(detail)); @@ -100,6 +109,8 @@ public ActionError(String id, Tuple2 detail) { /** * Construct with error id and details + * @param id Error id + * @param details Key-value pairs to add to the details of the error */ public ActionError(String id, List> details) { this.id = id; @@ -115,6 +126,9 @@ public ActionError(String id, List> details) { /** * Construct with error id, description, and detail + * @param id Error id + * @param description Error description + * @param detail Key-value pair to add to the details of the error */ public ActionError(String id, String description, Tuple2 detail) { this(id, description, Arrays.asList(detail)); @@ -122,6 +136,9 @@ public ActionError(String id, String description, Tuple2 detail) /** * Construct with error id, description, and details + * @param id Error id + * @param description Error description + * @param details Key-value pairs to add to the details of the error */ public ActionError(String id, String description, List> details) { this.id = id; @@ -137,6 +154,7 @@ public ActionError(String id, String description, List> d /** * Construct from exception + * @param t The exception to wrap */ public ActionError(Throwable t) { this.id = "exception"; @@ -151,6 +169,7 @@ public ActionError(Throwable t) { var type = t.getClass(); if (type.equals(ZenodoException.class) || type.equals(B2ShareException.class) || + type.equals(EsrfException.class) || type.equals(FileTransferServiceException.class) || type.equals(ClientWebApplicationException.class) || type.equals(WebApplicationException.class) ) { @@ -217,6 +236,8 @@ else if(type.equals(ProcessingException.class)) { /** * Construct from exception and detail + * @param t The exception to wrap + * @param detail Key-value pair to add to the details of the error */ public ActionError(Throwable t, Tuple2 detail) { this(t, Arrays.asList(detail)); @@ -224,6 +245,8 @@ public ActionError(Throwable t, Tuple2 detail) { /** * Construct from exception and details + * @param t The exception to wrap + * @param details Key-value pair to add to the details of the error */ public ActionError(Throwable t, List> details) { this(t); @@ -265,6 +288,7 @@ else if(keys.contains("seUrl")) /** * Retrieve the HTTP status code + * @return HTTP status code */ public Status getStatus() { return this.status; @@ -272,6 +296,8 @@ public Status getStatus() { /** * Update the HTTP status code + * @param status New HTTP status + * @return Instance to allow for fluent calls (with .) */ public ActionError setStatus(Status status) { this.status = status; @@ -280,6 +306,7 @@ public ActionError setStatus(Status status) { /** * Convert to Response that can be returned by a REST endpoint + * @return Response object */ public Response toResponse() { return Response.ok(this).status(this.status).build(); @@ -287,6 +314,8 @@ public Response toResponse() { /** * Convert to Response with new status that can be returned by a REST endpoint + * @param status New HTTP status + * @return Response object with new HTTP status code */ public Response toResponse(Status status) { return Response.ok(this).status(status).build(); diff --git a/src/main/java/eosc/eu/model/StorageElement.java b/src/main/java/eosc/eu/model/StorageElement.java index 891fade..23e9796 100644 --- a/src/main/java/eosc/eu/model/StorageElement.java +++ b/src/main/java/eosc/eu/model/StorageElement.java @@ -9,6 +9,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import parser.zenodo.model.ZenodoFile; import parser.b2share.model.B2ShareFile; +import parser.esrf.model.EsrfDataFile; import egi.fts.model.ObjectInfo; @@ -55,7 +56,9 @@ public class StorageElement extends StorageElementBase { public StorageElement() { super("StorageElement"); } /** - * Constructor + * Construct using access URL and media type + * @param url Access URL for the storage element + * @param type Media type */ public StorageElement(String url, String type) { super("StorageElement"); @@ -65,6 +68,7 @@ public StorageElement(String url, String type) { /** * Construct from Zenodo file + * @param zf Zenodo file */ public StorageElement(ZenodoFile zf) { super("StorageElement", zf.filename); @@ -76,8 +80,27 @@ public StorageElement(ZenodoFile zf) { } } + /** + * Construct from ESRF file + * @param ef ESRF file + * @param baseUrl The base URL for the access URL, as the ESRF file + * only contains the path to the file + */ + public StorageElement(EsrfDataFile ef, String baseUrl) { + super("StorageElement", ef.Datafile.name); + this.size = ef.Datafile.fileSize; + this.accessUrl = baseUrl + ef.Datafile.location; + + if(null != ef.Datafile.createTime) + this.createdAt = ef.Datafile.createTime; + + if(null != ef.Datafile.modTime) + this.modifiedAt = ef.Datafile.modTime; + } + /** * Construct from B2Share file + * @param b2sf B2SHARE file */ public StorageElement(B2ShareFile b2sf) { super("StorageElement", b2sf.name); @@ -94,6 +117,7 @@ public StorageElement(B2ShareFile b2sf) { /** * Construct from FTS object + * @param obj Information about an object returned by FTS */ public StorageElement(ObjectInfo obj) { super("StorageElement"); diff --git a/src/main/java/parser/b2share/B2ShareException.java b/src/main/java/parser/b2share/B2ShareException.java index b69c12b..ff53527 100644 --- a/src/main/java/parser/b2share/B2ShareException.java +++ b/src/main/java/parser/b2share/B2ShareException.java @@ -15,10 +15,19 @@ public B2ShareException() { super(); } + /*** + * Construct from a response + * @param resp Response object + * @param body Response body + */ public B2ShareException(Response resp, String body) { super(resp); this.responseBody = body; } + /*** + * Get the response body + * @return Body of the response + */ String responseBody() { return responseBody; } } diff --git a/src/main/java/parser/b2share/B2ShareParser.java b/src/main/java/parser/b2share/B2ShareParser.java index 1b2103a..0482087 100644 --- a/src/main/java/parser/b2share/B2ShareParser.java +++ b/src/main/java/parser/b2share/B2ShareParser.java @@ -37,6 +37,7 @@ public class B2ShareParser implements ParserService { /*** * Constructor + * @param id The key of the parser in the config file */ public B2ShareParser(String id) { this.id = id; } @@ -47,7 +48,6 @@ public class B2ShareParser implements ParserService { * @return true on success */ public boolean init(ParserConfig config, PortConfig port) { - this.name = config.name(); this.timeout = config.timeout(); @@ -122,7 +122,8 @@ else if(!doi.equals(redirectedToUrl)) LOG.debugf("Redirected DOI %s", redirectedToUrl); // Validate URL - Pattern p = Pattern.compile("^(https?://[^/:]*b2share[^/:]*:?[\\d]*)/records/(.+)", Pattern.CASE_INSENSITIVE); + Pattern p = Pattern.compile("^(https?://[^/:]*b2share[^/:]*:?[\\d]*)/records/(.+)", + Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(redirectedToUrl); boolean isSupported = m.matches(); diff --git a/src/main/java/parser/esrf/Esrf.java b/src/main/java/parser/esrf/Esrf.java new file mode 100644 index 0000000..824a139 --- /dev/null +++ b/src/main/java/parser/esrf/Esrf.java @@ -0,0 +1,38 @@ +package parser.esrf; + +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; +import parser.esrf.model.*; + +import java.util.List; + + +/*** + * REST client for ESRF + */ +@RegisterProvider(value = EsrfExceptionMapper.class) +@Produces(MediaType.APPLICATION_JSON) +public interface Esrf { + + @POST + @Path("/session") + @Consumes(MediaType.APPLICATION_JSON) + Uni getSessionAsync(EsrfCredentials credentials); + + @GET + @Path("/doi/{authority}/{recordId}/datasets") + Uni> getDataSetsAsync(@RestQuery("sessionId") String sessionId, + @RestPath("authority") String authority, + @RestPath("recordId") String recordId); + + @GET + @Path("/catalogue/{sessionId}/dataset/id/{datasetId}/datafile") + Uni> getDataFilesAsync(@RestPath("sessionId") String sessionId, + @RestPath("datasetId") String datasetId); +} diff --git a/src/main/java/parser/esrf/EsrfException.java b/src/main/java/parser/esrf/EsrfException.java new file mode 100644 index 0000000..b04dd4d --- /dev/null +++ b/src/main/java/parser/esrf/EsrfException.java @@ -0,0 +1,33 @@ +package parser.esrf; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + + +/** + * Exception class for ESRF API calls + */ +public class EsrfException extends WebApplicationException { + + private String responseBody; + + public EsrfException() { + super(); + } + + /*** + * Construct from a response + * @param resp Response object + * @param body Response body + */ + public EsrfException(Response resp, String body) { + super(resp); + this.responseBody = body; + } + + /*** + * Get the response body + * @return Body of the response + */ + String responseBody() { return responseBody; } +} diff --git a/src/main/java/parser/esrf/EsrfExceptionMapper.java b/src/main/java/parser/esrf/EsrfExceptionMapper.java new file mode 100644 index 0000000..cdb0077 --- /dev/null +++ b/src/main/java/parser/esrf/EsrfExceptionMapper.java @@ -0,0 +1,47 @@ +package parser.esrf; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; + + +/** + * Custom exception mapper for ESRF API calls, allows access to response body in case of error + */ +@Priority(Priorities.USER) +public final class EsrfExceptionMapper implements ResponseExceptionMapper { + + @Override + public EsrfException toThrowable(Response response) { + try { + response.bufferEntity(); + } catch(Exception ignored) {} + + String msg = getBody(response); + return new EsrfException(response, msg); + } + + @Override + public boolean handles(int status, MultivaluedMap headers) { + return status >= StatusCode.BAD_REQUEST; + } + + private String getBody(Response response) { + String body = ""; + if(response.hasEntity()) { + ByteArrayInputStream is = (ByteArrayInputStream)response.getEntity(); + if(null == is) + is = (ByteArrayInputStream)((ResponseImpl)response).getEntityStream(); + byte[] bytes = new byte[is.available()]; + is.read(bytes, 0, is.available()); + body = new String(bytes); + } + return body; + } +} diff --git a/src/main/java/parser/esrf/EsrfParser.java b/src/main/java/parser/esrf/EsrfParser.java new file mode 100644 index 0000000..f00f956 --- /dev/null +++ b/src/main/java/parser/esrf/EsrfParser.java @@ -0,0 +1,223 @@ +package parser.esrf; + +import eosc.eu.PortConfig; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.tuples.Tuple2; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.jboss.logging.Logger; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import eosc.eu.ParsersConfig.ParserConfig; +import eosc.eu.ParserService; +import eosc.eu.TransferServiceException; +import eosc.eu.model.*; +import parser.ParserHelper; +import parser.esrf.model.EsrfCredentials; + + +/*** + * Class for parsing ESRF DOIs + */ +public class EsrfParser implements ParserService { + + private static final Logger LOG = Logger.getLogger(EsrfParser.class); + + private String id; + private String name; + private int timeout; + private String baseUrl; + private String authority; + private String recordId; + private static Esrf parser; + + + /*** + * Constructor + * @param id The key of the parser in the config file + */ + public EsrfParser(String id) { this.id = id; } + + /*** + * Initialize the REST client for ESRF. + * @param config Configuration of the parser, from the config file. + * @param port The port on which the application runs, from the config file. + * @return true on success + */ + public boolean init(ParserConfig config, PortConfig port) { + this.name = config.name(); + this.timeout = config.timeout(); + + if (null != parser) + return true; + + LOG.debug("Obtaining REST client for ESRF"); + + // Check if base URL is valid + URL urlParserService; + try { + this.baseUrl = config.url().isPresent() ? config.url().get() : ""; + urlParserService = new URL(this.baseUrl); + + if(!this.baseUrl.isEmpty() && '/' == this.baseUrl.charAt(this.baseUrl.length() - 1)) + this.baseUrl = this.baseUrl.replaceAll("[/]+$", ""); + + } catch (MalformedURLException e) { + LOG.error(e.getMessage()); + return false; + } + + try { + // Create the REST client for the parser service + parser = RestClientBuilder.newBuilder() + .baseUrl(urlParserService) + .build(Esrf.class); + + return true; + } + catch (RestClientDefinitionException e) { + LOG.error(e.getMessage()); + } + + return false; + } + + /*** + * Get the Id of the parser. + * @return Id of the parser service. + */ + public String getId() { return this.id; } + + /*** + * Get the human-readable name of the parser. + * @return Name of the parser service. + */ + public String getName() { return this.name; } + + /*** + * Get the Id of the source record. + * @return Source record Id. + */ + public String sourceId() { return this.recordId; } + + /*** + * Checks if the parser service understands this DOI. + * @param auth The access token needed to call the service. + * @param doi The DOI for a data set. + * @param helper Helper class that can follow (and cache) redirects. + * @return Return true if the parser service can parse this DOI. + */ + public Uni> canParseDOI(String auth, String doi, ParserHelper helper) { + boolean isValid = null != doi && !doi.isBlank(); + if(!isValid) + return Uni.createFrom().failure(new TransferServiceException("doiInvalid")); + + // Check if DOI points/redirects to an ESRF record + var result = Uni.createFrom().item(helper.redirectedToUrl()) + + .chain(redirectedToUrl -> { + if(null != redirectedToUrl) + return Uni.createFrom().item(redirectedToUrl); + + return helper.checkRedirect(doi); + }) + .chain(redirectedToUrl -> { + if(null == redirectedToUrl) + redirectedToUrl = doi; + else if(!doi.equals(redirectedToUrl)) + LOG.debugf("Redirected DOI %s", redirectedToUrl); + + // Validate URL + Pattern p = Pattern.compile("^https?://([\\w\\.]*esrf.fr)/doi/([^/]+)/([^/#\\?]+)", + Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(redirectedToUrl); + boolean isSupported = m.matches(); + + if(isSupported) { + this.authority = m.group(2); + this.recordId = m.group(3); + } + + return Uni.createFrom().item(Tuple2.of(isSupported, (ParserService)this)); + }) + .onFailure().invoke(e -> { + LOG.errorf("Failed to check if DOI %s points to ESRF record", doi); + }); + + return result; + } + + /** + * Parse the DOI and return a set of files in the data set. + * @param auth The access token needed to call the service. + * @param doi The DOI for a data set. + * @param level Unused. + * @return List of files in the data set. + */ + public Uni parseDOI(String auth, String doi, int level) { + if(null == doi || doi.isBlank()) + return Uni.createFrom().failure(new TransferServiceException("doiInvalid")); + + if(null == parser) + return Uni.createFrom().failure(new TransferServiceException("configInvalid")); + + if(null == this.authority || this.authority.isEmpty() || + null == this.recordId || this.recordId.isEmpty()) + return Uni.createFrom().failure(new TransferServiceException("noRecordId")); + + AtomicReference sessionId = new AtomicReference<>(null); + Uni result = Uni.createFrom().nullItem() + + .ifNoItem() + .after(Duration.ofMillis(this.timeout)) + .failWith(new TransferServiceException("doiParseTimeout")) + .chain(unused -> { + // Get an ESRF session + return parser.getSessionAsync(new EsrfCredentials("reader", "reader")); + }) + .chain(session -> { + // Got a session + if(null == session || null == session.sessionId) + return Uni.createFrom().failure(new TransferServiceException("noSessionId")); + + sessionId.set(session.sessionId); + + // Get the dataset + return parser.getDataSetsAsync(sessionId.get(), this.authority, this.recordId); + }) + .chain(datasets -> { + // Got dataset(s) + LOG.infof("Found %d datasets at DOI %s", datasets.size(), doi); + + // Handle the first dataset, ignore the rest + var dataset = datasets.get(0); + LOG.infof("First dataset has ID %s", dataset.id); + + return parser.getDataFilesAsync(sessionId.get(), dataset.id); + }) + .chain(files -> { + // Got the files in the dataset + StorageContent srcFiles = new StorageContent(files.size()); + for(var file : files) { + srcFiles.elements.add(new StorageElement(file, this.baseUrl)); + } + + srcFiles.count = srcFiles.elements.size(); + + // Success + return Uni.createFrom().item(srcFiles); + }) + .onFailure().invoke(e -> { + LOG.errorf("Failed to parse ESRF DOI %s", doi); + }); + + return result; + } + +} diff --git a/src/main/java/parser/esrf/model/EsrfCredentials.java b/src/main/java/parser/esrf/model/EsrfCredentials.java new file mode 100644 index 0000000..0175d33 --- /dev/null +++ b/src/main/java/parser/esrf/model/EsrfCredentials.java @@ -0,0 +1,25 @@ +package parser.esrf.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + +/** + * Credentials for ESRF + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class EsrfCredentials { + + public String username; + public String password; + public String plugin = "db"; + + /*** + * Create from credentials + * @param username ESRF username + * @param password Password for the user + */ + public EsrfCredentials(String username, String password) { + this.username = username; + this.password = password; + } +} diff --git a/src/main/java/parser/esrf/model/EsrfDataFile.java b/src/main/java/parser/esrf/model/EsrfDataFile.java new file mode 100644 index 0000000..8aa663a --- /dev/null +++ b/src/main/java/parser/esrf/model/EsrfDataFile.java @@ -0,0 +1,32 @@ +package parser.esrf.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Date; + + +/** + * ESRF file + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class EsrfDataFile { + + public Info Datafile; + + /*** + * Constructor + */ + public EsrfDataFile() {} + + /*** + * The details of the ESRF file + */ + public class Info { + public String id; + public String name; + public Date createTime; + public Date modTime; + public long fileSize; + public String location; + } +} diff --git a/src/main/java/parser/esrf/model/EsrfDataSet.java b/src/main/java/parser/esrf/model/EsrfDataSet.java new file mode 100644 index 0000000..1efbf76 --- /dev/null +++ b/src/main/java/parser/esrf/model/EsrfDataSet.java @@ -0,0 +1,24 @@ +package parser.esrf.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Date; + + +/** + * ESRF dataset + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class EsrfDataSet { + + public String id; + public String name; + public Date startDate; + public Date endDate; + public String location; + + /*** + * Constructor + */ + public EsrfDataSet() {} +} diff --git a/src/main/java/parser/esrf/model/EsrfSession.java b/src/main/java/parser/esrf/model/EsrfSession.java new file mode 100644 index 0000000..17ba06d --- /dev/null +++ b/src/main/java/parser/esrf/model/EsrfSession.java @@ -0,0 +1,23 @@ +package parser.esrf.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + +/** + * ESRF session + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class EsrfSession { + + public String sessionId; + public double lifeTimeMinutes; + public String name; + public boolean isAdministrator; + public boolean isInstrumentScientist; + public boolean isMinter; + + /*** + * Constructor + */ + public EsrfSession() {} +} diff --git a/src/main/java/parser/generic/SignpostParser.java b/src/main/java/parser/generic/SignpostParser.java index f02ed52..e291fde 100644 --- a/src/main/java/parser/generic/SignpostParser.java +++ b/src/main/java/parser/generic/SignpostParser.java @@ -52,7 +52,6 @@ public class SignpostParser implements ParserService { * @return true on success */ public boolean init(ParserConfig config, PortConfig port) { - this.name = config.name(); this.timeout = config.timeout(); diff --git a/src/main/java/parser/zenodo/ZenodoException.java b/src/main/java/parser/zenodo/ZenodoException.java index dfd45eb..9ecaff5 100644 --- a/src/main/java/parser/zenodo/ZenodoException.java +++ b/src/main/java/parser/zenodo/ZenodoException.java @@ -15,10 +15,19 @@ public ZenodoException() { super(); } + /*** + * Construct from a response + * @param resp Response object + * @param body Response body + */ public ZenodoException(Response resp, String body) { super(resp); this.responseBody = body; } + /*** + * Get the response body + * @return Body of the response + */ String responseBody() { return responseBody; } } diff --git a/src/main/java/parser/zenodo/ZenodoParser.java b/src/main/java/parser/zenodo/ZenodoParser.java index 22f4882..c142068 100644 --- a/src/main/java/parser/zenodo/ZenodoParser.java +++ b/src/main/java/parser/zenodo/ZenodoParser.java @@ -41,14 +41,12 @@ public class ZenodoParser implements ParserService { public ZenodoParser(String id) { this.id = id; } /*** - * Initialize the REST client for B2Share. - * The hostname of the B2Share server should be already determined by a previous call to canParseDOI(). + * Initialize the REST client for Zenodo. * @param config Configuration of the parser, from the config file. * @param port The port on which the application runs, from the config file. * @return true on success */ public boolean init(ParserConfig config, PortConfig port) { - this.name = config.name(); this.timeout = config.timeout(); @@ -108,7 +106,6 @@ public boolean init(ParserConfig config, PortConfig port) { * @return Return true if the parser service can parse this DOI. */ public Uni> canParseDOI(String auth, String doi, ParserHelper helper) { - // Validate DOI without actually fetching the URL boolean isValid = null != doi && !doi.isBlank(); if(!isValid) return Uni.createFrom().failure(new TransferServiceException("doiInvalid")); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 915dda8..53b7ae4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,6 +6,10 @@ eosc: class: parser.zenodo.ZenodoParser url: https://zenodo.org timeout: 5000 + esrf: + name: European Synchrotron Radiation Facility + class: parser.esrf.EsrfParser + url: https://icatplus.esrf.fr b2share: name: B2Share class: parser.b2share.B2ShareParser @@ -86,7 +90,7 @@ quarkus: urls-primary-name: default smallrye-openapi: path: /openapi - info-version: 1.1.40 + info-version: 1.1.41 jackson: fail-on-unknown-properties: false http: