|
5 | 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
6 | 6 | import com.fasterxml.jackson.core.JsonParseException; |
7 | 7 | import com.fasterxml.jackson.databind.JsonMappingException; |
| 8 | +import com.fasterxml.jackson.databind.JsonNode; |
8 | 9 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 10 | +import com.fasterxml.jackson.databind.node.ObjectNode; |
9 | 11 | import com.google.i18n.phonenumbers.NumberParseException; |
10 | 12 | import com.google.i18n.phonenumbers.PhoneNumberUtil; |
11 | 13 | import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; |
|
14 | 16 | import gov.osti.repository.SubversionRepository; |
15 | 17 | import java.io.IOException; |
16 | 18 | import java.io.Serializable; |
| 19 | +import java.math.BigDecimal; |
17 | 20 | import java.net.URLEncoder; |
| 21 | +import java.util.regex.Matcher; |
18 | 22 | import java.util.regex.Pattern; |
19 | 23 | import javax.servlet.ServletContext; |
20 | 24 | import javax.servlet.http.HttpServletRequest; |
@@ -108,6 +112,7 @@ public class Validation { |
108 | 112 | protected static final Pattern EMAIL_PATTERN = Pattern.compile("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"); |
109 | 113 | protected static final Pattern URL_PATTERN = Pattern.compile("\\bhttps?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); |
110 | 114 | protected static final Pattern DOI_PATTERN = Pattern.compile("10.\\d{4,9}/[-._;()/:A-Za-z0-9]+$"); |
| 115 | + protected static final Pattern ORCID_PATTERN = Pattern.compile("(?i)^\\s*(?:(?:https?:\\/\\/)?orcid\\.org\\/)?(\\d{4}(?:-?\\d{4}){3})\\s*$"); |
111 | 116 |
|
112 | 117 | @JsonIgnoreProperties (ignoreUnknown = true) |
113 | 118 | private static class ValidationRequest implements Serializable { |
@@ -157,6 +162,85 @@ public void setError(String error) { |
157 | 162 | this.error = error; |
158 | 163 | } |
159 | 164 | } |
| 165 | + |
| 166 | + /** |
| 167 | + * Generates check digit as per ISO 7064 11,2. (from orcid.org) |
| 168 | + * @param value the value to check. |
| 169 | + * @return string the checksum digit of the value. |
| 170 | + */ |
| 171 | + private static String generateCheckDigit(String value) { |
| 172 | + int total = 0; |
| 173 | + for (int i = 0; i < value.length(); i++) { |
| 174 | + int digit = Character.getNumericValue(value.charAt(i)); |
| 175 | + total = (total + digit) * 2; |
| 176 | + } |
| 177 | + int remainder = total % 11; |
| 178 | + int result = (12 - remainder) % 11; |
| 179 | + return result == 10 ? "X" : String.valueOf(result); |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Strip a string down to the known ORCID 16 digit value |
| 184 | + * @param value the value to format. |
| 185 | + * @return string the stripped down base 16 digits of the input ORCID, or null if value is invalid ORCID. |
| 186 | + */ |
| 187 | + private static String stripBaseORCID(String value) { |
| 188 | + if (value == null) |
| 189 | + return null; |
| 190 | + |
| 191 | + // strip out valid 16 digit numeric |
| 192 | + Matcher matcher = ORCID_PATTERN.matcher(value); |
| 193 | + |
| 194 | + if (matcher.find()) |
| 195 | + // return matching 16 digit characters |
| 196 | + return matcher.group(1).replaceAll("-", ""); |
| 197 | + else |
| 198 | + // no valid ORCID pattern |
| 199 | + return null; |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Return a JSON of various formats of the ORCID value |
| 204 | + * @param value the value to format. |
| 205 | + * @return ObjectNode with three formats [base, dashed, url] of the value, or NULL if value is invalid ORCID. |
| 206 | + */ |
| 207 | + private static ObjectNode formatORCID(String value) { |
| 208 | + String baseValue = stripBaseORCID(value); |
| 209 | + |
| 210 | + ObjectNode theNode = mapper.createObjectNode(); |
| 211 | + |
| 212 | + if (baseValue == null) { |
| 213 | + theNode.put("original", value); |
| 214 | + return theNode; |
| 215 | + } |
| 216 | + |
| 217 | + String dashedValue = String.format("%1$s-%2$s-%3$s-%4$s", baseValue.substring(0,4), baseValue.substring(4,8), baseValue.substring(8,12), baseValue.substring(12)); |
| 218 | + |
| 219 | + theNode.put("base", baseValue); |
| 220 | + theNode.put("dashed", dashedValue); |
| 221 | + theNode.put("url", "https://orcid.org/" + dashedValue); |
| 222 | + |
| 223 | + return theNode; |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Return a specific format of the ORCID value |
| 228 | + * @param value the value to format. |
| 229 | + * @param format the specific format to return. Valid values [base, dashed, url]. |
| 230 | + * @return string that matches the requested format, or NULL if value is invalid ORCID. |
| 231 | + */ |
| 232 | + public static String formatORCID(String value, String format) { |
| 233 | + ObjectNode theNode = formatORCID(value); |
| 234 | + |
| 235 | + JsonNode originalValue = theNode.get("original"); |
| 236 | + |
| 237 | + // if value is invalid, we get back the original, and if it is NULL, return NULL. |
| 238 | + if (originalValue != null && originalValue.isNull()) |
| 239 | + return null; |
| 240 | + |
| 241 | + // otherwise, return the value of the original, or the formatted value. |
| 242 | + return (theNode.isNull()) ? null : (originalValue != null ? theNode.get("original").asText() : theNode.get(format).asText()); |
| 243 | + } |
160 | 244 |
|
161 | 245 | /** |
162 | 246 | * Creates a new instance of ValidationResource |
@@ -283,6 +367,25 @@ public static boolean isValidDoi(String value) { |
283 | 367 | return (null==value) ? false : DOI_PATTERN.matcher(value).find(); |
284 | 368 | } |
285 | 369 |
|
| 370 | + /** |
| 371 | + * Check to see if ORCID is valid or not. Matches the pattern of a ORCID, and |
| 372 | + * has a valid checksum. |
| 373 | + * |
| 374 | + * @param value the ORCID to check |
| 375 | + * @return true if the ORCID is a valid pattern and passes checksum; false if not |
| 376 | + */ |
| 377 | + public static boolean isValidORCID(String value) { |
| 378 | + value = stripBaseORCID(value); |
| 379 | + if (value == null) |
| 380 | + return false; |
| 381 | + |
| 382 | + // 16 digits found, now validate checksum |
| 383 | + String lastChar = (value.substring(value.length() - 1)); |
| 384 | + String baseValue = (value.substring(0, value.length() - 1)); |
| 385 | + |
| 386 | + return lastChar.equals(generateCheckDigit(baseValue)); |
| 387 | + } |
| 388 | + |
286 | 389 | /** |
287 | 390 | * View the API documentation. |
288 | 391 | * |
@@ -351,6 +454,32 @@ public Response checkDoi(@QueryParam("value") String value) { |
351 | 454 | ErrorResponse.badRequest("\"" + value + "\" is not a valid DOI.").build(); |
352 | 455 | } |
353 | 456 |
|
| 457 | + /** |
| 458 | + * Check a ORCID. |
| 459 | + * |
| 460 | + * Response Codes: |
| 461 | + * 200 - OK, value is valid |
| 462 | + * 400 - Bad Request, value is NOT valid |
| 463 | + * |
| 464 | + * @param value the ORCID to check |
| 465 | + * @return a Response |
| 466 | + */ |
| 467 | + @GET |
| 468 | + @Produces (MediaType.APPLICATION_JSON) |
| 469 | + @Path ("/orcid") |
| 470 | + public Response checkORCID(@QueryParam("value") String value) { |
| 471 | + ObjectNode theNode = mapper.createObjectNode(); |
| 472 | + |
| 473 | + if (isValidORCID(value)) { |
| 474 | + theNode.put("value", "OK"); |
| 475 | + theNode.set("format", formatORCID(value)); |
| 476 | + |
| 477 | + return Response.ok().entity(theNode.toString()).build(); |
| 478 | + } |
| 479 | + else |
| 480 | + return ErrorResponse.badRequest("\"" + value + "\" is not a valid ORCID.").build(); |
| 481 | + } |
| 482 | + |
354 | 483 | /** |
355 | 484 | * Check an email address. |
356 | 485 | * |
@@ -457,6 +586,10 @@ public Response request(String object) throws IOException { |
457 | 586 | req.setError((isValidEmail(req.getValue()) ? "" : req.getValue() + " is not a valid email address.")); |
458 | 587 | } else if (StringUtils.equalsIgnoreCase(req.getType(), "awardnumber")) { |
459 | 588 | req.setError((isValidAwardNumber(req.getValue()) ? "" : req.getValue() + " is not a valid Award Number.")); |
| 589 | + } else if (StringUtils.equalsIgnoreCase(req.getType(), "orcid")) { |
| 590 | + boolean isValid = isValidORCID(req.getValue()); |
| 591 | + req.setError((isValid ? "" : req.getValue() + " is not a valid ORCID.")); |
| 592 | + req.setValue((isValid ? req.getValue() : formatORCID(req.getValue(), "dashed"))); |
460 | 593 | } else { |
461 | 594 | log.warn("Unknown validation request type: " + req.getType()); |
462 | 595 | return ErrorResponse |
|
0 commit comments