mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 03:57:51 +02:00
235 lines
9.4 KiB
Java
235 lines
9.4 KiB
Java
// 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.tools;
|
|
|
|
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
|
|
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
|
|
import static com.google.common.base.Functions.toStringFunction;
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
import static com.google.common.base.Preconditions.checkState;
|
|
import static com.google.common.base.Strings.emptyToNull;
|
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
import static google.registry.util.DatastoreServiceUtils.getNameOrId;
|
|
import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
|
|
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.MoreObjects;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.collect.FluentIterable;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.googlecode.objectify.Key;
|
|
import com.googlecode.objectify.VoidWork;
|
|
import google.registry.model.ImmutableObject;
|
|
import google.registry.tools.Command.RemoteApiCommand;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import javax.annotation.Nullable;
|
|
|
|
/** A {@link ConfirmingCommand} that changes objects in Datastore. */
|
|
public abstract class MutatingCommand extends ConfirmingCommand implements RemoteApiCommand {
|
|
|
|
/**
|
|
* A mutation of a specific entity, represented by an old and a new version of the entity.
|
|
* Storing the old version is necessary to enable checking that the existing entity has not been
|
|
* modified when applying a mutation that was created outside the same transaction.
|
|
*/
|
|
private static class EntityChange {
|
|
|
|
/** The possible types of mutation that can be performed on an entity. */
|
|
public static enum ChangeType {
|
|
CREATE, DELETE, UPDATE;
|
|
|
|
/** Return the ChangeType corresponding to the given combination of version existences. */
|
|
public static ChangeType get(boolean hasOldVersion, boolean hasNewVersion) {
|
|
checkArgument(
|
|
hasOldVersion || hasNewVersion,
|
|
"An entity change must have an old version or a new version (or both)");
|
|
return !hasOldVersion ? CREATE : (!hasNewVersion ? DELETE : UPDATE);
|
|
}
|
|
}
|
|
|
|
/** The type of mutation being performed on the entity. */
|
|
final ChangeType type;
|
|
|
|
/** The old version of the entity, or null if this is a create. */
|
|
final ImmutableObject oldEntity;
|
|
|
|
/** The new version of the entity, or null if this is a delete. */
|
|
final ImmutableObject newEntity;
|
|
|
|
/** The key that points to the entity being changed. */
|
|
final Key<ImmutableObject> key;
|
|
|
|
public EntityChange(ImmutableObject oldEntity, ImmutableObject newEntity) {
|
|
type = ChangeType.get(oldEntity != null, newEntity != null);
|
|
checkArgument(
|
|
type != ChangeType.UPDATE || Key.create(oldEntity).equals(Key.create(newEntity)),
|
|
"Both entity versions in an update must have the same Key.");
|
|
this.oldEntity = oldEntity;
|
|
this.newEntity = newEntity;
|
|
key = Key.create(MoreObjects.firstNonNull(oldEntity, newEntity));
|
|
}
|
|
|
|
/** Returns a human-readable ID string for the entity being changed. */
|
|
public String getEntityId() {
|
|
return String.format(
|
|
"%s@%s",
|
|
key.getKind(),
|
|
// NB: try name before id, since name defaults to null, whereas id defaults to 0.
|
|
getNameOrId(key.getRaw()));
|
|
}
|
|
|
|
/** Returns a string representation of this entity change. */
|
|
@Override
|
|
public String toString() {
|
|
String changeText;
|
|
if (type == ChangeType.UPDATE) {
|
|
String diffText = prettyPrintEntityDeepDiff(
|
|
oldEntity.toDiffableFieldMap(), newEntity.toDiffableFieldMap());
|
|
changeText = Optional.fromNullable(emptyToNull(diffText)).or("[no changes]\n");
|
|
} else {
|
|
changeText = MoreObjects.firstNonNull(oldEntity, newEntity) + "\n";
|
|
}
|
|
return String.format(
|
|
"%s %s\n%s",
|
|
UPPER_UNDERSCORE.to(UPPER_CAMEL, type.toString()), getEntityId(), changeText);
|
|
}
|
|
}
|
|
|
|
/** Map from entity keys to EntityChange objects representing changes to those entities. */
|
|
private final LinkedHashMap<Key<ImmutableObject>, EntityChange> changedEntitiesMap =
|
|
new LinkedHashMap<>();
|
|
|
|
/** A set of resource keys for which new transactions should be created after. */
|
|
private final Set<Key<ImmutableObject>> transactionBoundaries = new HashSet<>();
|
|
|
|
@Nullable
|
|
private Key<ImmutableObject> lastAddedKey;
|
|
|
|
/**
|
|
* Initializes the command.
|
|
*
|
|
* <p>Subclasses override this method to populate {@link #changedEntitiesMap} with updated
|
|
* entities. The old entity is the key and the new entity is the value; the key is null for newly
|
|
* created entities and the value is null for deleted entities.
|
|
*/
|
|
@Override
|
|
protected abstract void init() throws Exception;
|
|
|
|
/**
|
|
* Performs the command and returns a result description.
|
|
*
|
|
* <p>Subclasses can override this method if the command does something besides update entities,
|
|
* such as running a full flow.
|
|
*/
|
|
@Override
|
|
protected String execute() throws Exception {
|
|
for (final List<EntityChange> batch : getCollatedEntityChangeBatches()) {
|
|
ofy().transact(new VoidWork() {
|
|
@Override
|
|
public void vrun() {
|
|
for (EntityChange change : batch) {
|
|
// Load the key of the entity to mutate and double-check that it hasn't been
|
|
// modified from the version that existed when the change was prepared.
|
|
ImmutableObject existingEntity = ofy().load().key(change.key).now();
|
|
checkState(
|
|
Objects.equals(change.oldEntity, existingEntity),
|
|
"Entity changed since init() was called.\n%s",
|
|
prettyPrintEntityDeepDiff(
|
|
change.oldEntity == null ? ImmutableMap.of()
|
|
: change.oldEntity.toDiffableFieldMap(),
|
|
existingEntity == null ? ImmutableMap.of()
|
|
: existingEntity.toDiffableFieldMap()));
|
|
switch (change.type) {
|
|
case CREATE: // Fall through.
|
|
case UPDATE:
|
|
ofy().save().entity(change.newEntity).now();
|
|
break;
|
|
case DELETE:
|
|
ofy().delete().key(change.key).now();
|
|
break;
|
|
default:
|
|
throw new UnsupportedOperationException(
|
|
"Unknown entity change type: " + change.type);
|
|
}
|
|
}
|
|
}});
|
|
}
|
|
return String.format("Updated %d entities.\n", changedEntitiesMap.size());
|
|
}
|
|
|
|
/**
|
|
* Returns a set of lists of EntityChange actions to commit. Each list should be executed in
|
|
* order inside a single transaction.
|
|
*/
|
|
private ImmutableSet<ImmutableList<EntityChange>> getCollatedEntityChangeBatches() {
|
|
ImmutableSet.Builder<ImmutableList<EntityChange>> batches = new ImmutableSet.Builder<>();
|
|
ArrayList<EntityChange> nextBatch = new ArrayList<>();
|
|
for (EntityChange change : changedEntitiesMap.values()) {
|
|
nextBatch.add(change);
|
|
if (transactionBoundaries.contains(change.key)) {
|
|
batches.add(ImmutableList.copyOf(nextBatch));
|
|
nextBatch.clear();
|
|
}
|
|
}
|
|
if (!nextBatch.isEmpty()) {
|
|
batches.add(ImmutableList.copyOf(nextBatch));
|
|
}
|
|
return batches.build();
|
|
}
|
|
|
|
/**
|
|
* Subclasses can call this to stage a mutation to an entity that will be applied by execute().
|
|
* Note that both objects passed must correspond to versions of the same entity with the same key.
|
|
*
|
|
* @param oldEntity the existing version of the entity, or null to create a new entity
|
|
* @param newEntity the new version of the entity to save, or null to delete the entity
|
|
*/
|
|
protected void stageEntityChange(
|
|
@Nullable ImmutableObject oldEntity, @Nullable ImmutableObject newEntity) {
|
|
EntityChange change = new EntityChange(oldEntity, newEntity);
|
|
checkArgument(
|
|
!changedEntitiesMap.containsKey(change.key),
|
|
"Cannot apply multiple changes for the same entity: %s",
|
|
change.getEntityId());
|
|
changedEntitiesMap.put(change.key, change);
|
|
lastAddedKey = change.key;
|
|
}
|
|
|
|
/**
|
|
* Subclasses can call this to write out all previously requested entity changes since the last
|
|
* transaction flush in a transaction.
|
|
*/
|
|
protected void flushTransaction() {
|
|
transactionBoundaries.add(checkNotNull(lastAddedKey));
|
|
}
|
|
|
|
/** Returns the changes that have been staged thus far. */
|
|
@Override
|
|
protected String prompt() {
|
|
return changedEntitiesMap.isEmpty()
|
|
? "No entity changes to apply."
|
|
: Joiner.on("\n").join(FluentIterable
|
|
.from(changedEntitiesMap.values())
|
|
.transform(toStringFunction()));
|
|
}
|
|
}
|