diff --git a/java/google/registry/tools/CreateLrpTokensCommand.java b/java/google/registry/tools/CreateLrpTokensCommand.java index 3bd4a6faa..7a530e2a6 100644 --- a/java/google/registry/tools/CreateLrpTokensCommand.java +++ b/java/google/registry/tools/CreateLrpTokensCommand.java @@ -27,6 +27,8 @@ import com.beust.jcommander.Parameters; import com.google.common.base.Function; import com.google.common.base.Splitter; 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.google.common.io.Files; import com.google.common.io.LineReader; @@ -34,6 +36,8 @@ import com.googlecode.objectify.Key; import com.googlecode.objectify.Work; import google.registry.model.domain.LrpTokenEntity; import google.registry.tools.Command.RemoteApiCommand; +import google.registry.tools.params.KeyValueMapParameter.StringToIntegerMap; +import google.registry.tools.params.KeyValueMapParameter.StringToStringMap; import google.registry.tools.params.PathParameter; import google.registry.util.StringGenerator; import google.registry.util.TokenUtils; @@ -71,6 +75,24 @@ public final class CreateLrpTokensCommand implements RemoteApiCommand { validateWith = PathParameter.InputFile.class) private Path assigneesFile; + @Parameter( + names = {"-m", "--metadata"}, + description = "Token metadata key-value pairs (formatted as key=value[,key=value...]). Used" + + " only in conjunction with -a/--assignee when creating a single token.", + converter = StringToStringMap.class, + validateWith = StringToStringMap.class) + private ImmutableMap metadata; + + @Parameter( + names = {"-c", "--metadata_columns"}, + description = "Token metadata columns (formatted as key=index[,key=index...], columns are" + + " zero-indexed). Used only in conjunction with -i/--input to map additional fields in" + + " the CSV file to metadata stored on the LRP token. The index corresponds to the column" + + " number in the CSV file (where the assignee is assigned column 0).", + converter = StringToIntegerMap.class, + validateWith = StringToIntegerMap.class) + private ImmutableMap metadataColumns; + @Inject StringGenerator stringGenerator; private static final int BATCH_SIZE = 20; @@ -80,13 +102,19 @@ public final class CreateLrpTokensCommand implements RemoteApiCommand { checkArgument( (assignee == null) == (assigneesFile != null), "Exactly one of either assignee or filename must be specified."); + checkArgument( + (assigneesFile == null) || (metadata == null), + "Metadata cannot be specified along with a filename."); + checkArgument( + (assignee == null) || (metadataColumns == null), + "Metadata columns cannot be specified along with an assignee."); final Set validTlds = ImmutableSet.copyOf(Splitter.on(',').split(tlds)); for (String tld : validTlds) { assertTldExists(tld); } LineReader reader = new LineReader( - (assignee == null) + (assigneesFile != null) ? Files.newReader(assigneesFile.toFile(), UTF_8) : new StringReader(assignee)); @@ -96,11 +124,27 @@ public final class CreateLrpTokensCommand implements RemoteApiCommand { for (String token : generateTokens(BATCH_SIZE)) { line = reader.readLine(); if (!isNullOrEmpty(line)) { - tokensToSave.add(new LrpTokenEntity.Builder() - .setAssignee(line) + ImmutableList values = ImmutableList.copyOf(Splitter.on(',').split(line)); + LrpTokenEntity.Builder tokenBuilder = new LrpTokenEntity.Builder() + .setAssignee(values.get(0)) .setToken(token) - .setValidTlds(validTlds) - .build()); + .setValidTlds(validTlds); + if (metadata != null) { + tokenBuilder.setMetadata(metadata); + } else if (metadataColumns != null) { + ImmutableMap.Builder metadataBuilder = ImmutableMap.builder(); + for (ImmutableMap.Entry entry : metadataColumns.entrySet()) { + checkArgument( + values.size() > entry.getValue(), + "Entry for %s does not have a value for %s (index %s)", + values.get(0), + entry.getKey(), + entry.getValue()); + metadataBuilder.put(entry.getKey(), values.get(entry.getValue())); + } + tokenBuilder.setMetadata(metadataBuilder.build()); + } + tokensToSave.add(tokenBuilder.build()); } } saveTokens(tokensToSave.build()); diff --git a/java/google/registry/tools/params/KeyValueMapParameter.java b/java/google/registry/tools/params/KeyValueMapParameter.java new file mode 100644 index 000000000..1389c0228 --- /dev/null +++ b/java/google/registry/tools/params/KeyValueMapParameter.java @@ -0,0 +1,96 @@ +// Copyright 2016 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.params; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * Combined converter and validator class for key-value map JCommander argument strings. + * + *

These strings have the form {@code =,[=]*} where + * {@code } and {@code } are strings that can be parsed into instances of some key + * type {@code K} and value type {@code V}, respectively. This class converts a string into an + * ImmutableMap mapping {@code K} to {@code V}. Validation and conversion share the same logic; + * validation is just done by attempting conversion and throwing exceptions if need be. + * + *

Subclasses must implement parseKey() and parseValue() to define how to parse {@code } + * and {@code } into {@code K} and {@code V}, respectively. + * + * @param instance key type + * @param instance value type + */ +public abstract class KeyValueMapParameter + extends ParameterConverterValidator> { + + public KeyValueMapParameter(String messageForInvalid) { + super(messageForInvalid); + } + + public KeyValueMapParameter() { + super("Not formatted correctly."); + } + + /** Override to define how to parse rawKey into an object of type K. */ + protected abstract K parseKey(String rawKey); + + /** Override to define how to parse rawValue into an object of type V. */ + protected abstract V parseValue(String rawValue); + + /** Override to perform any post-processing on the map. */ + protected ImmutableMap processMap(ImmutableMap map) { + return map; + } + + @Override + public final ImmutableMap convert(String keyValueMapString) { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + if (!Strings.isNullOrEmpty(keyValueMapString)) { + for (Map.Entry entry : + Splitter.on(',').withKeyValueSeparator('=').split(keyValueMapString).entrySet()) { + builder.put(parseKey(entry.getKey()), parseValue(entry.getValue())); + } + } + return processMap(builder.build()); + } + + /** Combined converter and validator class for string-to-string Map argument strings. */ + public static class StringToStringMap extends KeyValueMapParameter { + @Override + protected String parseKey(String rawKey) { + return rawKey; + } + + @Override + protected String parseValue(String value) { + return value; + } + } + + /** Combined converter and validator class for string-to-integer Map argument strings. */ + public static class StringToIntegerMap extends KeyValueMapParameter { + @Override + protected String parseKey(String rawKey) { + return rawKey; + } + + @Override + protected Integer parseValue(String value) { + return Integer.parseInt(value); + } + } +} diff --git a/java/google/registry/tools/params/TransitionListParameter.java b/java/google/registry/tools/params/TransitionListParameter.java index 7148dedb0..08f3b7b39 100644 --- a/java/google/registry/tools/params/TransitionListParameter.java +++ b/java/google/registry/tools/params/TransitionListParameter.java @@ -16,33 +16,16 @@ package google.registry.tools.params; import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Ordering; import google.registry.model.registry.Registry.TldState; -import java.util.Map; import org.joda.money.Money; import org.joda.time.DateTime; -/** - * Combined converter and validator class for transition list JCommander argument strings. - * - *

These strings have the form {@code =,[=]*} where - * {@code } is a string that can be parsed into an instance of some value type {@code T}, and - * the entire argument represents a series of timed transitions of some property taking on those - * values. This class converts such a string into an ImmutableSortedMap mapping DateTime to - * {@code T}. Validation and conversion share the same logic; validation is just done by attempting - * conversion and throwing exceptions if need be. - * - *

Subclasses must implement parseValue() to define how to parse {@code } into a - * {@code T}. - * - * @param instance value type - */ +/** Combined converter and validator class for transition list JCommander argument strings. */ // TODO(b/19031334): Investigate making this complex generic type work with the factory. -public abstract class TransitionListParameter - extends ParameterConverterValidator> { +public abstract class TransitionListParameter extends KeyValueMapParameter { private static final DateTimeParameter DATE_TIME_CONVERTER = new DateTimeParameter(); @@ -50,24 +33,17 @@ public abstract class TransitionListParameter super("Not formatted correctly or has transition times out of order."); } - /** Override to define how to parse rawValue into an object of type T. */ - protected abstract T parseValue(String rawValue); - @Override - public ImmutableSortedMap convert(String transitionListString) { - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - for (Map.Entry entry : - Splitter.on(',').withKeyValueSeparator('=').split(transitionListString).entrySet()) { - builder.put( - DATE_TIME_CONVERTER.convert(entry.getKey()), - parseValue(entry.getValue())); - } - ImmutableMap transitionMap = builder.build(); - checkArgument(Ordering.natural().isOrdered(transitionMap.keySet()), - "Transition times out of order."); - return ImmutableSortedMap.copyOf(transitionMap); + protected final DateTime parseKey(String rawKey) { + return DATE_TIME_CONVERTER.convert(rawKey); } + @Override + protected final ImmutableSortedMap processMap(ImmutableMap map) { + checkArgument(Ordering.natural().isOrdered(map.keySet()), "Transition times out of order."); + return ImmutableSortedMap.copyOf(map); + } + /** Converter-validator for TLD state transitions. */ public static class TldStateTransitions extends TransitionListParameter { @Override diff --git a/javatests/google/registry/tools/CreateLrpTokensCommandTest.java b/javatests/google/registry/tools/CreateLrpTokensCommandTest.java index 7d2658148..64d16a066 100644 --- a/javatests/google/registry/tools/CreateLrpTokensCommandTest.java +++ b/javatests/google/registry/tools/CreateLrpTokensCommandTest.java @@ -55,7 +55,20 @@ public class CreateLrpTokensCommandTest extends CommandTestCase validTlds, - @Nullable Key redemptionHistoryEntry) { + @Nullable Key redemptionHistoryEntry, + @Nullable ImmutableMap metadata) { LrpTokenEntity.Builder tokenBuilder = new LrpTokenEntity.Builder() .setAssignee(assignee) .setValidTlds(validTlds) @@ -198,6 +251,9 @@ public class CreateLrpTokensCommandTest extends CommandTestCase