content: 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);
      }
  }
  