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}