// 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.request; import static java.util.stream.Collectors.joining; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Streams; import java.util.Map; /** * Utility class to help in dumping routing maps. * *

Each of the App Engine services (frontend, backend, and tools) has a Dagger component used for * routing requests (e.g. FrontendRequestComponent). This class produces a text file representation * of the routing configuration, showing what paths map to what action classes, as well as the * properties of the action classes' annotations (which cover things like allowable HTTP methods, * authentication settings, etc.). The text file can be useful for documentation, and is also used * in unit tests to check against golden routing maps to find possibly unexpected changes. * *

The file has fixed-width columns with a header row. The width of the columns is determined by * the content to be displayed. The columns are: * *

    *
  1. the URL path which maps to this action (with a "(*)" after it if the prefix flag is set) *
  2. the simple name of the action class *
  3. the allowable HTTP methods *
  4. whether to automatically print "ok" in the response *
  5. the allowable authentication methods *
  6. the minimum authentication level *
  7. the user policy *
* *

See the Auth class for more information about authentication settings. */ public class RouterDisplayHelper { private static final String PATH = "path"; private static final String CLASS = "class"; private static final String METHODS = "methods"; private static final String AUTH_METHODS = "authMethods"; private static final String MINIMUM_LEVEL = "minLevel"; private static final String FORMAT = "%%-%ds %%-%ds %%-%ds %%-2s %%-%ds %%-%ds %%s"; /** Returns a string representation of the routing map in the specified component. */ public static String extractHumanReadableRoutesFromComponent(Class componentClass) { return formatRoutes(Router.extractRoutesFromComponent(componentClass).values()); } private static String getFormatString(Map columnWidths) { return String.format( FORMAT, columnWidths.get(PATH), columnWidths.get(CLASS), columnWidths.get(METHODS), columnWidths.get(AUTH_METHODS), columnWidths.get(MINIMUM_LEVEL)); } private static String headerToString(String formatString) { return String.format( formatString, "PATH", "CLASS", "METHODS", "OK", "AUTH_METHODS", "MIN", "USER_POLICY"); } private static String routeToString(Route route, String formatString) { return String.format( formatString, route.action().isPrefix() ? (route.action().path() + "(*)") : route.action().path(), route.actionClass().getSimpleName(), Joiner.on(",").join(route.action().method()), route.action().automaticallyPrintOk() ? "y" : "n", Joiner.on(",").join(route.action().auth().authSettings().methods()), route.action().auth().authSettings().minimumLevel(), route.action().auth().authSettings().userPolicy()); } private static String formatRoutes(Iterable routes) { // Use the column header length as a minimum. int pathWidth = 4; int classWidth = 5; int methodsWidth = 7; int authMethodsWidth = 12; int minLevelWidth = 3; for (Route route : routes) { int len = route.action().isPrefix() ? (route.action().path().length() + 3) : route.action().path().length(); if (len > pathWidth) { pathWidth = len; } len = route.actionClass().getSimpleName().length(); if (len > classWidth) { classWidth = len; } len = Joiner.on(",").join(route.action().method()).length(); if (len > methodsWidth) { methodsWidth = len; } len = Joiner.on(",").join(route.action().auth().authSettings().methods()).length(); if (len > authMethodsWidth) { authMethodsWidth = len; } len = route.action().auth().authSettings().minimumLevel().toString().length(); if (len > minLevelWidth) { minLevelWidth = len; } } final String formatString = getFormatString( new ImmutableMap.Builder() .put(PATH, pathWidth) .put(CLASS, classWidth) .put(METHODS, methodsWidth) .put(AUTH_METHODS, authMethodsWidth) .put(MINIMUM_LEVEL, minLevelWidth) .build()); return headerToString(formatString) + String.format("%n") + Streams.stream(routes) .map(route -> routeToString(route, formatString)) .collect(joining(String.format("%n"))); } }