// 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.export.sheet; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.model.common.Cursor.CursorType.SYNC_REGISTRAR_SHEET; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.registrar.RegistrarContact.Type.ABUSE; import static google.registry.model.registrar.RegistrarContact.Type.ADMIN; import static google.registry.model.registrar.RegistrarContact.Type.BILLING; import static google.registry.model.registrar.RegistrarContact.Type.LEGAL; import static google.registry.model.registrar.RegistrarContact.Type.MARKETING; import static google.registry.model.registrar.RegistrarContact.Type.TECH; import static google.registry.model.registrar.RegistrarContact.Type.WHOIS; import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import google.registry.model.common.Cursor; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarContact; import google.registry.util.Clock; import google.registry.util.DateTimeUtils; import java.io.IOException; import java.util.function.Predicate; import javax.annotation.Nullable; import javax.inject.Inject; import org.joda.time.DateTime; /** * Class for synchronizing all {@link Registrar} Datastore objects to a Google Spreadsheet. * * @see SyncRegistrarsSheetAction */ class SyncRegistrarsSheet { @Inject Clock clock; @Inject SheetSynchronizer sheetSynchronizer; @Inject SyncRegistrarsSheet() {} /** * Returns true if any {@link Registrar} entity was modified since the last time this task * successfully completed, as measured by a cursor. */ boolean wereRegistrarsModified() { Cursor cursor = ofy().load().key(Cursor.createGlobalKey(SYNC_REGISTRAR_SHEET)).now(); DateTime lastUpdateTime = (cursor == null) ? START_OF_TIME : cursor.getCursorTime(); for (Registrar registrar : Registrar.loadAllCached()) { if (DateTimeUtils.isAtOrAfter(registrar.getLastUpdateTime(), lastUpdateTime)) { return true; } } return false; } /** Performs the synchronization operation. */ void run(String spreadsheetId) throws IOException { final DateTime executionTime = clock.nowUtc(); sheetSynchronizer.synchronize( spreadsheetId, new Ordering() { @Override public int compare(Registrar left, Registrar right) { return left.getClientId().compareTo(right.getClientId()); } }.immutableSortedCopy(Registrar.loadAllCached()) .stream() .filter( registrar -> registrar.getType() == Registrar.Type.REAL || registrar.getType() == Registrar.Type.OTE) .map( registrar -> { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); ImmutableSortedSet contacts = registrar.getContacts(); RegistrarAddress address = firstNonNull( registrar.getLocalizedAddress(), firstNonNull( registrar.getInternationalizedAddress(), new RegistrarAddress.Builder() .setStreet(ImmutableList.of("UNKNOWN")) .setCity("UNKNOWN") .setCountryCode("US") .build())); // // (╯°□°)╯ WARNING WARNING WARNING // // Do not change these mappings simply because the Registrar model changed. Only // change these mappings if the people who use the spreadsheet requested it be // changed. // // These values are hard-coded because they correspond to actual spreadsheet // columns. If you change this dictionary, then you'll need to manually add new // columns to the registrar spreadsheets for all environments before deployment, // and you'll need to remove deleted columns probably like a week after // deployment. // builder.put("clientIdentifier", convert(registrar.getClientId())); builder.put("registrarName", convert(registrar.getRegistrarName())); builder.put("state", convert(registrar.getState())); builder.put("ianaIdentifier", convert(registrar.getIanaIdentifier())); builder.put("billingIdentifier", convert(registrar.getBillingIdentifier())); builder.put("billingAccountMap", convert(registrar.getBillingAccountMap())); builder.put("primaryContacts", convertContacts(contacts, byType(ADMIN))); builder.put("techContacts", convertContacts(contacts, byType(TECH))); builder.put("marketingContacts", convertContacts(contacts, byType(MARKETING))); builder.put("abuseContacts", convertContacts(contacts, byType(ABUSE))); builder.put("whoisInquiryContacts", convertContacts(contacts, byType(WHOIS))); builder.put("legalContacts", convertContacts(contacts, byType(LEGAL))); builder.put("billingContacts", convertContacts(contacts, byType(BILLING))); builder.put( "contactsMarkedAsWhoisAdmin", convertContacts(contacts, RegistrarContact::getVisibleInWhoisAsAdmin)); builder.put( "contactsMarkedAsWhoisTech", convertContacts(contacts, RegistrarContact::getVisibleInWhoisAsTech)); builder.put("emailAddress", convert(registrar.getEmailAddress())); builder.put("address.street", convert(address.getStreet())); builder.put("address.city", convert(address.getCity())); builder.put("address.state", convert(address.getState())); builder.put("address.zip", convert(address.getZip())); builder.put("address.countryCode", convert(address.getCountryCode())); builder.put("phoneNumber", convert(registrar.getPhoneNumber())); builder.put("faxNumber", convert(registrar.getFaxNumber())); builder.put("creationTime", convert(registrar.getCreationTime())); builder.put("lastUpdateTime", convert(registrar.getLastUpdateTime())); builder.put("allowedTlds", convert(registrar.getAllowedTlds())); builder.put("whoisServer", convert(registrar.getWhoisServer())); builder.put("blockPremiumNames", convert(registrar.getBlockPremiumNames())); builder.put("ipAddressWhitelist", convert(registrar.getIpAddressWhitelist())); builder.put("url", convert(registrar.getUrl())); builder.put("referralUrl", convert(registrar.getReferralUrl())); builder.put("icannReferralEmail", convert(registrar.getIcannReferralEmail())); return builder.build(); }) .collect(toImmutableList())); ofy() .transact( () -> ofy().save().entity(Cursor.createGlobal(SYNC_REGISTRAR_SHEET, executionTime))); } private static String convertContacts( Iterable contacts, Predicate filter) { StringBuilder result = new StringBuilder(); boolean first = true; for (RegistrarContact contact : contacts) { if (!filter.test(contact)) { continue; } if (first) { first = false; } else { result.append("\n"); } result.append(contact.toStringMultilinePlainText()); } return result.toString(); } private static Predicate byType(final RegistrarContact.Type type) { return contact -> contact.getTypes().contains(type); } /** Converts a value to a string representation that can be stored in a spreadsheet cell. */ private static String convert(@Nullable Object value) { if (value == null) { return ""; } else if (value instanceof Iterable) { return Joiner.on('\n').join((Iterable) value); } else { return value.toString(); } } }