001/* 002 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining 005 * a copy of this software and associated documentation files (the 006 * "Software"), to deal in the Software without restriction, including 007 * without limitation the rights to use, copy, modify, merge, publish, 008 * distribute, sublicense, and/or sell copies of the Software, and to 009 * permit persons to whom the Software is furnished to do so, subject to 010 * the following conditions: 011 * 012 * The above copyright notice and this permission notice shall be 013 * included in all copies or substantial portions of the Software. 014 * 015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 016 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 017 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 018 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 019 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 020 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 021 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 022 */ 023 024package co.aikar.commands; 025 026import com.google.common.collect.SetMultimap; 027 028import java.util.ArrayList; 029import java.util.Comparator; 030import java.util.HashSet; 031import java.util.Iterator; 032import java.util.List; 033import java.util.Set; 034import java.util.regex.Pattern; 035 036@SuppressWarnings("WeakerAccess") 037public class CommandHelp { 038 private final CommandManager manager; 039 private final CommandIssuer issuer; 040 private final List<HelpEntry> helpEntries = new ArrayList<>(); 041 private final String commandName; 042 final String commandPrefix; 043 private int page; 044 private int perPage; 045 List<String> search; 046 private HelpEntry selectedEntry; 047 private int totalResults; 048 private int totalPages; 049 private boolean lastPage; 050 051 public CommandHelp(CommandManager manager, RootCommand rootCommand, CommandIssuer issuer) { 052 this.manager = manager; 053 this.issuer = issuer; 054 this.perPage = manager.defaultHelpPerPage; 055 this.commandPrefix = manager.getCommandPrefix(issuer); 056 this.commandName = rootCommand.getCommandName(); 057 058 059 SetMultimap<String, RegisteredCommand> subCommands = rootCommand.getSubCommands(); 060 Set<RegisteredCommand> seen = new HashSet<>(); 061 062 if (!rootCommand.getDefCommand().hasHelpCommand) { 063 helpEntries.add(new HelpEntry(this, rootCommand.getDefaultRegisteredCommand())); 064 seen.add(rootCommand.getDefaultRegisteredCommand()); 065 } 066 067 subCommands.entries().forEach(e -> { 068 String key = e.getKey(); 069 if (key.equals(BaseCommand.DEFAULT) || key.equals(BaseCommand.CATCHUNKNOWN)) { 070 return; 071 } 072 073 RegisteredCommand regCommand = e.getValue(); 074 075 if (!regCommand.isPrivate && regCommand.hasPermission(issuer) && !seen.contains(regCommand)) { 076 this.helpEntries.add(new HelpEntry(this, regCommand)); 077 seen.add(regCommand); 078 } 079 }); 080 } 081 082 @UnstableAPI // Not sure on this one yet even when API becomes unstable 083 protected void updateSearchScore(HelpEntry help) { 084 if (this.search == null || this.search.isEmpty()) { 085 help.setSearchScore(1); 086 return; 087 } 088 final RegisteredCommand<?> cmd = help.getRegisteredCommand(); 089 090 int searchScore = 0; 091 for (String word : this.search) { 092 Pattern pattern = Pattern.compile(".*" + Pattern.quote(word) + ".*", Pattern.CASE_INSENSITIVE); 093 for (String subCmd : cmd.registeredSubcommands) { 094 Pattern subCmdPattern = Pattern.compile(".*" + Pattern.quote(subCmd) + ".*", Pattern.CASE_INSENSITIVE); 095 if (pattern.matcher(subCmd).matches()) { 096 searchScore += 3; 097 } else if (subCmdPattern.matcher(word).matches()) { 098 searchScore++; 099 } 100 } 101 102 103 if (pattern.matcher(help.getDescription()).matches()) { 104 searchScore += 2; 105 } 106 if (pattern.matcher(help.getParameterSyntax()).matches()) { 107 searchScore++; 108 } 109 if (help.getSearchTags() != null && pattern.matcher(help.getSearchTags()).matches()) { 110 searchScore += 2; 111 } 112 } 113 help.setSearchScore(searchScore); 114 } 115 116 public CommandManager getManager() { 117 return manager; 118 } 119 120 public boolean isExactMatch(String command) { 121 for (HelpEntry helpEntry : helpEntries) { 122 if (helpEntry.getCommand().endsWith(" " + command)) { 123 selectedEntry = helpEntry; 124 return true; 125 } 126 } 127 return false; 128 } 129 130 public void showHelp() { 131 showHelp(issuer); 132 } 133 134 public void showHelp(CommandIssuer issuer) { 135 CommandHelpFormatter formatter = manager.getHelpFormatter(); 136 if (selectedEntry != null) { 137 formatter.showDetailedHelp(this, selectedEntry); 138 return; 139 } 140 141 List<HelpEntry> helpEntries = getHelpEntries(); 142 Iterator<HelpEntry> results = helpEntries.stream() 143 .filter(HelpEntry::shouldShow) 144 .sorted(Comparator.comparingInt(helpEntry -> helpEntry.getSearchScore() * -1)).iterator(); 145 if (!results.hasNext()) { 146 issuer.sendMessage(MessageType.ERROR, MessageKeys.NO_COMMAND_MATCHED_SEARCH, "{search}", ACFUtil.join(this.search, " ")); 147 helpEntries = getHelpEntries(); 148 results = helpEntries.iterator(); 149 } 150 this.totalResults = helpEntries.size(); 151 int min = (this.page - 1) * this.perPage; // TODO: per page configurable? 152 int max = min + this.perPage; 153 this.totalPages = (int) Math.ceil((float) totalResults / (float) this.perPage); 154 int i = 0; 155 if (min >= totalResults) { 156 issuer.sendMessage(MessageType.HELP, MessageKeys.HELP_NO_RESULTS); 157 return; 158 } 159 160 List<HelpEntry> printEntries = new ArrayList<>(); 161 while (results.hasNext()) { 162 HelpEntry e = results.next(); 163 if (i >= max) { 164 break; 165 } 166 if (i++ < min) { 167 continue; 168 } 169 printEntries.add(e); 170 } 171 this.lastPage = max >= totalResults; 172 173 if (search == null) { 174 formatter.showAllResults(this, printEntries); 175 } else { 176 formatter.showSearchResults(this, printEntries); 177 } 178 179 } 180 181 public List<HelpEntry> getHelpEntries() { 182 return helpEntries; 183 } 184 185 public void setPerPage(int perPage) { 186 this.perPage = perPage; 187 } 188 189 public void setPage(int page) { 190 this.page = page; 191 } 192 193 public void setPage(int page, int perPage) { 194 this.setPage(page); 195 this.setPerPage(perPage); 196 } 197 198 public void setSearch(List<String> search) { 199 this.search = search; 200 getHelpEntries().forEach(this::updateSearchScore); 201 } 202 203 public CommandIssuer getIssuer() { 204 return issuer; 205 } 206 207 public String getCommandName() { 208 return commandName; 209 } 210 211 public String getCommandPrefix() { 212 return commandPrefix; 213 } 214 215 public int getPage() { 216 return page; 217 } 218 219 public int getPerPage() { 220 return perPage; 221 } 222 223 public List<String> getSearch() { 224 return search; 225 } 226 227 public HelpEntry getSelectedEntry() { 228 return selectedEntry; 229 } 230 231 public int getTotalResults() { 232 return totalResults; 233 } 234 235 public int getTotalPages() { 236 return totalPages; 237 } 238 239 public boolean isLastPage() { 240 return lastPage; 241 } 242}