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.Act; import act.app.App; import act.app.AppServiceBase; import act.cli.builtin.Exit; import act.cli.builtin.Help; import act.cli.builtin.IterateCursor; import act.cli.meta.CommandMethodMetaInfo; import act.cli.meta.CommanderClassMetaInfo; import act.handler.CliHandler; import act.handler.builtin.cli.CliHandlerProxy; import act.route.Router; import org.osgl.$; import org.osgl.http.H; import org.osgl.logging.LogManager; import org.osgl.logging.Logger; import org.osgl.util.C; import org.osgl.util.E; import org.osgl.util.Keyword; import org.osgl.util.S; import javax.inject.Inject; import java.util.*; /** * Dispatch console command to CLI command handler */ public class CliDispatcher extends AppServiceBase<CliDispatcher> { private static Logger logger = LogManager.get(CliDispatcher.class); private static final String NAME_PART_SEPARATOR = "[\\.\\-_]+"; private Map<Keyword, CliHandler> registry = new HashMap<>(); private Map<Keyword, String> rawNameRepo = new HashMap<>(); private Map<String, String> shortCuts = new HashMap<>(); private Map<String, List<CliHandler>> ambiguousShortCuts = new HashMap<>(); private Map<CliHandler, List<String>> nameMap = new HashMap<>(); private Map<CliHandler, List<String>> shortCutMap = new HashMap<>(); private Router cmdRouter; private Router defRouter; @Inject public CliDispatcher(App app) { super(app); cmdRouter = app.cliOverHttpRouter(); if (app.isDev()) { defRouter = app.router(); } app.jobManager().now(new Runnable() { @Override public void run() { registerBuiltInHandlers(); } }); app.jobManager().beforeAppStart(new Runnable() { @Override public void run() { for (Map.Entry<Keyword, CliHandler> entry : registry.entrySet()) { Keyword keyword = entry.getKey(); CliHandler handler = entry.getValue(); if (handler instanceof CliHandlerProxy) { CliHandlerProxy proxy = $.cast((handler)); CommandMethodMetaInfo methodMetaInfo = proxy.methodMetaInfo(); String handlerName = methodMetaInfo.fullName(); Set<String> variations = new TreeSet<>(); variations.add(keyword.kebabCase()); variations.add(keyword.snakeCase()); variations.add(keyword.javaVariable()); variations.add(keyword.dotted()); for (String s : variations) { S.Buffer buf = S.buffer(); if (!handlerName.startsWith("act.")) { buf.a("/~/cmd/run/"); } else { buf.a("/cmd/run/"); } String urlPath = buf.a(s).toString(); cmdRouter.addMapping(H.Method.GET, urlPath, handlerName); cmdRouter.addMapping(H.Method.POST, urlPath, handlerName); if (null != defRouter) { defRouter.addMapping(H.Method.GET, urlPath, handlerName); defRouter.addMapping(H.Method.POST, urlPath, handlerName); } } } } } }); } public CliDispatcher registerCommandHandler(String command, CommandMethodMetaInfo methodMetaInfo, CommanderClassMetaInfo classMetaInfo) { String sa[] = command.split(CommanderClassMetaInfo.NAME_SEPARATOR); for (String s : sa) { if (registry.containsKey(s)) { throw E.invalidConfiguration("Command %s already registered", command); } addToRegistry(s, new CliHandlerProxy(classMetaInfo, methodMetaInfo, app())); logger.debug("Command registered: %s", s); } return this; } public boolean registered(String command) { return registry.containsKey(command); } public CliHandler handler(String command) { String command0 = command; command = shortCuts.get(command); if (null == command) { command = command0; } Keyword keyword = Keyword.of(command); CliHandler handler = registry.get(keyword); if (null == handler && !command.startsWith("act.")) { handler = registry.get(Keyword.of("act." + command)); } Act.Mode mode = Act.mode(); if (null != handler && handler.appliedIn(mode)) { return handler; } return null; } /** * Returns a list of system commands in alphabetic order * * @return the system command list */ public List<String> systemCommands() { return commands(true, false); } /** * Returns a list of application commands in alphabetic order * * @return the application command list */ public List<String> applicationCommands() { return commands(false, true); } public SortedSet<CliCmdInfo> commandInfoList(boolean sys, boolean app) { SortedSet<CliCmdInfo> list = new TreeSet<>(); boolean all = !sys && !app; for (Map.Entry<Keyword, CliHandler> entry : registry.entrySet()) { Keyword keyword = entry.getKey(); String s = rawNameRepo.get(keyword); boolean isSysCmd = s.startsWith("act."); if (isSysCmd && !sys && !all) { continue; } if (!isSysCmd && !app && !all) { continue; } CliHandler h = entry.getValue(); CliCmdInfo info = new CliCmdInfo(); info.help = h.commandLine()._2; info.name = s; List<String> shortcuts = shortCuts(h); if (null != shortcuts && !shortcuts.isEmpty()) { info.shortcut = shortcuts.get(0); } else { info.shortcut = info.name; } if (info.shortcut.length() > s.length()) { info.shortcut = s; } if (info.shortcut.startsWith("act.")) { info.shortcut = info.shortcut.substring(4); } list.add(info); } return list; } public List<String> commandsWithShortcut(boolean sys, boolean app) { return commands0(sys, app, true); } /** * Returns all commands in alphabetic order * * @return the list of commands */ public List<String> commands(boolean sys, boolean app) { return commands0(sys, app, false); } private List<String> commands0(boolean sys, boolean app, boolean withShortcut) { C.List<String> list = C.newList(); Act.Mode mode = Act.mode(); boolean all = !sys && !app; for (Keyword keyword : registry.keySet()) { String s = rawNameRepo.get(keyword); boolean isSysCmd = s.startsWith("act."); if (isSysCmd && !sys && !all) { continue; } if (!isSysCmd && !app && !all) { continue; } CliHandler h = registry.get(keyword); if (h.appliedIn(mode)) { if (withShortcut) { List<String> shortcuts = shortCuts(h); if (null != shortcuts && !shortcuts.isEmpty()) { s = s + " | " + shortcuts.get(0); } } list.add(s); } } return list.sorted(new $.Comparator<String>() { @Override public int compare(String o1, String o2) { boolean b1 = (o1.startsWith("act.")); boolean b2 = (o2.startsWith("act.")); if (b1 & !b2) { return -1; } if (!b1 & b2) { return 1; } return o1.compareTo(o2); } }); } public List<String> names(CliHandler handler) { return nameMap.get(handler); } public List<String> shortCuts(CliHandler handler) { return shortCutMap.get(handler); } @Override protected void releaseResources() { registry.clear(); } private void addToRegistry0(String name, CliHandler handler) { Keyword keyword = Keyword.of(name); registry.put(keyword, handler); rawNameRepo.put(keyword, name); } private void addToRegistry(String name, CliHandler handler) { addToRegistry0(name, handler); Help.updateMaxWidth(name.length()); updateNameIndex(name, handler); registerShortCut(name, handler); } private void addRouterMapping(String name, CommandMethodMetaInfo methodMetaInfo) { if (null != cmdRouter) { String handlerName = methodMetaInfo.fullName(); String urlPath = S.concat("~/cmd/run/", name); cmdRouter.addMapping(H.Method.GET, urlPath, methodMetaInfo.fullName()); cmdRouter.addMapping(H.Method.POST, urlPath, methodMetaInfo.fullName()); } } private void resolveCommandPrefix() { Map<Keyword, CliHandler> temp = new HashMap<>(registry); registry.clear(); App app = app(); for (Map.Entry<Keyword, CliHandler> pair : temp.entrySet()) { Keyword keyword = pair.getKey(); String name = rawNameRepo.get(keyword); CliHandler handler = pair.getValue(); if (handler instanceof CliHandlerProxy) { CliHandlerProxy proxy = $.cast(handler); Class<?> type = app.classForName(proxy.classMetaInfo().className()); CommandPrefix prefix = type.getAnnotation(CommandPrefix.class); if (null != prefix) { String pre = prefix.value(); if (S.notBlank(pre)) { name = S.pathConcat(pre, '.', rawNameRepo.get(keyword)); } } } addToRegistry(name, handler); } } private void updateNameIndex(String name, CliHandler handler) { List<String> nameList = nameMap.get(handler); if (null == nameList) { nameList = new ArrayList<>(); nameMap.put(handler, nameList); } nameList.add(name); } private void registerShortCut(String name, CliHandler handler) { List<String> shortCutNames = shortCutMap.get(handler); if (null == shortCutNames) { shortCutNames = new ArrayList<>(); shortCutMap.put(handler, shortCutNames); } for (int i = 0; i < 5; ++i) { String shortCut = shortCut(name, i); if (null == shortCut) { continue; } if (ambiguousShortCuts.containsKey(shortCut)) { List<CliHandler> list = ambiguousShortCuts.get(shortCut); list.add(handler); } else if (shortCuts.containsKey(shortCut)) { shortCuts.remove(shortCut); List<CliHandler> list = new ArrayList<>(); ambiguousShortCuts.put(shortCut, list); for (List<String> ls : shortCutMap.values()) { ls.remove(shortCut); } } else { shortCuts.put(shortCut, name); shortCutNames.add(shortCut); } } } /** * level: * * 0 - "foo.bar.zee" -> ".fbz" * 1 - "foo.bar.zee" -> "f.b.z" * 2 - "foo.bar.zee" -> "f.b.zee" * 3 - "foo.bar.zee" -> "f.bar.z" * 4 - "foo.bar.zee" -> "fo.ba.ze" */ private static String shortCut(String name, int level) { String sa[] = name.split(NAME_PART_SEPARATOR); if (sa.length < 2) { return null; } S.Buffer sb = S.buffer(); switch (level) { case 0: sb.append("."); for (String s : sa) { sb.append(s.charAt(0)); } return sb.toString(); case 1: for (String s : sa) { sb.append(s.charAt(0)).append("."); } sb.deleteCharAt(sb.length() - 1); return sb.toString(); case 2: for (int i = 0; i < sa.length - 1; ++i) { sb.append(sa[i].charAt(0)).append("."); } sb.append(sa[sa.length - 1]); return sb.toString(); case 3: for (int i = 0; i < sa.length - 2; ++i) { sb.append(sa[i].charAt(0)).append("."); } sb.append(sa[sa.length - 2]).append("."); sb.append(sa[sa.length - 1].charAt(0)); return sb.toString(); case 4: for (String s : sa) { sb.append(s.charAt(0)); if (s.length() > 1) { sb.append(s.charAt(1)); } sb.append("."); } sb.deleteCharAt(sb.length() - 1); return sb.toString(); default: throw E.unsupport(); } } private void registerBuiltInHandlers() { addToRegistry("act.exit", Exit.INSTANCE); addToRegistry("act.quit", Exit.INSTANCE); addToRegistry("act.bye", Exit.INSTANCE); addToRegistry("act.help", Help.INSTANCE); addToRegistry("act.it", IterateCursor.INSTANCE); } }