Java tutorial
// 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.server; import static com.google.common.base.Preconditions.checkArgument; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; 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.setStatus(e instanceof IllegalArgumentException ? SC_BAD_REQUEST : SC_INTERNAL_SERVER_ERROR); 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 = new 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); } }; } }