001/* 002 * Copyright 2020-2021 Global Biodiversity Information Facility (GBIF) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.gbif.api.model.registry; 017 018import io.swagger.v3.oas.annotations.media.Schema; 019 020import org.gbif.api.vocabulary.ContactType; 021import org.gbif.api.vocabulary.Country; 022 023import java.net.URI; 024import java.util.ArrayList; 025import java.util.Date; 026import java.util.List; 027import java.util.Objects; 028import java.util.StringJoiner; 029import java.util.stream.Collectors; 030import java.util.stream.Stream; 031 032import javax.annotation.Nullable; 033import javax.validation.constraints.NotNull; 034import javax.validation.constraints.Null; 035import javax.validation.constraints.Size; 036 037import org.apache.commons.lang3.StringUtils; 038 039// TODO: Should have a cross-field validation for key & created 040@SuppressWarnings({"unused", "LombokSetterMayBeUsed", "LombokGetterMayBeUsed"}) 041public class Contact implements Address, LenientEquals<Contact> { 042 043 @Schema( 044 description = "Identifier for the contact", 045 accessMode = Schema.AccessMode.READ_ONLY 046 ) 047 private Integer key; 048 049 @Schema( 050 description = "The type of contact." 051 ) 052 private ContactType type; 053 054 @Schema( 055 description = "Whether this is the primary contact for the associated entity." 056 ) 057 private boolean primary; 058 059 @Schema( 060 description = "A list of user identifiers for this contact." 061 ) 062 private List<String> userId = new ArrayList<>(); 063 064 @Schema( 065 description = "The salutation is used in addressing an individual with a particular title, " + 066 "such as Dr., Ms., Mrs., Mr., etc." 067 ) 068 private String salutation; 069 070 @Schema( 071 description = "The personal name of the contact." 072 ) 073 private String firstName; 074 075 @Schema( 076 description = "The family name of the contact." 077 ) 078 private String lastName; 079 080 @Schema( 081 description = "The contact's position, job title or similar within the `organization`." 082 ) 083 private List<String> position = new ArrayList<>(); 084 085 @Schema( 086 description = "A description of this contact." 087 ) 088 private String description; 089 090 @Schema( 091 description = "Email addresses associated with this contact." 092 ) 093 private List<String> email = new ArrayList<>(); 094 095 @Schema( 096 description = "Telephone numbers associated with this contact." 097 ) 098 private List<String> phone = new ArrayList<>(); 099 100 @Schema( 101 description = "Homepages with further details on the contact." 102 ) 103 private List<URI> homepage = new ArrayList<>(); 104 105 @Schema( 106 description = "The organization (e.g. employer) associated with this contact." 107 ) 108 private String organization; 109 110 @Schema( 111 description = "Address lines other than the city, province, country and" + 112 "postal code, which have their own fields." 113 ) 114 private List<String> address = new ArrayList<>(); 115 116 @Schema( 117 description = "The city or similar line of the contact's address." 118 ) 119 private String city; 120 121 @Schema( 122 description = "The province or similar line of the contact's address." 123 ) 124 private String province; 125 126 @Schema( 127 description = "The country or other region of the contact's address." 128 ) 129 private Country country; 130 131 @Schema( 132 description = "The postal code or similar line of the contact's address." 133 ) 134 private String postalCode; 135 136 @Schema( 137 description = "The GBIF username of the creator of the contact", 138 accessMode = Schema.AccessMode.READ_ONLY 139 ) 140 private String createdBy; 141 142 @Schema( 143 description = "The GBIF username of the last user to modify the contact", 144 accessMode = Schema.AccessMode.READ_ONLY 145 ) 146 private String modifiedBy; 147 148 @Schema( 149 description = "Timestamp of when the contact was created", 150 accessMode = Schema.AccessMode.READ_ONLY 151 ) 152 private Date created; 153 154 @Schema( 155 description = "Timestamp of when the contact was last modified", 156 accessMode = Schema.AccessMode.READ_ONLY 157 ) 158 private Date modified; 159 160 @Null(groups = {PrePersist.class}) 161 @NotNull(groups = {PostPersist.class}) 162 public Integer getKey() { 163 return key; 164 } 165 166 public void setKey(Integer key) { 167 this.key = key; 168 } 169 170 @Nullable 171 public ContactType getType() { 172 return type; 173 } 174 175 public void setType(ContactType type) { 176 this.type = type; 177 } 178 179 public boolean isPrimary() { 180 return primary; 181 } 182 183 public void setPrimary(boolean primary) { 184 this.primary = primary; 185 } 186 187 public List<String> getUserId() { 188 return userId; 189 } 190 191 public void setUserId(List<String> userId) { 192 this.userId = userId; 193 } 194 195 public void addUserId(String userId) { 196 this.userId.add(userId); 197 } 198 199 /** 200 * Adds a new user id that is assembled from a directory name and a local id within it. 201 * Format used by EML, though see https://github.com/gbif/gbif-api/issues/30. 202 * The directory should be a valid URI, if it's not, it will be ignored by this method. 203 * 204 * @param directory identifier for the directory, preferably a URL domain like http://orcid.org 205 * @param id the identifier in that directory 206 */ 207 public void addUserId(String directory, String id) { 208 if (StringUtils.isNotEmpty(id)) { 209 if (StringUtils.isEmpty(directory)) { 210 userId.add(id); 211 } else { 212 try { 213 URI dir = URI.create(directory); 214 if (dir.isAbsolute()) { 215 String dir2 = dir.toString(); 216 if (!dir2.endsWith("/") && !dir2.endsWith("=")) { 217 dir2 = dir2 + "/"; 218 } 219 220 // Check if the id is already prefixed with the directory URI, either HTTP or HTTPS. 221 //noinspection HttpUrlsUsage 222 if (id.startsWith(dir2) 223 || id.startsWith(dir2.replace("http://", "https://")) 224 || id.startsWith(dir2.replace("https://", "http://"))) { 225 userId.add(id); 226 // Check if the id is prefixed with the hostname. 227 } else if (id.startsWith(dir.getHost())) { 228 userId.add(dir.getScheme() + "://" + id); 229 } else { 230 userId.add(dir2 + id); 231 } 232 } else { 233 if (id.startsWith(dir.toString())) { 234 userId.add(id); 235 } else { 236 userId.add(dir + ":" + id); 237 } 238 } 239 } catch (IllegalArgumentException iaEx) { 240 // in case the directory is not a valid URL keep only the user id 241 userId.add(id); 242 } 243 } 244 } 245 } 246 247 @Nullable 248 public String getSalutation() { 249 return salutation; 250 } 251 252 public void setSalutation(String salutation) { 253 this.salutation = salutation; 254 } 255 256 @Nullable 257 @Size(min = 1) 258 public String getFirstName() { 259 return firstName; 260 } 261 262 public void setFirstName(String firstName) { 263 this.firstName = firstName; 264 } 265 266 @Nullable 267 @Size(min = 1) 268 public String getLastName() { 269 return lastName; 270 } 271 272 public void setLastName(String lastName) { 273 this.lastName = lastName; 274 } 275 276 /** 277 * Compute and returns the complete name in the form: FirstName LastName. 278 * Since all parts are optional, this method can return an empty string (but never null) 279 * 280 * @return the non-empty parts of FirstName LastName or empty string if none 281 */ 282 public String computeCompleteName() { 283 return Stream.of(firstName, lastName) 284 .map(StringUtils::trimToNull) 285 .filter(Objects::nonNull) 286 .collect(Collectors.joining(" ")); 287 } 288 289 public List<String> getPosition() { 290 return position; 291 } 292 293 public void setPosition(List<String> position) { 294 this.position = position; 295 } 296 297 public void addPosition(String position) { 298 this.position.add(position); 299 } 300 301 @Nullable 302 public String getDescription() { 303 return description; 304 } 305 306 public void setDescription(String description) { 307 this.description = description; 308 } 309 310 @Override 311 public List<String> getEmail() { 312 return email; 313 } 314 315 @Override 316 public void setEmail(List<String> email) { 317 this.email = email; 318 } 319 320 public void addEmail(String email) { 321 this.email.add(email); 322 } 323 324 @Override 325 public List<String> getPhone() { 326 return phone; 327 } 328 329 @Override 330 public void setPhone(List<String> phone) { 331 this.phone = phone; 332 } 333 334 public void addPhone(String phone) { 335 this.phone.add(phone); 336 } 337 338 @Override 339 public List<String> getAddress() { 340 return address; 341 } 342 343 @Override 344 public void setAddress(List<String> address) { 345 this.address = address; 346 } 347 348 public void addAddress(String address) { 349 this.address.add(address); 350 } 351 352 @Override 353 public String getCity() { 354 return city; 355 } 356 357 @Override 358 public void setCity(String city) { 359 this.city = city; 360 } 361 362 @Override 363 public String getProvince() { 364 return province; 365 } 366 367 @Override 368 public void setProvince(String province) { 369 this.province = province; 370 } 371 372 @Override 373 public Country getCountry() { 374 return country; 375 } 376 377 @Override 378 public void setCountry(Country country) { 379 this.country = country; 380 } 381 382 @Override 383 public String getPostalCode() { 384 return postalCode; 385 } 386 387 @Override 388 public void setPostalCode(String postalCode) { 389 this.postalCode = postalCode; 390 } 391 392 @Override 393 @Nullable 394 @Size(min = 2) 395 public String getOrganization() { 396 return organization; 397 } 398 399 @Override 400 public void setOrganization(String organization) { 401 this.organization = organization; 402 } 403 404 @Override 405 public List<URI> getHomepage() { 406 return homepage; 407 } 408 409 @Override 410 public void setHomepage(List<URI> homepage) { 411 this.homepage = homepage; 412 } 413 414 public void addHomepage(URI homepage) { 415 this.homepage.add(homepage); 416 } 417 418 @Size(min = 3) 419 public String getCreatedBy() { 420 return createdBy; 421 } 422 423 public void setCreatedBy(String createdBy) { 424 this.createdBy = createdBy; 425 } 426 427 @Size(min = 3) 428 public String getModifiedBy() { 429 return modifiedBy; 430 } 431 432 public void setModifiedBy(String modifiedBy) { 433 this.modifiedBy = modifiedBy; 434 } 435 436 @Null(groups = {PrePersist.class}) 437 @NotNull(groups = {PostPersist.class}) 438 public Date getCreated() { 439 return created; 440 } 441 442 public void setCreated(Date created) { 443 this.created = created; 444 } 445 446 @Null(groups = {PrePersist.class}) 447 @NotNull(groups = {PostPersist.class}) 448 public Date getModified() { 449 return modified; 450 } 451 452 public void setModified(Date modified) { 453 this.modified = modified; 454 } 455 456 @Override 457 public boolean equals(Object o) { 458 if (this == o) { 459 return true; 460 } 461 if (o == null || getClass() != o.getClass()) { 462 return false; 463 } 464 Contact contact = (Contact) o; 465 return primary == contact.primary 466 && Objects.equals(key, contact.key) 467 && type == contact.type 468 && Objects.equals(userId, contact.userId) 469 && Objects.equals(salutation, contact.salutation) 470 && Objects.equals(firstName, contact.firstName) 471 && Objects.equals(lastName, contact.lastName) 472 && Objects.equals(position, contact.position) 473 && Objects.equals(description, contact.description) 474 && Objects.equals(email, contact.email) 475 && Objects.equals(phone, contact.phone) 476 && Objects.equals(homepage, contact.homepage) 477 && Objects.equals(organization, contact.organization) 478 && Objects.equals(address, contact.address) 479 && Objects.equals(city, contact.city) 480 && Objects.equals(province, contact.province) 481 && country == contact.country 482 && Objects.equals(postalCode, contact.postalCode) 483 && Objects.equals(createdBy, contact.createdBy) 484 && Objects.equals(modifiedBy, contact.modifiedBy) 485 && Objects.equals(created, contact.created) 486 && Objects.equals(modified, contact.modified); 487 } 488 489 @Override 490 public int hashCode() { 491 return Objects.hash( 492 key, 493 type, 494 primary, 495 userId, 496 salutation, 497 firstName, 498 lastName, 499 position, 500 description, 501 email, 502 phone, 503 homepage, 504 organization, 505 address, 506 city, 507 province, 508 country, 509 postalCode, 510 createdBy, 511 modifiedBy, 512 created, 513 modified); 514 } 515 516 @Override 517 public String toString() { 518 return new StringJoiner(", ", Contact.class.getSimpleName() + "[", "]") 519 .add("key=" + key) 520 .add("type=" + type) 521 .add("primary=" + primary) 522 .add("userId=" + userId) 523 .add("salutation='" + salutation + "'") 524 .add("firstName='" + firstName + "'") 525 .add("lastName='" + lastName + "'") 526 .add("position=" + position) 527 .add("description='" + description + "'") 528 .add("email=" + email) 529 .add("phone=" + phone) 530 .add("homepage=" + homepage) 531 .add("organization='" + organization + "'") 532 .add("address=" + address) 533 .add("city='" + city + "'") 534 .add("province='" + province + "'") 535 .add("country=" + country) 536 .add("postalCode='" + postalCode + "'") 537 .add("createdBy='" + createdBy + "'") 538 .add("modifiedBy='" + modifiedBy + "'") 539 .add("created=" + created) 540 .add("modified=" + modified) 541 .toString(); 542 } 543 544 /** 545 * This implementation of the {@link #equals(Object)} method does only check <em>business equality</em> and disregards 546 * automatically set and maintained fields like {@code createdBy, key} and others. 547 */ 548 @Override 549 public boolean lenientEquals(Contact contact) { 550 if (this == contact) { 551 return true; 552 } 553 554 return Objects.equals(type, contact.type) 555 && Objects.equals(primary, contact.primary) 556 && Objects.equals(userId, contact.userId) 557 && Objects.equals(salutation, contact.salutation) 558 && Objects.equals(firstName, contact.firstName) 559 && Objects.equals(lastName, contact.lastName) 560 && Objects.equals(position, contact.position) 561 && Objects.equals(description, contact.description) 562 && Objects.equals(email, contact.email) 563 && Objects.equals(phone, contact.phone) 564 && Objects.equals(homepage, contact.homepage) 565 && Objects.equals(organization, contact.organization) 566 && Objects.equals(address, contact.address) 567 && Objects.equals(city, contact.city) 568 && Objects.equals(province, contact.province) 569 && Objects.equals(country, contact.country) 570 && Objects.equals(postalCode, contact.postalCode); 571 } 572}