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}