1
|
/**
|
2
|
* Copyright (C) 2007 EDIT
|
3
|
* European Distributed Institute of Taxonomy
|
4
|
* http://www.e-taxonomy.eu
|
5
|
*
|
6
|
* The contents of this file are subject to the Mozilla Public License Version 1.1
|
7
|
* See LICENSE.TXT at the top of this package for the full license terms.
|
8
|
*/
|
9
|
package eu.etaxonomy.cdm.model.agent;
|
10
|
|
11
|
import java.util.regex.Matcher;
|
12
|
import java.util.regex.Pattern;
|
13
|
|
14
|
import org.apache.commons.lang3.StringUtils;
|
15
|
|
16
|
import eu.etaxonomy.cdm.common.UTF8;
|
17
|
|
18
|
|
19
|
/**
|
20
|
* A class for handling ORCIDs (http://https://orcid.org/, https://support.orcid.org/hc/en-us/articles/360006897674).
|
21
|
* It offers parsing and formatting functionality as well as validation.
|
22
|
* A {@link ORCID} object can only be created by syntactic valid input.
|
23
|
* It internally stores 1 strings (length=16).
|
24
|
*
|
25
|
*
|
26
|
* @author a.mueller
|
27
|
* @since 2019-11-08
|
28
|
*/
|
29
|
public final class ORCID implements java.io.Serializable{
|
30
|
|
31
|
/**
|
32
|
* Explicit serialVersionUID for interoperability.
|
33
|
*/
|
34
|
private static final long serialVersionUID = 4304992020966546747L;
|
35
|
|
36
|
public static final String ORCID_ORG = "orcid.org/";
|
37
|
|
38
|
/**
|
39
|
* The default public ORCID proxy server
|
40
|
*/
|
41
|
public static final String HTTP_ORCID_ORG = "https://" + ORCID_ORG;
|
42
|
|
43
|
private volatile transient int hashCode = -1; // Zero ==> undefined
|
44
|
|
45
|
|
46
|
//********************************* VARIABLES *************************************/
|
47
|
|
48
|
/**
|
49
|
* The base digits without prefix, checksum and hyphens("-")
|
50
|
*/
|
51
|
private String baseNumber;
|
52
|
|
53
|
/**
|
54
|
* The checksum.
|
55
|
* @see #checkDigit()
|
56
|
*/
|
57
|
private String checkSum;
|
58
|
|
59
|
// ***************************** FACTORY METHODS ***************************************/
|
60
|
|
61
|
public static ORCID fromString(String orcid) throws IllegalArgumentException{
|
62
|
return new ORCID(orcid);
|
63
|
}
|
64
|
|
65
|
// ******************************* CONSTRUCTOR ************************************/
|
66
|
|
67
|
private ORCID(){} //empty constructor required for JAXB
|
68
|
|
69
|
// /**
|
70
|
// * Creates a doi by its registrantCode and its suffix
|
71
|
// * @param registrantCode the registrant code, the is the part following the directoryIndicator "10."
|
72
|
// * and preceding the first forward slash (followed by the suffix)
|
73
|
// * @param suffix the suffix is the part of the DOI following the first forward slash. It is provided
|
74
|
// * by the registrant
|
75
|
// */
|
76
|
// private ORCID(String registrantCode, String suffix) {
|
77
|
// //preliminary until prefix_registrantCode and suffix validation is implemented
|
78
|
// this("10." + registrantCode + "/" + suffix);
|
79
|
//
|
80
|
// //use only after validation of both parts
|
81
|
//// this.prefix_registrantCode = registrantCode;
|
82
|
//// this.suffix = suffix;
|
83
|
// }
|
84
|
|
85
|
private ORCID(String doiString) {
|
86
|
parseOrcidString(doiString);
|
87
|
}
|
88
|
|
89
|
//************************************ GETTER ***********************************/
|
90
|
|
91
|
|
92
|
/**
|
93
|
* The pure number representation, including the checksum (this maybe 'X'
|
94
|
* so it's not only digits
|
95
|
* @return
|
96
|
*/
|
97
|
public String getDigitsOnly() {
|
98
|
return baseNumber+checkSum;
|
99
|
}
|
100
|
|
101
|
|
102
|
// ********************************************* PARSER *******************************/
|
103
|
|
104
|
private static Pattern orcidPattern = Pattern.compile("^(\\d{4}("+UTF8.ANY_DASH_RE()+")?){3}\\d{3}[0-9Xx]?$");
|
105
|
|
106
|
private void parseOrcidString(String orcid){
|
107
|
if (StringUtils.isBlank(orcid)){
|
108
|
throw new IllegalArgumentException("ORCID string must not be null or blank");
|
109
|
}
|
110
|
orcid = orcid.trim();
|
111
|
if (orcid.startsWith("http:") ){
|
112
|
orcid = orcid.replaceFirst("http:", "https:").trim(); //https is the current display standard
|
113
|
}
|
114
|
|
115
|
//replace URI prefix
|
116
|
if (orcid.startsWith(HTTP_ORCID_ORG)){
|
117
|
orcid = orcid.replaceFirst(HTTP_ORCID_ORG, "");
|
118
|
}else if (orcid.startsWith(ORCID_ORG)){
|
119
|
orcid = orcid.replaceFirst(ORCID_ORG, "");
|
120
|
}
|
121
|
|
122
|
//now we should have the pure orcid
|
123
|
if (orcid.length() != 15 && orcid.length() != 16 && orcid.length() != 18 && orcid.length() != 19){
|
124
|
//for persistence reason we currently restrict the length of DOIs to 1000
|
125
|
throw new IllegalArgumentException("ORCIDs must have exactly 16 digits. 3 dashes ('-') may be included after each group of 4 digits.");
|
126
|
}
|
127
|
|
128
|
Matcher matcher = orcidPattern.matcher(orcid);
|
129
|
if (!matcher.find()){
|
130
|
throw new IllegalArgumentException("ORCID can not be parsed. It must have exactly 16 digits. 3 dashes ('-') may be included after each group of 4 digits.");
|
131
|
}
|
132
|
|
133
|
orcid = orcid.replaceAll(UTF8.ANY_DASH_RE(), "");
|
134
|
|
135
|
if (orcid.length() == 16){
|
136
|
this.baseNumber = orcid.substring(0, 15);
|
137
|
this.checkSum = orcid.substring(15);
|
138
|
if (!checkDigit(baseNumber).equals(checkSum)){
|
139
|
throw new IllegalArgumentException("ORCID checksum not correct (last digit is checksum, see https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier).");
|
140
|
}
|
141
|
}else{
|
142
|
this.baseNumber = orcid;
|
143
|
this.checkSum = checkDigit(baseNumber);
|
144
|
}
|
145
|
}
|
146
|
|
147
|
private String makeOrcid(){
|
148
|
return baseNumber.substring(0,4) + "-" + baseNumber.substring(4,8) + "-"
|
149
|
+ baseNumber.substring(8,12) + "-" + baseNumber.substring(12,15) + checkSum;
|
150
|
}
|
151
|
|
152
|
public String asURI(){
|
153
|
return HTTP_ORCID_ORG + makeOrcid();
|
154
|
}
|
155
|
|
156
|
/**
|
157
|
* Generates check digit as per ISO 7064 11,2.
|
158
|
* (code from https://support.orcid.org/hc/en-us/articles/360006897674)
|
159
|
*/
|
160
|
public String checkDigit(){
|
161
|
return checkDigit(baseNumber);
|
162
|
}
|
163
|
|
164
|
/**
|
165
|
* @see #checkDigit()
|
166
|
* @param baseDigits the base digits without the checkSum digit
|
167
|
*/
|
168
|
private static String checkDigit(String baseDigits) {
|
169
|
int total = 0;
|
170
|
for (int i = 0; i < baseDigits.length(); i++) {
|
171
|
int digit = Character.getNumericValue(baseDigits.charAt(i));
|
172
|
total = (total + digit) * 2;
|
173
|
}
|
174
|
int remainder = total % 11;
|
175
|
int result = (12 - remainder) % 11;
|
176
|
return result == 10 ? "X" : String.valueOf(result);
|
177
|
}
|
178
|
|
179
|
//************************************************* toString/equals /hashCode *********************/
|
180
|
|
181
|
@Override
|
182
|
public int hashCode() {
|
183
|
if (hashCode == -1) {
|
184
|
hashCode = 31 * baseNumber.hashCode();
|
185
|
}
|
186
|
return hashCode;
|
187
|
}
|
188
|
|
189
|
@Override
|
190
|
public boolean equals(Object obj) {
|
191
|
if (obj instanceof ORCID){
|
192
|
return this.baseNumber.equals(((ORCID)obj).baseNumber) &&
|
193
|
this.checkSum.equals(((ORCID)obj).checkSum);
|
194
|
}
|
195
|
return false;
|
196
|
}
|
197
|
|
198
|
@Override
|
199
|
public String toString(){
|
200
|
return asURI();
|
201
|
}
|
202
|
}
|