diff --git a/java/google/registry/env/common/tools/WEB-INF/web.xml b/java/google/registry/env/common/tools/WEB-INF/web.xml index 1bc691d46..38614a9ec 100644 --- a/java/google/registry/env/common/tools/WEB-INF/web.xml +++ b/java/google/registry/env/common/tools/WEB-INF/web.xml @@ -72,6 +72,12 @@ /_dr/task/resaveAllEppResources + + + tools-servlet + /_dr/task/resaveAllHistoryEntries + + tools-servlet diff --git a/java/google/registry/module/tools/ToolsRequestComponent.java b/java/google/registry/module/tools/ToolsRequestComponent.java index d345e9d67..bd909f7cd 100644 --- a/java/google/registry/module/tools/ToolsRequestComponent.java +++ b/java/google/registry/module/tools/ToolsRequestComponent.java @@ -45,6 +45,7 @@ import google.registry.tools.server.ListTldsAction; import google.registry.tools.server.PollMapreduceAction; import google.registry.tools.server.RefreshDnsForAllDomainsAction; import google.registry.tools.server.ResaveAllEppResourcesAction; +import google.registry.tools.server.ResaveAllHistoryEntriesAction; import google.registry.tools.server.ToolsServerModule; import google.registry.tools.server.UpdatePremiumListAction; import google.registry.tools.server.VerifyOteAction; @@ -82,6 +83,7 @@ interface ToolsRequestComponent { PublishDetailReportAction publishDetailReportAction(); RefreshDnsForAllDomainsAction refreshDnsForAllDomainsAction(); ResaveAllEppResourcesAction resaveAllEppResourcesAction(); + ResaveAllHistoryEntriesAction resaveAllHistoryEntriesAction(); RestoreCommitLogsAction restoreCommitLogsAction(); UpdatePremiumListAction updatePremiumListAction(); VerifyOteAction verifyOteAction(); diff --git a/java/google/registry/tools/server/ResaveAllHistoryEntriesAction.java b/java/google/registry/tools/server/ResaveAllHistoryEntriesAction.java new file mode 100644 index 000000000..42f65d9f4 --- /dev/null +++ b/java/google/registry/tools/server/ResaveAllHistoryEntriesAction.java @@ -0,0 +1,84 @@ +// 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.server; + +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.util.PipelineUtils.createJobPath; + +import com.google.appengine.tools.mapreduce.Mapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.VoidWork; +import google.registry.mapreduce.MapreduceRunner; +import google.registry.mapreduce.inputs.EppResourceInputs; +import google.registry.model.EppResource; +import google.registry.model.reporting.HistoryEntry; +import google.registry.request.Action; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import javax.inject.Inject; + +/** + * A mapreduce that re-saves all {@link HistoryEntry} entities. + * + *

This is useful for completing data migrations on HistoryEntry fields. + * + *

Because there are no auth settings in the {@link Action} annotation, this command can only be + * run internally, or by pretending to be internal by setting the X-AppEngine-QueueName header, + * which only admin users can do. + */ +@Action( + path = "/_dr/task/resaveAllHistoryEntries", + auth = Auth.AUTH_INTERNAL_OR_ADMIN +) +public class ResaveAllHistoryEntriesAction implements Runnable { + + @Inject MapreduceRunner mrRunner; + @Inject Response response; + @Inject ResaveAllHistoryEntriesAction() {} + + @SuppressWarnings("unchecked") + @Override + public void run() { + response.sendJavaScriptRedirect(createJobPath(mrRunner + .setJobName("Re-save all HistoryEntry entities") + .setModuleName("tools") + .runMapOnly( + new ResaveAllHistoryEntriesActionMapper(), + ImmutableList.of(EppResourceInputs.createChildEntityInput( + ImmutableSet.>of(EppResource.class), + ImmutableSet.>of(HistoryEntry.class)))))); + } + + /** Mapper to re-save all HistoryEntry entities. */ + public static class ResaveAllHistoryEntriesActionMapper + extends Mapper { + + private static final long serialVersionUID = 123064872315192L; + + @Override + public final void map(final HistoryEntry historyEntry) { + ofy().transact(new VoidWork() { + @Override + public void vrun() { + ofy().save().entity(ofy().load().entity(historyEntry).now()).now(); + }}); + getContext().incrementCounter( + String.format( + "HistoryEntries parented under %s re-saved", historyEntry.getParent().getKind())); + } + } +} + diff --git a/javatests/google/registry/module/tools/testdata/tools_routing.txt b/javatests/google/registry/module/tools/testdata/tools_routing.txt index f19dd9b94..7b1b7fc11 100644 --- a/javatests/google/registry/module/tools/testdata/tools_routing.txt +++ b/javatests/google/registry/module/tools/testdata/tools_routing.txt @@ -19,4 +19,5 @@ PATH CLASS METHODS OK AUTH /_dr/task/pollMapreduce PollMapreduceAction POST n INTERNAL APP IGNORED /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n INTERNAL,API APP ADMIN /_dr/task/resaveAllEppResources ResaveAllEppResourcesAction GET n INTERNAL,API APP ADMIN +/_dr/task/resaveAllHistoryEntries ResaveAllHistoryEntriesAction GET n INTERNAL,API APP ADMIN /_dr/task/restoreCommitLogs RestoreCommitLogsAction POST y INTERNAL,API APP ADMIN diff --git a/javatests/google/registry/tools/server/ResaveAllHistoryEntriesActionTest.java b/javatests/google/registry/tools/server/ResaveAllHistoryEntriesActionTest.java new file mode 100644 index 000000000..8130612b1 --- /dev/null +++ b/javatests/google/registry/tools/server/ResaveAllHistoryEntriesActionTest.java @@ -0,0 +1,83 @@ +// 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.server; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistActiveContact; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import google.registry.model.contact.ContactResource; +import google.registry.model.domain.DomainResource; +import google.registry.model.reporting.HistoryEntry; +import google.registry.testing.FakeResponse; +import google.registry.testing.mapreduce.MapreduceTestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ResaveAllEppResourcesAction}. */ +@RunWith(JUnit4.class) +public class ResaveAllHistoryEntriesActionTest + extends MapreduceTestCase { + + private static final DatastoreService datastoreService = + DatastoreServiceFactory.getDatastoreService(); + + @Before + public void init() { + action = new ResaveAllHistoryEntriesAction(); + action.mrRunner = makeDefaultRunner(); + action.response = new FakeResponse(); + } + + private void runMapreduce() throws Exception { + action.run(); + executeTasksUntilEmpty("mapreduce"); + } + + @Test + public void test_mapreduceSuccessfullyResavesEntity() throws Exception { + createTld("tld"); + DomainResource domain = persistActiveDomain("test.tld"); + ContactResource contact = persistActiveContact("humanBeing"); + Entity domainEntry = + ofy().save().toEntity(new HistoryEntry.Builder().setParent(domain).build()); + Entity contactEntry = + ofy().save().toEntity(new HistoryEntry.Builder().setParent(contact).build()); + + // Set raw properties outside the Objectify schema, which will be deleted upon re-save. + domainEntry.setProperty("clientId", "validId"); + contactEntry.setProperty("otherClientId", "anotherId"); + domainEntry.setProperty("propertyToBeDeleted", "123blah"); + contactEntry.setProperty("alsoShouldBeDeleted", "456nah"); + datastoreService.put(domainEntry); + datastoreService.put(contactEntry); + ofy().clearSessionCache(); + runMapreduce(); + + Entity updatedDomainEntry = datastoreService.get(domainEntry.getKey()); + Entity updatedContactEntry = datastoreService.get(contactEntry.getKey()); + assertThat(updatedDomainEntry.getProperty("clientId")).isEqualTo("validId"); + assertThat(updatedDomainEntry.getProperty("propertyToBeDeleted")).isNull(); + assertThat(updatedContactEntry.getProperty("otherClientId")).isEqualTo("anotherId"); + assertThat(updatedContactEntry.getProperty("alsoShouldBeDeleted")).isNull(); + } +}