// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package google.registry.model.registrar; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Sets.difference; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; import static java.util.stream.Collectors.joining; import com.google.common.base.Enums; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Streams; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Index; import com.googlecode.objectify.annotation.Parent; import google.registry.model.Buildable; import google.registry.model.ImmutableObject; import google.registry.model.JsonMapBuilder; import google.registry.model.Jsonifiable; import google.registry.model.annotations.ReportedOn; import java.util.Arrays; import java.util.Map; import java.util.Set; /** * A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only * enable key equality. * *

IMPORTANT NOTE: Any time that you change, update, or delete RegistrarContact entities, you * *MUST* also modify the persisted Registrar entity with {@link Registrar#contactsRequireSyncing} * set to true. */ @ReportedOn @Entity public class RegistrarContact extends ImmutableObject implements Jsonifiable { @Parent Key parent; /** * Registrar contacts types for partner communication tracking. * *

Note: These types only matter to the registry. They are not meant to be used for * WHOIS or RDAP results. */ public enum Type { ABUSE("abuse", true), ADMIN("primary", true), BILLING("billing", true), LEGAL("legal", true), MARKETING("marketing", false), TECH("technical", true), WHOIS("whois-inquiry", true); private final String displayName; private final boolean required; public String getDisplayName() { return displayName; } public boolean isRequired() { return required; } Type(String display, boolean required) { this.displayName = display; this.required = required; } } /** The name of the contact. */ String name; /** The email address of the contact. */ @Id String emailAddress; /** The voice number of the contact. */ String phoneNumber; /** The fax number of the contact. */ String faxNumber; /** * Multiple types are used to associate the registrar contact with * various mailing groups. This data is internal to the registry. */ Set types; /** * A GAE user ID allowed to act as this registrar contact. * *

This can be derived from a known email address using http://email-to-gae-id.appspot.com. * * @see com.google.appengine.api.users.User#getUserId() */ @Index String gaeUserId; /** * Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact. */ boolean visibleInWhoisAsAdmin = false; /** * Whether this contact is publicly visible in WHOIS registrar query results as a Technical * contact. */ boolean visibleInWhoisAsTech = false; /** * Whether this contact's phone number and email address is publicly visible in WHOIS domain query * results as registrar abuse contact info. */ boolean visibleInDomainWhoisAsAbuse = false; public static ImmutableSet typesFromCSV(String csv) { return typesFromStrings(Arrays.asList(csv.split(","))); } public static ImmutableSet typesFromStrings(Iterable typeNames) { return Streams.stream(typeNames) .map(Enums.stringConverter(Type.class)) .collect(toImmutableSet()); } /** * Helper to update the contacts associated with a Registrar. This requires querying for the * existing contacts, deleting existing contacts that are not part of the given {@code contacts} * set, and then saving the given {@code contacts}. * *

IMPORTANT NOTE: If you call this method then it is your responsibility to also persist the * relevant Registrar entity with the {@link Registrar#contactsRequireSyncing} field set to true. */ public static void updateContacts( final Registrar registrar, final Set contacts) { ofy() .transact( () -> { ofy() .delete() .keys( difference( ImmutableSet.copyOf( ofy().load().type(RegistrarContact.class).ancestor(registrar).keys()), contacts.stream().map(Key::create).collect(toImmutableSet()))); ofy().save().entities(contacts); }); } public Key getParent() { return parent; } public String getName() { return name; } public String getEmailAddress() { return emailAddress; } public String getPhoneNumber() { return phoneNumber; } public String getFaxNumber() { return faxNumber; } public ImmutableSortedSet getTypes() { return nullToEmptyImmutableSortedCopy(types); } public boolean getVisibleInWhoisAsAdmin() { return visibleInWhoisAsAdmin; } public boolean getVisibleInWhoisAsTech() { return visibleInWhoisAsTech; } public boolean getVisibleInDomainWhoisAsAbuse() { return visibleInDomainWhoisAsAbuse; } public String getGaeUserId() { return gaeUserId; } public Builder asBuilder() { return new Builder(clone(this)); } /** * Returns a string representation that's human friendly. * *

The output will look something like this:

   {@code
   *
   *   Some Person
   *   person@example.com
   *   Tel: +1.2125650666
   *   Types: [ADMIN, WHOIS]
   *   Visible in WHOIS as Admin contact: Yes
   *   Visible in WHOIS as Technical contact: No
   *   GAE-UserID: 1234567890
   *   Registrar-Console access: Yes}
*/ public String toStringMultilinePlainText() { StringBuilder result = new StringBuilder(256); result.append(getName()).append('\n'); result.append(getEmailAddress()).append('\n'); if (phoneNumber != null) { result.append("Tel: ").append(getPhoneNumber()).append('\n'); } if (faxNumber != null) { result.append("Fax: ").append(getFaxNumber()).append('\n'); } result.append("Types: ").append(getTypes()).append('\n'); result .append("Visible in registrar WHOIS query as Admin contact: ") .append(getVisibleInWhoisAsAdmin() ? "Yes" : "No") .append('\n'); result .append("Visible in registrar WHOIS query as Technical contact: ") .append(getVisibleInWhoisAsTech() ? "Yes" : "No") .append('\n'); result .append( "Phone number and email visible in domain WHOIS query as " + "Registrar Abuse contact info: ") .append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No") .append('\n'); result .append("Registrar-Console access: ") .append(getGaeUserId() != null ? "Yes" : "No") .append('\n'); if (getGaeUserId() != null) { result.append("GAE-UserID: ").append(getGaeUserId()).append('\n'); } return result.toString(); } @Override public Map toJsonMap() { return new JsonMapBuilder() .put("name", name) .put("emailAddress", emailAddress) .put("phoneNumber", phoneNumber) .put("faxNumber", faxNumber) .put("types", getTypes().stream().map(Object::toString).collect(joining(","))) .put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin) .put("visibleInWhoisAsTech", visibleInWhoisAsTech) .put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse) .put("gaeUserId", gaeUserId) .build(); } /** A builder for constructing a {@link RegistrarContact}, since it is immutable. */ public static class Builder extends Buildable.Builder { public Builder() {} private Builder(RegistrarContact instance) { super(instance); } public Builder setParent(Registrar parent) { return this.setParent(Key.create(parent)); } public Builder setParent(Key parentKey) { getInstance().parent = parentKey; return this; } /** Build the registrar, nullifying empty fields. */ @Override public RegistrarContact build() { checkNotNull(getInstance().parent, "Registrar parent cannot be null"); checkNotNull(getInstance().emailAddress, "Email address cannot be null"); return cloneEmptyToNull(super.build()); } public Builder setName(String name) { getInstance().name = name; return this; } public Builder setEmailAddress(String emailAddress) { getInstance().emailAddress = emailAddress; return this; } public Builder setPhoneNumber(String phoneNumber) { getInstance().phoneNumber = phoneNumber; return this; } public Builder setFaxNumber(String faxNumber) { getInstance().faxNumber = faxNumber; return this; } public Builder setTypes(Iterable types) { getInstance().types = ImmutableSet.copyOf(types); return this; } public Builder setVisibleInWhoisAsAdmin(boolean visible) { getInstance().visibleInWhoisAsAdmin = visible; return this; } public Builder setVisibleInWhoisAsTech(boolean visible) { getInstance().visibleInWhoisAsTech = visible; return this; } public Builder setVisibleInDomainWhoisAsAbuse(boolean visible) { getInstance().visibleInDomainWhoisAsAbuse = visible; return this; } public Builder setGaeUserId(String gaeUserId) { getInstance().gaeUserId = gaeUserId; return this; } } }