mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 12:07:51 +02:00
This change renames directories in preparation for the great package rename. The repository is now in a broken state because the code itself hasn't been updated. However this should ensure that git correctly preserves history for each file.
272 lines
12 KiB
Java
272 lines
12 KiB
Java
// 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<T extends EppResource> 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<T> mapper;
|
|
DeleteEppResourceReducer<T> reducer;
|
|
|
|
protected DeleteEppResourceAction(
|
|
DeleteEppResourceMapper<T> mapper,
|
|
DeleteEppResourceReducer<T> reducer) {
|
|
this.mapper = mapper;
|
|
this.reducer = reducer;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
Key<T> 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<T>(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<DomainBase>(),
|
|
EppResourceInputs.createEntityInput(DomainBase.class)))));
|
|
}
|
|
|
|
/**
|
|
* A mapper that iterates over all {@link DomainBase} entities.
|
|
*
|
|
* <p>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<T extends EppResource>
|
|
extends Mapper<DomainBase, Key<T>, Boolean> {
|
|
|
|
private static final long serialVersionUID = -7355145176854995813L;
|
|
|
|
private DateTime targetResourceUpdateTimestamp;
|
|
private Key<T> targetEppResourceKey;
|
|
|
|
private void setTargetResource(Key<T> 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<T> 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<T extends EppResource>
|
|
extends Reducer<Key<T>, 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<T> key, final ReducerInput<Boolean> values) {
|
|
final boolean hasNoActiveReferences = !Iterators.contains(values, true);
|
|
logger.infofmt("Processing delete request for %s", key.toString());
|
|
String pollMessageText = ofy().transactNew(new Work<String>() {
|
|
@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().<Object>entities(deletedResource, historyEntry, deleteResultMessage);
|
|
} else {
|
|
ofy().save().<Object>entities(targetResource, historyEntry, deleteResultMessage);
|
|
}
|
|
return pollMessageText;
|
|
}
|
|
});
|
|
logger.infofmt(pollMessageText);
|
|
}
|
|
}
|
|
}
|