mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 20:17:51 +02:00
284 lines
12 KiB
Java
284 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 google.registry.tools.server;
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
|
|
import com.google.common.base.Function;
|
|
import com.google.common.base.Functions;
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.base.Splitter;
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.ImmutableBiMap;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.ImmutableSortedSet;
|
|
import com.google.common.collect.ImmutableTable;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Ordering;
|
|
import google.registry.model.ImmutableObject;
|
|
import google.registry.request.JsonResponse;
|
|
import google.registry.request.Parameter;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import javax.inject.Inject;
|
|
|
|
/**
|
|
* Abstract base class for actions that list ImmutableObjects.
|
|
*
|
|
* <p>Returns formatted text to be displayed on the screen.
|
|
*
|
|
* @param <T> type of object
|
|
*/
|
|
public abstract class ListObjectsAction<T extends ImmutableObject> implements Runnable {
|
|
|
|
public static final String FIELDS_PARAM = "fields";
|
|
public static final String PRINT_HEADER_ROW_PARAM = "printHeaderRow";
|
|
public static final String FULL_FIELD_NAMES_PARAM = "fullFieldNames";
|
|
|
|
@Inject JsonResponse response;
|
|
@Inject @Parameter("fields") Optional<String> fields;
|
|
@Inject @Parameter("printHeaderRow") Optional<Boolean> printHeaderRow;
|
|
@Inject @Parameter("fullFieldNames") Optional<Boolean> fullFieldNames;
|
|
|
|
/** Returns the set of objects to list, in the desired listing order. */
|
|
abstract ImmutableSet<T> loadObjects();
|
|
|
|
/**
|
|
* Returns a set of fields to always include in the output as the leftmost columns. Subclasses
|
|
* can use this to specify the equivalent of a "primary key" for each object listed.
|
|
*/
|
|
ImmutableSet<String> getPrimaryKeyFields() {
|
|
return ImmutableSet.of();
|
|
}
|
|
|
|
/**
|
|
* Returns an {@link ImmutableBiMap} that maps any field name aliases to the actual field names.
|
|
*
|
|
* <p>Users can select aliased fields for display using either the original name or the alias. By
|
|
* default, aliased fields will use the alias name as the header instead of the original name.
|
|
*/
|
|
ImmutableBiMap<String, String> getFieldAliases() {
|
|
return ImmutableBiMap.of();
|
|
}
|
|
|
|
/**
|
|
* Returns for a given {@link ImmutableObject} a mapping from field names to field values that
|
|
* will override, for any overlapping field names, the default behavior of getting the field
|
|
* value by looking up that field name in the map returned by
|
|
* {@link ImmutableObject#toDiffableFieldMap}.
|
|
*
|
|
* <p>This can be used to specify customized printing of certain fields (e.g. to print out a
|
|
* boolean field as "active" or "-" instead of "true" or "false"). It can also be used to add
|
|
* fields to the data, e.g. for computed fields that can be accessed from the object directly but
|
|
* aren't stored as simple fields.
|
|
*/
|
|
ImmutableMap<String, String> getFieldOverrides(@SuppressWarnings("unused") T object) {
|
|
return ImmutableMap.of();
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
// Get the object data first, so we can figure out the list of all available fields using the
|
|
// data if necessary.
|
|
ImmutableSet<T> objects = loadObjects();
|
|
// Get the list of fields we should return.
|
|
ImmutableSet<String> fieldsToUse = getFieldsToUse(objects);
|
|
// Convert the data into a table.
|
|
ImmutableTable<T, String, String> data = extractData(fieldsToUse, objects);
|
|
// Now that we have the data table, compute the column widths.
|
|
ImmutableMap<String, Integer> columnWidths =
|
|
computeColumnWidths(data, isHeaderRowInUse(data));
|
|
// Finally, convert the table to an array of lines of text.
|
|
List<String> lines = generateFormattedData(data, columnWidths);
|
|
// Return the results.
|
|
response.setPayload(ImmutableMap.of(
|
|
"lines", lines,
|
|
"status", "success"));
|
|
} catch (Exception e) {
|
|
String message = e.getMessage();
|
|
if (message == null) {
|
|
message = e.getClass().getName();
|
|
}
|
|
response.setPayload(ImmutableMap.of(
|
|
"error", message,
|
|
"status", "error"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the set of fields to return, aliased or not according to --full_field_names, and
|
|
* with duplicates eliminated but the ordering otherwise preserved.
|
|
*/
|
|
private ImmutableSet<String> getFieldsToUse(ImmutableSet<T> objects) {
|
|
// Get the list of fields from the received parameter.
|
|
List<String> fieldsToUse;
|
|
if ((fields == null) || !fields.isPresent()) {
|
|
fieldsToUse = new ArrayList<>();
|
|
} else {
|
|
fieldsToUse = Splitter.on(',').splitToList(fields.get());
|
|
// Check whether any field name is the wildcard; if so, use all fields.
|
|
if (fieldsToUse.contains("*")) {
|
|
fieldsToUse = getAllAvailableFields(objects);
|
|
}
|
|
}
|
|
// Handle aliases according to the state of the fullFieldNames parameter.
|
|
final ImmutableMap<String, String> nameMapping =
|
|
((fullFieldNames != null) && fullFieldNames.isPresent() && fullFieldNames.get())
|
|
? getFieldAliases() : getFieldAliases().inverse();
|
|
return ImmutableSet.copyOf(Iterables.transform(
|
|
Iterables.concat(getPrimaryKeyFields(), fieldsToUse),
|
|
new Function<String, String>() {
|
|
@Override
|
|
public String apply(String field) {
|
|
// Rename fields that are in the map according to the map, and leave the others as is.
|
|
return nameMapping.containsKey(field) ? nameMapping.get(field) : field;
|
|
}}));
|
|
}
|
|
|
|
/**
|
|
* Constructs a list of all available fields for use by the wildcard field specification.
|
|
* Don't include aliases, since then we'd wind up returning the same field twice.
|
|
*/
|
|
private ImmutableList<String> getAllAvailableFields(ImmutableSet<T> objects) {
|
|
ImmutableList.Builder<String> fields = new ImmutableList.Builder<>();
|
|
for (T object : objects) {
|
|
// Base case of the mapping is to use ImmutableObject's toDiffableFieldMap().
|
|
fields.addAll(object.toDiffableFieldMap().keySet());
|
|
// Next, overlay any field-level overrides specified by the subclass.
|
|
fields.addAll(getFieldOverrides(object).keySet());
|
|
}
|
|
return fields.build();
|
|
}
|
|
|
|
/**
|
|
* Returns a table of data for the given sets of fields and objects. The table is row-keyed by
|
|
* object and column-keyed by field, in the same iteration order as the provided sets.
|
|
*/
|
|
private ImmutableTable<T, String, String>
|
|
extractData(ImmutableSet<String> fields, ImmutableSet<T> objects) {
|
|
ImmutableTable.Builder<T, String, String> builder = ImmutableTable.builder();
|
|
for (T object : objects) {
|
|
Map<String, Object> fieldMap = new HashMap<>();
|
|
// Base case of the mapping is to use ImmutableObject's toDiffableFieldMap().
|
|
fieldMap.putAll(object.toDiffableFieldMap());
|
|
// Next, overlay any field-level overrides specified by the subclass.
|
|
fieldMap.putAll(getFieldOverrides(object));
|
|
// Next, add to the mapping all the aliases, with their values defined as whatever was in the
|
|
// map under the aliased field's original name.
|
|
fieldMap.putAll(
|
|
Maps.transformValues(getFieldAliases(), Functions.forMap(new HashMap<>(fieldMap))));
|
|
Set<String> expectedFields = ImmutableSortedSet.copyOf(fieldMap.keySet());
|
|
for (String field : fields) {
|
|
checkArgument(fieldMap.containsKey(field),
|
|
"Field '%s' not found - recognized fields are:\n%s", field, expectedFields);
|
|
builder.put(object, field, Objects.toString(fieldMap.get(field), ""));
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
/**
|
|
* Computes the column widths of the given table of strings column-keyed by strings and returns
|
|
* them as a map from column key name to integer width. The column width is defined as the max
|
|
* length of any string in that column, including the name of the column.
|
|
*/
|
|
private static ImmutableMap<String, Integer> computeColumnWidths(
|
|
ImmutableTable<?, String, String> data, final boolean includingHeader) {
|
|
return ImmutableMap.copyOf(Maps.transformEntries(
|
|
data.columnMap(),
|
|
new Maps.EntryTransformer<String, Map<?, String>, Integer>() {
|
|
@Override
|
|
public Integer transformEntry(String columnName, Map<?, String> columnValues) {
|
|
// Return the length of the longest string in this column (including the column name).
|
|
return Ordering.natural().max(Iterables.transform(
|
|
Iterables.concat(
|
|
ImmutableList.of(includingHeader ? columnName : ""),
|
|
columnValues.values()),
|
|
new Function<String, Integer>() {
|
|
@Override
|
|
public Integer apply(String value) {
|
|
return value.length();
|
|
}}));
|
|
}}));
|
|
}
|
|
|
|
/**
|
|
* Check whether to display headers. If the parameter is not set, print headers only if there
|
|
* is more than one column.
|
|
*/
|
|
private boolean isHeaderRowInUse(final ImmutableTable<?, String, String> data) {
|
|
return ((printHeaderRow != null) && printHeaderRow.isPresent())
|
|
? printHeaderRow.get() : (data.columnKeySet().size() > 1);
|
|
}
|
|
|
|
/** Converts the provided table of data to text, formatted using the provided column widths. */
|
|
private List<String> generateFormattedData(
|
|
ImmutableTable<T, String, String> data,
|
|
ImmutableMap<String, Integer> columnWidths) {
|
|
Function<Map<String, String>, String> rowFormatter = makeRowFormatter(columnWidths);
|
|
List<String> lines = new ArrayList<>();
|
|
|
|
if (isHeaderRowInUse(data)) {
|
|
// Add a row of headers (column names mapping to themselves).
|
|
Map<String, String> headerRow =
|
|
Maps.asMap(data.columnKeySet(), Functions.<String>identity());
|
|
lines.add(rowFormatter.apply(headerRow));
|
|
|
|
// Add a row of separator lines (column names mapping to '-' * column width).
|
|
Map<String, String> separatorRow = Maps.transformValues(columnWidths,
|
|
new Function<Integer, String>() {
|
|
@Override
|
|
public String apply(Integer width) {
|
|
return Strings.repeat("-", width);
|
|
}});
|
|
lines.add(rowFormatter.apply(separatorRow));
|
|
}
|
|
|
|
// Add the actual data rows.
|
|
for (Map<String, String> row : data.rowMap().values()) {
|
|
lines.add(rowFormatter.apply(row));
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Returns for the given column widths map a row formatting function that converts a row map (of
|
|
* column keys to cell values) into a single string with each column right-padded to that width.
|
|
*
|
|
* <p>The resulting strings separate padded fields with two spaces and each end in a newline.
|
|
*/
|
|
private static Function<Map<String, String>, String> makeRowFormatter(
|
|
final Map<String, Integer> columnWidths) {
|
|
return new Function<Map<String, String>, String>() {
|
|
@Override
|
|
public String apply(Map<String, String> rowByColumns) {
|
|
List<String> paddedFields = new ArrayList<>();
|
|
for (Map.Entry<String, String> cell : rowByColumns.entrySet()) {
|
|
paddedFields.add(Strings.padEnd(cell.getValue(), columnWidths.get(cell.getKey()), ' '));
|
|
}
|
|
return Joiner.on(" ").join(paddedFields);
|
|
}};
|
|
}
|
|
}
|