// Copyright 2016 The Domain Registry 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 com.google.domain.registry.flows.async; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.domain.registry.flows.ResourceFlowUtils.prepareDeletedResourceAsBuilder; import static com.google.domain.registry.flows.ResourceFlowUtils.updateForeignKeyIndexDeletionTime; import static com.google.domain.registry.model.EppResourceUtils.isActive; import static com.google.domain.registry.model.EppResourceUtils.isDeleted; import static com.google.domain.registry.model.ofy.ObjectifyService.ofy; import static com.google.domain.registry.util.PipelineUtils.createJobPath; import static com.google.domain.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.appengine.tools.mapreduce.Mapper; import com.google.appengine.tools.mapreduce.Reducer; import com.google.appengine.tools.mapreduce.ReducerInput; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.domain.registry.mapreduce.MapreduceAction; import com.google.domain.registry.mapreduce.MapreduceRunner; import com.google.domain.registry.mapreduce.inputs.EppResourceInputs; import com.google.domain.registry.mapreduce.inputs.NullInput; import com.google.domain.registry.model.EppResource; import com.google.domain.registry.model.annotations.ExternalMessagingName; import com.google.domain.registry.model.domain.DomainBase; import com.google.domain.registry.model.domain.ReferenceUnion; import com.google.domain.registry.model.eppcommon.StatusValue; import com.google.domain.registry.model.poll.PollMessage; import com.google.domain.registry.model.reporting.HistoryEntry; import com.google.domain.registry.request.HttpException.BadRequestException; import com.google.domain.registry.request.Parameter; import com.google.domain.registry.request.Response; import com.google.domain.registry.util.Clock; import com.google.domain.registry.util.FormattingLogger; import com.google.domain.registry.util.NonFinalForTesting; import com.google.domain.registry.util.SystemClock; import com.google.domain.registry.util.TypeUtils.TypeInstantiator; import com.googlecode.objectify.Key; import com.googlecode.objectify.Ref; import com.googlecode.objectify.Work; import org.joda.time.DateTime; import javax.inject.Inject; /** * A mapreduce to delete the specified EPP resource, but ONLY if it is not referred to by any * existing DomainBase entity. */ public abstract class DeleteEppResourceAction implements MapreduceAction { @NonFinalForTesting static Clock clock = new SystemClock(); /** The HTTP parameter name used to specify the websafe key of the resource to delete. */ public static final String PARAM_RESOURCE_KEY = "resourceKey"; public static final String PARAM_REQUESTING_CLIENT_ID = "requestingClientId"; public static final String PARAM_IS_SUPERUSER = "superuser"; private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); @Inject @Parameter(PARAM_RESOURCE_KEY) String resourceKeyString; @Inject @Parameter(PARAM_REQUESTING_CLIENT_ID) String requestingClientId; @Inject @Parameter(PARAM_IS_SUPERUSER) boolean isSuperuser; @Inject MapreduceRunner mrRunner; @Inject Response response; DeleteEppResourceMapper mapper; DeleteEppResourceReducer reducer; protected DeleteEppResourceAction( DeleteEppResourceMapper mapper, DeleteEppResourceReducer reducer) { this.mapper = mapper; this.reducer = reducer; } @Override public void run() { Key resourceKey = null; T resource; try { resourceKey = Key.create(resourceKeyString); resource = checkArgumentNotNull(ofy().load().key(resourceKey).now()); } catch (IllegalArgumentException e) { throw new BadRequestException(resourceKey == null ? "Could not parse key string: " + resourceKeyString : "Could not load resource for key: " + resourceKey); } checkArgument( resource.getClass().equals(new TypeInstantiator(getClass()){}.getExactType()), String.format("Cannot delete a %s via this action.", resource.getClass().getSimpleName())); checkState( !isDeleted(resource, clock.nowUtc()), "Resource %s is already deleted.", resource.getForeignKey()); checkState( resource.getStatusValues().contains(StatusValue.PENDING_DELETE), "Resource %s is not set as PENDING_DELETE", resource.getForeignKey()); mapper.setTargetResource(resourceKey); reducer.setClient(requestingClientId, isSuperuser); logger.infofmt("Executing Delete EPP resource mapreduce for %s", resourceKey); response.sendJavaScriptRedirect(createJobPath(mrRunner .setJobName("Check for EPP resource references and then delete") .setModuleName("backend") .runMapreduce( mapper, reducer, ImmutableList.of( // Add an extra shard that maps over a null domain. See the mapper code for why. new NullInput(), EppResourceInputs.createEntityInput(DomainBase.class))))); } /** * A mapper that iterates over all {@link DomainBase} entities. * *

