From 04e9e455a4b6f210805520a95003c7692590a204 Mon Sep 17 00:00:00 2001 From: "konstantina.galouni" Date: Tue, 25 Jul 2023 11:58:23 +0300 Subject: [PATCH] oaipmh-validator.component: [WIP] Added Validator-first screen | oaipmh-validator.service.ts: Added methods "getSets()" and "validate()" | string-utils.class.ts: Added StringUtils file (from library). --- .../oaipmh-validator.component.html | 58 ++- .../oaipmh-validator.component.ts | 48 +- src/app/services/oaipmh-validator.service.ts | 12 + src/app/shared/utils/string-utils.class.ts | 455 ++++++++++++++++++ 4 files changed, 568 insertions(+), 5 deletions(-) create mode 100644 src/app/shared/utils/string-utils.class.ts diff --git a/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.html b/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.html index f0644e4..9d47132 100644 --- a/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.html +++ b/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.html @@ -1 +1,57 @@ -under development... +
+
+
+
+ +
+
1. Select a data source (repository or journal)
+
+
+ +
+
2. Select guidelines
+
+
+ + +
+
+
+ +
+
3. Define the number of record to validate
+
+ + +
+
+ + + + + {{recordsNum}} + + +
+ +
+ +
+ +
+ +
+
+
+
+
+
+ diff --git a/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.ts b/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.ts index a7d4aa5..eb748c9 100644 --- a/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.ts +++ b/src/app/pages/oaipmh-validator/validation-settings/oaipmh-validator.component.ts @@ -1,5 +1,8 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {OaipmhValidatorService} from "../../../services/oaipmh-validator.service"; +import {Option} from "../../../shared/utils/input/input.component"; +import {UntypedFormBuilder, UntypedFormGroup, Validators} from "@angular/forms"; +import {StringUtils} from "../../../shared/utils/string-utils.class"; @Component({ selector: 'app-oaipmh-validator', @@ -7,9 +10,46 @@ import {OaipmhValidatorService} from "../../../services/oaipmh-validator.service styleUrls: ['./oaipmh-validator.component.less'] }) export class OaipmhValidatorComponent implements OnInit { - constructor(private validator: OaipmhValidatorService) {} + public options: Option[] = [ + {label: 'OpenAIRE Guidelines for Data Archives Profile v2', value: 'OpenAIRE Guidelines for Data Archives Profile v2'}, + {label: 'OpenAIRE Guidelines for Literature Repositories Profile v3', value: 'OpenAIRE Guidelines for Literature Repositories Profile v3'}, + {label: 'OpenAIRE Guidelines for Literature Repositories Profile v4', value: 'OpenAIRE Guidelines for Literature Repositories Profile v4'}, + {label: 'OpenAIRE FAIR Guidelines for Data Repositories Profile', value: 'OpenAIRE FAIR Guidelines for Data Repositories Profile'} + ]; + public form: UntypedFormGroup; + public sets: Option[] = [{label: 'All sets', value: 'all'}]; + public recordsNum: number = 10; - ngOnInit() { - console.log("under development..."); + constructor(private fb: UntypedFormBuilder, private validator: OaipmhValidatorService) { + this.form = this.fb.group({ + url: this.fb.control("", StringUtils.urlValidator()),//[Validators.required/*, Validators.email*/]), + guidelines: this.fb.control("", Validators.required), + recordsNum: this.fb.control(null, Validators.required), + set: this.fb.control('', Validators.required) + }); + } + + ngOnInit() {} + + updateRecordsNum(increase: boolean = true) { + this.recordsNum = this.recordsNum + (increase ? 10 : -10); + this.form.get('recordsNum').setValue(this.recordsNum); + } + + getSets() { + let options: Option[] = [{label: 'All sets', value: 'all'}]; + if(this.form.get('url') && this.form.get('url').value && StringUtils.isValidUrl(this.form.get('url').value)) { + this.validator.getSets(this.form.get('url').value).subscribe(sets => { + for(let set of sets) { + options.push({label: set['setName'], value: set['setSpec']}); + } + this.sets = options; + }); + } + } + + validate() { + console.log(this.form.value); + this.validator.validate(this.form.get('guidelines').value, this.form.get('url').value, this.form.get('recordsNum').value, this.form.get('set').value); } } diff --git a/src/app/services/oaipmh-validator.service.ts b/src/app/services/oaipmh-validator.service.ts index 49605fe..32c883f 100644 --- a/src/app/services/oaipmh-validator.service.ts +++ b/src/app/services/oaipmh-validator.service.ts @@ -1,6 +1,7 @@ import {Injectable} from "@angular/core"; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; +import {map} from "rxjs"; @Injectable({ providedIn: "root" @@ -28,4 +29,15 @@ export class OaipmhValidatorService { let url: string = environment.validatorAPI + "reports/getJobResult?jobId="+jobId; return this.http.get(url); } + + getSets(baseUrl) { + let url: string = environment.validatorAPI + "getSets?baseUrl="+baseUrl; + return this.http.get(url).pipe(map(res => res['set'])); + } + + validate(guidelines: string, baseUrl: string, numberOfRecords: string, set: string) { + let url: string = environment.validatorAPI + "realValidator?guidelines="+guidelines+"&baseUrl="+baseUrl + +(numberOfRecords ? ("&numberOfRecords="+numberOfRecords) : "") + (set ? ("&set="+set) : ""); + console.log(url); + } } diff --git a/src/app/shared/utils/string-utils.class.ts b/src/app/shared/utils/string-utils.class.ts new file mode 100644 index 0000000..8eb8ab1 --- /dev/null +++ b/src/app/shared/utils/string-utils.class.ts @@ -0,0 +1,455 @@ +import {UrlSegment} from '@angular/router'; +import {AbstractControl, UntypedFormGroup, ValidationErrors, ValidatorFn, Validators} from "@angular/forms"; + +export class Dates { + public static yearMin = 1800; + public static yearMax = (new Date().getFullYear()) + 10; + public static currentYear = (new Date().getFullYear()); + + public static isValidYear(yearString, yearMin = this.yearMin, yearMax = this.yearMax) { + // First check for the pattern + if (!/^\d{4}$/.test(yearString)) + return false; + var year = parseInt(yearString, 10); + + // Check the ranges of month and year + return !(year < yearMin || year > yearMax); + } + + //format YYYY-MM-DD + public static isValidDate(dateString: string) { + // First check for the pattern + if (!/^\d{4}\-\d{1,2}\-\d{1,2}$/.test(dateString)) + return false; + + // Parse the date parts to integers + var parts = dateString.split("-"); + var day = parseInt(parts[2], 10); + var month = parseInt(parts[1], 10); + var year = parseInt(parts[0], 10); + if (!this.isValidYear(parts[0])) { + return false; + } + + // Check the ranges of month and year + if (month == 0 || month > 12) + return false; + + var monthLength = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + + // Adjust for leap years + if (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)) + monthLength[1] = 29; + + // Check the range of the day + return day > 0 && day <= monthLength[month - 1]; + + } + + public static getDateToday(): Date { + var myDate = new Date(); + return myDate; + + } + + public static getDateToString(myDate: Date): string { + var date: string = myDate.getFullYear() + "-"; + date += ((myDate.getMonth() + 1) < 10) ? "0" + (myDate.getMonth() + 1) : (myDate.getMonth() + 1); + date += "-"; + date += (myDate.getDate() < 10) ? "0" + myDate.getDate() : myDate.getDate(); + return date; + + } + + public static getDateXMonthsAgo(x: number): Date { + var myDate = new Date(); + myDate.setMonth(myDate.getMonth() - x); + return myDate; + + } + + public static getDateXYearsAgo(x: number): Date { + var myDate = new Date(); + myDate.setFullYear(myDate.getFullYear() - x); + return myDate; + + } + + public static getDateFromString(date: string): Date { + + var myDate = new Date(); + myDate.setFullYear(+date.substring(0, 4)); + myDate.setMonth(((date.length > 5) ? (+date.substring(5, 7) - 1) : (0))); + myDate.setDate(((date.length > 8) ? (+date.substring(8, 11)) : (1))); + return myDate; + + } + + public static getDate(dateString: string): Date { + let date = new Date(dateString); + if (Object.prototype.toString.call(date) === "[object Date]") { + if (isNaN(date.getTime())) { + return null; + } else { + return date; + } + } else { + return null; + } + } + + public static timeSince(date: Date) { + + let seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000); + + let interval = seconds / (365*24*60*60); + + if (interval > 1) { + let years = Math.floor(interval); + return (years > 1?(years + ' years ago'):'a year ago'); + } + interval = seconds / (7*24*60*60); + if (interval > 1) { + let weeks = Math.floor(interval); + return (weeks > 1?(weeks + ' weeks ago'):'a week ago'); + } + interval = seconds / (24*60*60); + if (interval > 1) { + let days = Math.floor(interval); + return (days > 1?(days + ' days ago'):'a day ago'); + } + interval = seconds / (60*60); + if (interval > 1) { + let hours = Math.floor(interval); + return (hours > 1?(hours + ' hours ago'):'an hour ago'); + } + interval = seconds / 60; + if (interval > 1) { + let minutes = Math.floor(interval); + return (minutes > 1?(minutes + ' minutes ago'):'a minute ago'); + } + seconds = Math.floor(interval); + return (seconds > 1?(seconds + ' seconds ago'):' just now'); + } +} + +export class DOI { + + public static getDOIsFromString(str: string): string[] { + return Identifier.getDOIsFromString(str); + } + + public static isValidDOI(str: string): boolean { + return Identifier.isValidDOI(str); + } +} + +export class Identifier { + class: "doi" | "pmc" | "pmid" | "handle" | "ORCID" | "re3data" = null; + id: string; + + public static getDOIsFromString(str: string): string[] { + var DOIs: string[] = []; + var words: string[] = str.split(" "); + + for (var i = 0; i < words.length; i++) { + let id = words[i]; + if (DOI.isValidDOI(id) ) { + id = Identifier.getRawDOIValue(id); + if( DOIs.indexOf(id) == -1){ + DOIs.push(id); + } + } + } + return DOIs; + } + public static getRawDOIValue(id: string): string { + if(id.indexOf("doi.org")!=-1 && id.split("doi.org/").length > 1){ + id = id.split("doi.org/")[1]; + } + return id; + } + public static getIdentifiersFromString(str: string): Identifier[] { + let identifiers: Identifier[] = []; + let words: string[] = str.split(" "); + + for (let id of words) { + if (id.length > 0) { + let identifier: Identifier = this.getIdentifierFromString(id); + if (identifier) { + identifiers.push(identifier); + } + } + } + return identifiers; + } + + public static getIdentifierFromString(pid: string,strict:boolean = true): Identifier { + if (Identifier.isValidDOI(pid)) { + pid = Identifier.getRawDOIValue(pid); + return {"class": "doi", "id": pid}; + } else if (Identifier.isValidORCID(pid)) { + return {"class": "ORCID", "id": pid}; + } else if (Identifier.isValidPMCID(pid)) { + return {"class": "pmc", "id": pid}; + } else if (Identifier.isValidPMID(pid)) { + return {"class": "pmid", "id": pid}; + } else if (Identifier.isValidHANDLE(pid)) { + return {"class": "handle", "id": pid}; + } else if (Identifier.isValidRe3Data(pid)) { + return {"class": "re3data", "id": pid}; + } + //set it as a doi, to catch the case that doi has not valid format + return (strict?null:{"class": "doi", "id": pid}); + } + + public static getPIDFromIdentifiers(identifiers: Map): Identifier { + let classes:string [] = ["doi", "handle", "pmc", "pmid", "re3data"]; + if(identifiers) { + for (let cl of classes) { + if (identifiers.get(cl)) { + for (let pid of identifiers.get(cl)) { + let identifier = Identifier.getIdentifierFromString(pid); + if (identifier) { + return identifier; + } + } + } + } + } + return null; + } + + public static isValidDOI(str: string): boolean { + //keep only exp3? + let exp1 = /\b(10[.][0-9]{4,}(?:[.][0-9]+)*\/(?:(?!["&\'<>])\S)+)\b/g; + let exp2 = /\b(10[.][0-9]{4,}(?:[.][0-9]+)*\/(?:(?!["&\'<>])[[:graph:]])+)\b/g; + let exp3 = /\b(10[.]*)\b/g; + return (str.match(exp1) != null || str.match(exp2) != null || str.match(exp3) != null); + } + + public static isValidORCID(str: string): boolean { + let exp = /\b\d{4}-\d{4}-\d{4}-(\d{3}X|\d{4})\b/g; + return str.match(exp) != null; + } + + public static isValidPMID(str: string): boolean { + let exp = /^\d*$/g; + return str.match(exp) != null; + + } + + public static isValidPMCID(str: string): boolean { + let exp = /^(PMC\d{7})$/g; + return str.match(exp) != null; + } + + public static isValidHANDLE(str: string): boolean { + let exp = /^[0-9a-zA-Z-]*\/[0-9a-zA-Z-]*$/g; + return str.match(exp) != null; + } + + public static isValidRe3Data(str: string): boolean { + let exp = /(r3d[1-9]\d{0,8})/g; + return str.match(exp) != null; + } +} + +export class StringUtils { + + public static urlRegex = 'https?:\\/\\/(?:www(2?)\\.|(?!www(2?)))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www(2?)\\.' + + '[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www(2?)\\.|(?!www(2?)))[a-zA-Z0-9]+\\.[^\\s]{2,}|www(2?)\\.' + + '[a-zA-Z0-9]+\\.[^\\s]{2,}'; + + public static routeRegex = '^[a-zA-Z0-9\/][a-zA-Z0-9\/-]*$'; + + public static jsonRegex = /^[\],:{}\s]*$/; + + public static urlPrefix(url: string): string { + if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) { + return ""; + } else { + return "//"; + } + } + + public static quote(params: string): string { + return '"' + params + '"'; + } + + public static unquote(params: string): string { + if (params.length > 2 && (params[0] == '"' && params[params.length - 1] == '"') || (params[0] == "'" && params[params.length - 1] == "'")) { + params = params.substring(1, params.length - 1); + } + return params; + } + + public static URIEncode(params: string): string { + return encodeURIComponent(params); + } + + public static URIDecode(params: string): string { + return decodeURIComponent(params); + } + + public static validateEmails(emails: string): boolean { + return (emails.split(',') + .map(email => Validators.email({value: email.trim()})) + .find(_ => _ !== null) === undefined); + } + + public static b64DecodeUnicode(str) { + return decodeURIComponent(Array.prototype.map.call(atob(str), function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } + + private emailValidator(email: any): boolean { + return !!email.match("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$"); + } + + public static isValidUrl(url: string): boolean { + return new RegExp(this.urlRegex).test(url); + } + + public static urlValidator(): ValidatorFn { + return Validators.pattern(StringUtils.urlRegex); + } + + public static jsonValidator(error: string = ''): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if(control.value) { + let test = control.getRawValue().replace(/\\["\\\/bfnrtu]/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + if(!new RegExp(this.jsonRegex).test(test)) { + return {error: 'Please provide a valid JSON.' + error} + } + } + return null; + }; + } + + public static validatorType(options: string[]): ValidatorFn { + return (control: AbstractControl): { [key: string]: boolean } | null => { + if (options.filter(type => type === control.value).length === 0) { + return {'type': false}; + } + return null; + } + } + + public static validRoute(pages: any[], field: string, initial: string = null): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if(control.value) { + if(!new RegExp(this.routeRegex).test(control.value)) { + return {error: 'Route should contain only letters or numbers, e.g /route or route'} + } + if(pages && pages.length > 0 && control.value !== initial) { + const forbidden = pages.filter(page => page[field].replace('/', '') === control.value.replace('/', '')).length > 0; + return forbidden ? {error: 'This route is used by an other page'} : null; + } + } + return null; + }; + } + + public static sliceString(mystr, size: number): string { + const sliced = String(mystr).substr(0, size); + return sliced + (String(mystr).length > size ? '...' : ''); + } + + /** + * Splits a text to words base on a list of separators. Returns the words of the text including the separators. + * DO NOT TOUCH, IT WORKS + * + * @param text + * @param separators + */ + public static split(text: string, separators: string[]): string[] { + let words: (string | string[])[] = [text]; + separators.forEach(separator => { + words.forEach((word, index) => { + if (typeof word === "string" && separators.indexOf(word) === -1) { + let tokens: string[] = word.split(separator).filter(value => value !== ''); + if (tokens.length > 1) { + words[index] = []; + tokens.forEach((token, i) => { + ((words[index])).push(token); + if (i !== (tokens.length - 1)) { + ((words[index])).push(separator); + } + }); + } + } + }); + words = [].concat.apply([], words); + }); + return words; + } + + public static capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); + } + + /** + * Checks if a text contains a word + */ + public static containsWord(text: string, word: string): boolean { + return (text && text.toLowerCase().includes(word)); + } + + public static URLSegmentsToPath(segments: UrlSegment[]): string { + let path = ''; + segments.forEach(route => { + path += '/' + route.path; + }) + return path; + } + + public static isEuropeanCountry(country: string) { + let countries = ["Albania", "Andorra", "Armenia", "Austria", "Azerbaijan", "Belarus", "Belgium", "Bosnia and Herzegovina", + "Bulgaria", "Croatia", "Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Georgia", "Germany", "Greece", "Hungary", "Iceland", "Ireland", + "Italy", "Kosovo", "Latvia", "Liechtenstein", "Lithuania", "Luxembourg", "Macedonia", "Malta", "Moldova", "Monaco", "Montenegro", "The Netherlands", "Norway", "Poland", + "Portugal", "Romania", "Russia", "San Marino", "Serbia", "Slovakia", "Slovenia", "Spain", "Sweden", "Switzerland", "Turkey", "Ukraine", "United Kingdom", "Vatican City", + ]; + return (country && countries.indexOf(country) != -1); + } + + public static isOpenAIREID(id: string) { + if (id && id.length == 46) { + let exp1 = /^.{12}::([0-9a-z]{32})$/g; + return (id.match(exp1) != null); + } + return false; + } + public static HTMLToString( html:string){ + try { + html = html.replace(/ /g, ' '); + html = html.replace(/(\r\n|\n|\r| +(?= ))|\s\s+/gm, " "); + html = html.replace(/<[^>]*>/g, ''); + }catch( e){ + } + return html; + } + + public static inValidYearValidator(minYear, maxYear): ValidatorFn { + return (control: AbstractControl): {[key: string]: any} | null => { + return ((control.value && !Dates.isValidYear(control.value, minYear, maxYear)) ? + {error: 'Year must be between ' + minYear + ' and ' + maxYear + '.'} : null); + }; + } + + public static fromYearAfterToYearValidator: ValidatorFn = (control: UntypedFormGroup): ValidationErrors | null => { + const yearFrom = control.get('yearFrom'); + const yearTo = control.get('yearTo'); + return ((yearFrom && yearTo && (parseInt(yearFrom.value, 10) > parseInt(yearTo.value, 10))) ? + {error: 'Starting year must be greater than or equal to ending year.' } : null); + } + + public static rangeRequired( enabled:boolean): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const yearFrom = control.get('yearFrom'); + const yearTo = control.get('yearTo'); + return ((yearFrom && yearTo && enabled && (yearFrom.value == "" || yearTo.value == "")) ? { error: 'Both starting and ending year are required' } : null); + }; + } +}