import DoiClientContract from '#app/Library/Doi/DoiClientContract'; import DoiClientException from '#app/exceptions/DoiClientException'; import { StatusCodes } from 'http-status-codes'; import logger from '@adonisjs/core/services/logger'; import { AxiosResponse } from 'axios'; import { default as axios } from 'axios'; export class DoiClient implements DoiClientContract { public username: string; public password: string; public serviceUrl: string; public apiUrl: string; constructor() { // const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug'; this.username = process.env.DATACITE_USERNAME || ''; this.password = process.env.DATACITE_PASSWORD || ''; this.serviceUrl = process.env.DATACITE_SERVICE_URL || ''; this.apiUrl = process.env.DATACITE_API_URL || 'https://api.datacite.org'; if (this.username === '' || this.password === '' || this.serviceUrl === '') { const message = 'issing configuration settings to properly initialize DOI client'; logger.error(message); throw new DoiClientException(StatusCodes.BAD_REQUEST, message); } } /** * Creates a DOI with the given identifier * * @param doiValue The desired DOI identifier e.g. '10.5072/tethys.999', * @param xmlMeta * @param landingPageUrl e.g. https://www.tethys.at/dataset/1 * * @return Promise> The http response in the form of a axios response */ public async registerDoi(doiValue: string, xmlMeta: string, landingPageUrl: string): Promise> { //step 1: register metadata via xml upload // state draft // let response; // let url = `${this.serviceUrl}/metadata/${doiValue}`; //https://mds.test.datacite.org/metadata/10.21388/tethys.213 const auth = { username: this.username, password: this.password, }; let headers = { 'Content-Type': 'application/xml;charset=UTF-8', }; try { const metadataResponse = await axios.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers }); // Response Codes // 201 Created: operation successful // 401 Unauthorised: no login // 403 Forbidden: login problem, quota exceeded // 415 Wrong Content Type : Not including content type in the header. // 422 Unprocessable Entity : invalid XML // let test = metadataResponse.data; // 'OK (10.21388/TETHYS.213)' if (metadataResponse.status !== 201) { const message = `Unexpected DataCite MDS response code ${metadataResponse.status}`; logger.error(message); throw new DoiClientException(metadataResponse.status, message); } const doiResponse = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, { auth, headers, }); // Response Codes // 201 Created: operation successful // 400 Bad Request: request body must be exactly two lines: DOI and URL; wrong domain, wrong prefix; // 401 Unauthorised: no login // 403 Forbidden: login problem, quota exceeded // 412 Precondition failed: metadata must be uploaded first. if (doiResponse.status !== 201) { const message = `Unexpected DataCite MDS response code ${doiResponse.status}`; logger.error(message); throw new DoiClientException(doiResponse.status, message); } return doiResponse; } catch (error) { // const message = `request for registering DOI failed with ${error.message}`; // Handle the error, log it, or rethrow as needed logger.error(error.message); throw new DoiClientException(error.response.status, error.response.data); } } /** * Retrieves DOI information from DataCite REST API * * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' * @returns Promise with DOI information or null if not found */ public async getDoiInfo(doiValue: string): Promise { try { // Use configurable DataCite REST API URL const dataciteApiUrl = `${this.apiUrl}/dois/${doiValue}`; const response = await axios.get(dataciteApiUrl, { headers: { Accept: 'application/vnd.api+json', }, }); if (response.status === 200 && response.data.data) { return { created: response.data.data.attributes.created, registered: response.data.data.attributes.registered, updated: response.data.data.attributes.updated, published: response.data.data.attributes.published, state: response.data.data.attributes.state, url: response.data.data.attributes.url, metadata: response.data.data.attributes, }; } } catch (error) { if (error.response?.status === 404) { logger.debug(`DOI ${doiValue} not found in DataCite`); return null; } logger.debug(`DataCite REST API failed for ${doiValue}: ${error.message}`); // Fallback to MDS API return await this.getDoiInfoFromMds(doiValue); } return null; } /** * Fallback method to get DOI info from MDS API * * @param doiValue The DOI identifier * @returns Promise with basic DOI information or null */ private async getDoiInfoFromMds(doiValue: string): Promise { try { const auth = { username: this.username, password: this.password, }; // Get DOI URL const doiResponse = await axios.get(`${this.serviceUrl}/doi/${doiValue}`, { auth }); if (doiResponse.status === 200) { // Get metadata if available try { const metadataResponse = await axios.get(`${this.serviceUrl}/metadata/${doiValue}`, { auth, headers: { Accept: 'application/xml', }, }); return { url: doiResponse.data.trim(), metadata: metadataResponse.data, created: new Date().toISOString(), // MDS doesn't provide creation dates registered: new Date().toISOString(), // Use current time as fallback source: 'mds', }; } catch (metadataError) { // Return basic info even if metadata fetch fails return { url: doiResponse.data.trim(), created: new Date().toISOString(), registered: new Date().toISOString(), source: 'mds', }; } } } catch (error) { if (error.response?.status === 404) { logger.debug(`DOI ${doiValue} not found in DataCite MDS`); return null; } logger.debug(`DataCite MDS API failed for ${doiValue}: ${error.message}`); } return null; } /** * Checks if a DOI exists in DataCite * * @param doiValue The DOI identifier * @returns Promise True if DOI exists */ public async doiExists(doiValue: string): Promise { const doiInfo = await this.getDoiInfo(doiValue); return doiInfo !== null; } /** * Gets the last modification date of a DOI * * @param doiValue The DOI identifier * @returns Promise Last modification date or creation date if never updated, null if not found */ public async getDoiLastModified(doiValue: string): Promise { const doiInfo = await this.getDoiInfo(doiValue); if (doiInfo) { // Use updated date if available, otherwise fall back to created/registered date const dateToUse = doiInfo.updated || doiInfo.registered || doiInfo.created; if (dateToUse) { logger.debug( `DOI ${doiValue}: Using ${doiInfo.updated ? 'updated' : doiInfo.registered ? 'registered' : 'created'} date: ${dateToUse}`, ); return new Date(dateToUse); } } return null; } /** * Makes a DOI unfindable (registered but not discoverable) * Note: DOIs cannot be deleted, only made unfindable * await doiClient.makeDoiUnfindable('10.21388/tethys.231'); * * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' * @returns Promise> The http response */ public async makeDoiUnfindable(doiValue: string): Promise> { const auth = { username: this.username, password: this.password, }; try { // First, check if DOI exists const exists = await this.doiExists(doiValue); if (!exists) { throw new DoiClientException(404, `DOI ${doiValue} not found`); } // Delete the DOI URL mapping to make it unfindable // This removes the URL but keeps the metadata registered const response = await axios.delete(`${this.serviceUrl}/doi/${doiValue}`, { auth }); // Response Codes for DELETE /doi/{doi} // 200 OK: operation successful // 401 Unauthorized: no login // 403 Forbidden: login problem, quota exceeded // 404 Not Found: DOI does not exist if (response.status !== 200) { const message = `Unexpected DataCite MDS response code ${response.status}`; logger.error(message); throw new DoiClientException(response.status, message); } logger.info(`DOI ${doiValue} successfully made unfindable`); return response; } catch (error) { logger.error(`Failed to make DOI ${doiValue} unfindable: ${error.message}`); if (error instanceof DoiClientException) { throw error; } throw new DoiClientException(error.response?.status || 500, error.response?.data || error.message); } } /** * Makes a DOI findable again by re-registering the URL * await doiClient.makeDoiFindable( * '10.21388/tethys.231', * 'https://doi.dev.tethys.at/10.21388/tethys.231' * ); * * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' * @param landingPageUrl The landing page URL * @returns Promise> The http response */ public async makeDoiFindable(doiValue: string, landingPageUrl: string): Promise> { const auth = { username: this.username, password: this.password, }; try { // Re-register the DOI with its URL to make it findable again const response = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, { auth }); // Response Codes for PUT /doi/{doi} // 201 Created: operation successful // 400 Bad Request: request body must be exactly two lines: DOI and URL // 401 Unauthorized: no login // 403 Forbidden: login problem, quota exceeded // 412 Precondition failed: metadata must be uploaded first if (response.status !== 201) { const message = `Unexpected DataCite MDS response code ${response.status}`; logger.error(message); throw new DoiClientException(response.status, message); } logger.info(`DOI ${doiValue} successfully made findable again`); return response; } catch (error) { logger.error(`Failed to make DOI ${doiValue} findable: ${error.message}`); if (error instanceof DoiClientException) { throw error; } throw new DoiClientException(error.response?.status || 500, error.response?.data || error.message); } } /** * Gets the current state of a DOI (draft, registered, findable) * const state = await doiClient.getDoiState('10.21388/tethys.231'); * console.log(`Current state: ${state}`); // 'findable' * * @param doiValue The DOI identifier * @returns Promise The DOI state or null if not found */ public async getDoiState(doiValue: string): Promise { const doiInfo = await this.getDoiInfo(doiValue); return doiInfo?.state || null; } }