It emits the target key and {@code true} for domains referencing the target resource. For * the special input of {@code null} it emits the target key and {@code false}. */ public abstract static class DeleteEppResourceMapper extends Mapper, Boolean> { private static final long serialVersionUID = -7355145176854995813L; private DateTime targetResourceUpdateTimestamp; private Key targetEppResourceKey; private void setTargetResource(Key targetEppResourceKey) { this.targetEppResourceKey = targetEppResourceKey; this.targetResourceUpdateTimestamp = ofy().load().key(targetEppResourceKey).now().getUpdateAutoTimestamp().getTimestamp(); } /** Determine whether the target resource is a linked resource on the domain. */ protected abstract boolean isLinked(DomainBase domain, ReferenceUnion targetResourceRef); @Override public void map(DomainBase domain) { // The reducer only runs if at least one value is emitted. We add a null input to the // mapreduce and always emit 'false' for it to force the reducer to run. We can then emit // 'true' for linked domains and not emit anything for unlinked domains, which speeds up the // reducer since it will only receive true keys, of which there will be few (usually none). if (domain == null) { emit(targetEppResourceKey, false); return; } // The ReferenceUnion can't be a field on the Mapper, because when a Ref is serialized // (required for each MapShardTask), it uses the DeadRef version, which contains the Ref's // value, which isn't serializable. Thankfully, this isn't expensive. // See: https://github.com/objectify/objectify/blob/master/src/main/java/com/googlecode/objectify/impl/ref/DeadRef.java if (isActive(domain, targetResourceUpdateTimestamp) && isLinked(domain, ReferenceUnion.create(Ref.create(targetEppResourceKey)))) { emit(targetEppResourceKey, true); } } } /** * A reducer that checks if the EPP resource to be deleted is referenced anywhere, and then * deletes it if not and unmarks it for deletion if so. */ public abstract static class DeleteEppResourceReducer extends Reducer, Boolean, Void> { private static final long serialVersionUID = 875017002097945151L; private String requestingClientId; private boolean isSuperuser; private void setClient(String requestingClientId, boolean isSuperuser) { this.requestingClientId = requestingClientId; this.isSuperuser = isSuperuser; } /** * Determine the proper history entry type for the delete operation, as a function of * whether or not the delete was successful. */ protected abstract HistoryEntry.Type getHistoryType(boolean successfulDelete); /** Perform any type-specific tasks on the resource to be deleted (and/or its dependencies). */ protected abstract void performDeleteTasks( T targetResource, T deletedResource, DateTime deletionTime, HistoryEntry historyEntryForDelete); @Override public void reduce(final Key key, final ReducerInput values) { final boolean hasNoActiveReferences = !Iterators.contains(values, true); logger.infofmt("Processing delete request for %s", key.toString()); String pollMessageText = ofy().transactNew(new Work() { @Override @SuppressWarnings("unchecked") public String run() { DateTime now = ofy().getTransactionTime(); T targetResource = (T) ofy().load().key(key).now().cloneProjectedAtTime(now); String resourceName = targetResource.getForeignKey(); // Double-check that the resource is still active and in PENDING_DELETE within the // transaction. checkState( !isDeleted(targetResource, now), "Resource %s is already deleted.", resourceName); checkState( targetResource.getStatusValues().contains(StatusValue.PENDING_DELETE), "Resource %s is not in PENDING_DELETE.", resourceName); targetResource = (T) targetResource.asBuilder() .removeStatusValue(StatusValue.PENDING_DELETE) .build(); boolean requestedByCurrentOwner = targetResource.getCurrentSponsorClientId().equals(requestingClientId); boolean deleteAllowed = hasNoActiveReferences && (requestedByCurrentOwner || isSuperuser); String resourceTypeName = targetResource.getClass().getAnnotation(ExternalMessagingName.class).value(); HistoryEntry.Type historyType = getHistoryType(deleteAllowed); String pollMessageText = deleteAllowed ? String.format("Deleted %s %s.", resourceTypeName, resourceName) : String.format( "Can't delete %s %s because %s.", resourceTypeName, resourceName, requestedByCurrentOwner ? "it is referenced by a domain" : "it was transferred prior to deletion"); HistoryEntry historyEntry = new HistoryEntry.Builder() .setClientId(requestingClientId) .setModificationTime(now) .setType(historyType) .setParent(key) .build(); PollMessage.OneTime deleteResultMessage = new PollMessage.OneTime.Builder() .setClientId(requestingClientId) .setMsg(pollMessageText) .setParent(historyEntry) .setEventTime(now) .build(); if (deleteAllowed) { T deletedResource = prepareDeletedResourceAsBuilder(targetResource, now).build(); performDeleteTasks(targetResource, deletedResource, now, historyEntry); updateForeignKeyIndexDeletionTime(deletedResource); ofy().save().entities(deletedResource, historyEntry, deleteResultMessage); } else { ofy().save().entities(targetResource, historyEntry, deleteResultMessage); } return pollMessageText; } }); logger.infofmt(pollMessageText); } } }