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