package act.cli; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * 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. * #L% */ import act.app.ActionContext; import act.app.App; import act.cli.ascii_table.ASCIITableHeader; import act.cli.ascii_table.impl.SimpleASCIITableImpl; import act.cli.ascii_table.spec.IASCIITable; import act.cli.ascii_table.spec.IASCIITableAware; import act.cli.builtin.Exit; import act.cli.builtin.Help; import act.cli.util.CommandLineParser; import act.handler.CliHandler; import act.util.ActContext; import act.util.PropertySpec; import jline.console.ConsoleReader; import org.osgl.$; import org.osgl.cache.CacheService; import org.osgl.concurrent.ContextLocal; import org.osgl.http.H; import org.osgl.util.C; import org.osgl.util.E; import org.osgl.util.S; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; public class CliContext extends ActContext.Base<CliContext> implements IASCIITable { public static class ParsingContext { // The number of options plus arguments in // the command executor method params. // This does not include the provided params (e.g. those // params that should be injected by framework, like. App etc) private int optionArgumentsCnt; // mark the current loading argument private AtomicInteger curArgId; // Keep track the number of options provided // for a specific required group Map<String, AtomicInteger> required; private ParsingContext() {} public AtomicInteger curArgId() { return curArgId; } public boolean hasArguments(CommandLineParser command) { return curArgId.get() < command.argumentCount(); } public void foundRequired(String group) { required.get(group).incrementAndGet(); } public boolean hasMultipleOptionArguments() { return optionArgumentsCnt > 1; } public Set<String> missingOptions() { Set<String> set = new HashSet<String>(); for (Map.Entry<String, AtomicInteger> entry : required.entrySet()) { if (entry.getValue().get() < 1) { set.add(entry.getKey()); } } return set; } public void raiseExceptionIfThereAreMissingOptions(CliContext context) { Set<String> missings = missingOptions(); int missing = missings.size(); switch (missing) { case 0: return; case 1: // check if there are command argument if (!context.arguments().isEmpty()) { return; } default: throw new CliException("Missing required options: %s", missings); } } public ParsingContext copy() { ParsingContext ctx = new ParsingContext(); ctx.optionArgumentsCnt = optionArgumentsCnt; ctx.required = new HashMap<String, AtomicInteger>(required); for (Map.Entry<String, AtomicInteger> entry : ctx.required.entrySet()) { entry.setValue(new AtomicInteger(0)); } ctx.curArgId = new AtomicInteger(0); return ctx; } } public static class ParsingContextBuilder { private static final ThreadLocal<ParsingContext> ctx = new ThreadLocal<ParsingContext>(); public static void start() { ParsingContext ctx0 = new ParsingContext(); ctx0.required = new HashMap<String, AtomicInteger>(); ctx.set(ctx0); } public static void foundOptional() { ctx.get().optionArgumentsCnt++; } public static void foundArgument() { ctx.get().optionArgumentsCnt++; } public static void foundRequired(String group) { ParsingContext ctx0 = ctx.get(); ctx0.optionArgumentsCnt++; ctx0.required.put(group, new AtomicInteger(0)); } public static ParsingContext finish() { ParsingContext ctx0 = ctx.get(); ctx.remove(); return ctx0; } } public static final String ATTR_PWD = "__act_pwd__"; public static final String ATTR_METHOD = "__act_method__"; private static final ContextLocal<CliContext> _local = $.contextLocal(); private CliSession session; private String commandPath; // e.g. myapp.cli.ListUser private Map<String, Object> commanderInstances = C.newMap(); private ConsoleReader console; // workaround of issue http://stackoverflow.com/questions/34467383/jline2-print-j-when-it-should-print-n-on-a-telnet-console private PrintWriter pw; private CommandLineParser parser; private IASCIITable asciiTable; private CacheService evaluatorCache; private ParsingContext parsingContext; private CliHandler handler; private boolean rawPrint; private Map<String, String> preparsedOptionValues; public CliContext(String line, App app, ConsoleReader console, CliSession session) { this(line, app, console, session, null == System.getenv("cli-no-raw-print")); } protected CliContext(String line, App app, ConsoleReader console, CliSession session, boolean rawPrint) { super(app); this.session = session; this.parser = new CommandLineParser(line); this.evaluatorCache = app.cache(); this.console = $.NPE(console); this.pw = new PrintWriter(console.getOutput()); this.rawPrint = rawPrint; this.handler = app.cliDispatcher().handler(command()); this.preparsedOptionValues = new HashMap<String, String>(); this.saveLocal(); } /** * Set the console prompt * @param prompt the prompt */ public void prompt(String prompt) { console.setPrompt(prompt); } public void prepare(ParsingContext ctx) { this.parsingContext = ctx.copy(); } public ParsingContext parsingContext() { return this.parsingContext; } /** * Reset the console prompt to "{@code act[<session-id>]>}" */ public void resetPrompt() { prompt("act[" + session.id() + "]>"); } public CacheService evaluatorCache() { return evaluatorCache; } public CommandLineParser commandLine() { return parser; } public String command() { return parser.command(); } public List<String> arguments() { return parser.arguments(); } public CliSession session() { return session; } @Override public Set<String> paramKeys() { return preparsedOptionValues.keySet(); } public void param(String key, String val) { this.preparsedOptionValues.put(key, val); } @Override public String paramVal(String key) { String s = this.preparsedOptionValues.get(key); if (null == s) { // try free options s = parser.getOptions().get(S.concat("--", key)); } return s; } @Override public String[] paramVals(String key) { return new String[]{paramVal(key)}; } /** * Return the current working directory * @return the current working directory */ public File curDir() { File file = session().attribute(ATTR_PWD); if (null == file) { file = new File(System.getProperty("user.dir")); session().attribute(ATTR_PWD, file); } return file; } public CliContext chDir(File dir) { E.illegalArgumentIf(!dir.isDirectory()); session().attribute(ATTR_PWD, dir); return this; } @Override public CliContext accept(H.Format fmt) { throw E.unsupport(); } @Override public H.Format accept() { throw E.unsupport(); } public void flush() { pw.flush(); } public boolean disconnected() { return pw.checkError(); } public void print(String template, Object ... args) { if (rawPrint) { print1(template, args); } else { print0(template, args); } } /** * Run handler and return `false` if it needs to exit the CLI or `true` otherwise */ void handle() throws IOException { if (null == handler) { println("Command not recognized: %s", command()); return; } if (handler == Exit.INSTANCE) { handler.handle(this); } CommandLineParser parser = commandLine(); boolean help = parser.getBoolean("-h", "--help"); if (help) { Help.INSTANCE.showHelp(parser.command(), this); } else { try { session.handler(handler); handler.handle(this); } catch ($.Break b) { throw b; } catch (Exception e) { console.println("Error: " + e.getMessage()); } } } private void print0(String template, Object... args) { try { console.print(S.fmt(template, args)); } catch (IOException e) { throw E.ioException(e); } } private void print1(String template, Object ... args) { if (args.length == 0) { pw.print(template); } else { pw.printf(osNative(template), args); } } private void println0(String template, Object... args) { try { if (args.length > 0) { template = S.fmt(template); } console.println(template); } catch (IOException e) { throw E.ioException(e); } } public void println() { if (rawPrint) { println1(""); } else { println0(""); } } public void println(String template, Object... args) { if (rawPrint) { println1(template, args); } else { println0(template, args); } } private void println1(String template, Object... args) { if (args.length == 0) { pw.print(template); } else { pw.printf(osNative(template), args); } pw.println(); } @Override protected void releaseResources() { super.releaseResources(); _local.remove(); PropertySpec.current.remove(); } public String commandPath() { return commandPath; } public CliContext commandPath(String path) { commandPath = path; return this; } @Override public String methodPath() { return commandPath; } public CliContext __commanderInstance(String className, Object instance) { if (null == commanderInstances) { commanderInstances = C.newMap(); } commanderInstances.put(className, instance); return this; } public Object __commanderInstance(String className) { return null == commanderInstances ? null : commanderInstances.get(className); } @Override public <T> T renderArg(String name) { return super.renderArg(name); } @Override public CliContext renderArg(String name, Object val) { return super.renderArg(name, val); } @Override public Map<String, Object> renderArgs() { return super.renderArgs(); } /** * Called by bytecode enhancer to set the name list of the render arguments that is update * by the enhancer * @param names the render argument names separated by "," * @return this AppContext */ @SuppressWarnings("unused") public CliContext __appRenderArgNames(String names) { return renderArg("__arg_names__", C.listOf(names.split(","))); } @SuppressWarnings("unused") public List<String> __appRenderArgNames() { return renderArg("__arg_names__"); } private synchronized IASCIITable tbl() { if (asciiTable == null) { asciiTable = new SimpleASCIITableImpl(new PrintWriter(console.getOutput())); } return asciiTable; } @Override public void printTable(String[] header, String[][] data) { tbl().printTable(header, data); } @Override public void printTable(String[] header, String[][] data, int dataAlign) { tbl().printTable(header, data, dataAlign); } @Override public void printTable(String[] header, int headerAlign, String[][] data, int dataAlign) { tbl().printTable(header, headerAlign, data, dataAlign); } @Override public void printTable(ASCIITableHeader[] headerObjs, String[][] data) { tbl().printTable(headerObjs, data); } @Override public void printTable(IASCIITableAware asciiTableAware) { tbl().printTable(asciiTableAware); } @Override public String getTable(String[] header, String[][] data) { return tbl().getTable(header, data); } @Override public String getTable(String[] header, String[][] data, int dataAlign) { return tbl().getTable(header, data, dataAlign); } @Override public String getTable(String[] header, int headerAlign, String[][] data, int dataAlign) { return tbl().getTable(header, headerAlign, data, dataAlign); } @Override public String getTable(ASCIITableHeader[] headerObjs, String[][] data) { return tbl().getTable(headerObjs, data); } @Override public String getTable(IASCIITableAware asciiTableAware) { return tbl().getTable(asciiTableAware); } public File getFile(String path) { if (path.startsWith("~/")) { path = System.getProperty("user.home") + path.substring(1); } File file = new File(path); if (file.isAbsolute()) { return file; } file = new File(curDir(), path); return new File(file.getAbsolutePath()); } private void saveLocal() { _local.set(this); } private void initOverHttp(ActionContext actionContext) { } public static CliContext current() { return _local.get(); } private static String osNative(String s) { s = s.replace("\n\r", "\n"); s = s.replace("\r", "\n"); if ("\n".equals($.OS.lineSeparator())) { return s; } s = s.replace("\n", $.OS.lineSeparator()); return s; } }