All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.facebook.tools.io.IO Maven / Gradle / Ivy

There is a newer version: 0.1.32
Show newest version
/*
 * Copyright (C) 2014 Facebook, Inc.
 *
 * 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 com.facebook.tools.io;

import com.facebook.tools.subprocess.SubprocessBuilder;

import java.io.Console;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Container for standard input/output-related objects. Mimics {@link java.lang.System} with the
 * following public fields:
 * 
*
{@link #out}
*
A {@link com.facebook.tools.io.StatusPrintStream} instance for stdout-like output
*
{@link #err}
*
A {@link com.facebook.tools.io.PrintStreamPlus} instance for stderr-like output
*
{@link #in}
*
A {@link com.facebook.tools.io.Input} instance for stdin-like input
*
*

* The {@link #ask(Enum, String, Object...)} and {@link #ask(Enum, String)} methods can be used to * easily prompt for decisions. *

* Also included is a {@link #subprocess} builder for spawing new processes. *

* Some behavior depends on whether Java is being run on an interactive terminal or spawned from * another process, e.g., {@literal java -jar my-tool.jar} vs {@literal java -jar my-tool.jar | wc}. * If interactive, output to {@link #err} inserts * ANSII escape codes to render * error output as white text on a bright red background. Additional escape codes are used so that * consecutive {@link com.facebook.tools.io.Status} methods overwrite previous ones, making them * appropriate for outputting status that would otherwise be too spammy, e.g., * {@code io.out.statusf("Finished %s of %s", done, total);}. * If non-interactive, no escape codes are inserted, and {@link com.facebook.tools.io.Status} * methods do nothing. */ public class IO { private static final String WHITE_ON_RED = "\033[1;37;41m"; private static final String DEFAULT_COLORS = "\033[0m"; public final StatusPrintStream out; public final PrintStreamPlus err; public final Input in; public final SubprocessBuilder subprocess; public IO(PrintStream out, PrintStream err, Input in, SubprocessBuilder subprocess) { Console console = System.console(); if (console == null) { this.out = new NoninteractiveStatusPrintStream(out); this.err = new NoninteractiveStatusPrintStream(err); } else { ConsoleStatus status = new ConsoleStatus(console.writer()); this.out = new InteractiveStatusPrintStream(out, status, DEFAULT_COLORS); this.err = new InteractiveStatusPrintStream(err, status, WHITE_ON_RED); Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() { @Override public void run() { // reset console color and erase final status line IO.this.out.print(""); } } ) ); } this.in = in; this.subprocess = subprocess; } /** * Creates a new container using {@link java.lang.System#out}, {@link java.lang.System#err}, * and {@link java.lang.System#in}. */ public IO() { this(System.out, System.err, new InputStreamInput(System.in)); } public IO(PrintStream out, PrintStream err, Input in) { this(out, err, in, new SubprocessBuilder()); } /** * Convenience method equivalent to ask(defaultValue, String.format(format, args)). */ public T ask(T defaultValue, String format, Object... args) { return ask(defaultValue, String.format(format, args)); } /** * Prompts the user to make a decision. For example: *


   * if (io.ask(YesNo.YES, "Are you sure you want to dance").isYes()) {
   *   io.out.println("Dance party!!");
   * } else {
   *   io.out.println("Some other time, then…");
   * }
   * 

*

*

   * > Are you sure you want to dance? [Y/n] x
   * > Are you sure you want to dance? [Y/n] this is not a valid response
   * > Are you sure you want to dance? [Y/n] y
   * > Dance party!!
   * 
* The options shown are determined by {@code defaultValue} which must be an {@code enum} with * its members annotated with {@link com.facebook.tools.io.Answer}. The first * {@link com.facebook.tools.io.Answer#value()} is show in the brackets after the prompt, but any * of the values is accepted. The values must all be lower-case; the default value, i.e., the one * used if the user simply hits enter, is capitalized. * * @param defaultValue value to use if no input is given * @param prompt message to show user * @return the {@code enum} value corresponding to the user's input */ public T ask(T defaultValue, String prompt) { Class enumClass = defaultValue.getDeclaringClass(); Field[] fields = enumClass.getFields(); Map answers = new LinkedHashMap<>(); StringBuilder displayValues = new StringBuilder(16); for (Field field : fields) { Answer annotation = field.getAnnotation(Answer.class); if (annotation != null) { int modifiers = field.getModifiers(); if (enumClass.isAssignableFrom(field.getType()) && Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { String fieldName = enumClass.getName() + "." + field.getName(); T answer; try { answer = (T) field.get(enumClass); } catch (IllegalAccessException | RuntimeException e) { throw new IllegalArgumentException("Error accessing " + fieldName, e); } String[] values = annotation.value(); if (values == null || values.length == 0) { throw new NullPointerException("Missing values for " + fieldName); } for (String value : values) { if (value == null) { throw new NullPointerException("Null value for " + fieldName); } if (!value.equals(value.trim().toLowerCase())) { throw new IllegalArgumentException( String.format("Values must be all lower-case, but got %s for %s", value, fieldName) ); } T existing = answers.put(value, answer); if (existing != null) { throw new IllegalArgumentException( String.format("Duplicate value %s for %s", value, fieldName) ); } } if (displayValues.length() > 0) { displayValues.append("/"); } if (defaultValue == answer) { displayValues.append(values[0].toUpperCase()); } else { displayValues.append(values[0]); } } } } if (answers.isEmpty()) { throw new IllegalArgumentException("No fields annotated with @PromptAnswer in " + enumClass); } String fullPrompt = String.format("%s [%s]? ", prompt, displayValues); T answer; do { out.print(fullPrompt); out.flush(); String response = in.readLine(); response = response == null ? "" : response.trim().toLowerCase(); if (response.isEmpty()) { answer = defaultValue; } else { answer = answers.get(response); } } while (answer == null); return answer; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy