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 subCommands.entries().forEach(e -> { 062 String key = e.getKey(); 063 if (key.equals(BaseCommand.DEFAULT) || key.equals(BaseCommand.CATCHUNKNOWN)) { 064 return; 065 } 066 067 RegisteredCommand regCommand = e.getValue(); 068 if (regCommand.hasPermission(issuer) && !seen.contains(regCommand)) { 069 this.helpEntries.add(new HelpEntry(this, regCommand)); 070 seen.add(regCommand); 071 } 072 }); 073 } 074 075 @UnstableAPI // Not sure on this one yet even when API becomes unstable 076 protected void updateSearchScore(HelpEntry help) { 077 if (this.search == null || this.search.isEmpty()) { 078 help.setSearchScore(1); 079 return; 080 } 081 final RegisteredCommand<?> cmd = help.getRegisteredCommand(); 082 083 int searchScore = 0; 084 for (String word : this.search) { 085 Pattern pattern = Pattern.compile(".*" + Pattern.quote(word) + ".*", Pattern.CASE_INSENSITIVE); 086 for (String subCmd : cmd.registeredSubcommands) { 087 Pattern subCmdPattern = Pattern.compile(".*" + Pattern.quote(subCmd) + ".*", Pattern.CASE_INSENSITIVE); 088 if (pattern.matcher(subCmd).matches()) { 089 searchScore += 3; 090 } else if (subCmdPattern.matcher(word).matches()) { 091 searchScore++; 092 } 093 } 094 095 096 if (pattern.matcher(help.getDescription()).matches()) { 097 searchScore += 2; 098 } 099 if (pattern.matcher(help.getParameterSyntax()).matches()) { 100 searchScore++; 101 } 102 if (help.getSearchTags() != null && pattern.matcher(help.getSearchTags()).matches()) { 103 searchScore += 2; 104 } 105 } 106 help.setSearchScore(searchScore); 107 } 108 109 public CommandManager getManager() { 110 return manager; 111 } 112 113 public boolean isExactMatch(String command) { 114 for (HelpEntry helpEntry : helpEntries) { 115 if (helpEntry.getCommand().endsWith(" " + command)) { 116 selectedEntry = helpEntry; 117 return true; 118 } 119 } 120 return false; 121 } 122 123 public void showHelp() { 124 showHelp(issuer); 125 } 126 127 public void showHelp(CommandIssuer issuer) { 128 if (selectedEntry != null) { 129 showDetailedHelp(selectedEntry, issuer); 130 return; 131 } 132 133 List<HelpEntry> helpEntries = getHelpEntries(); 134 Iterator<HelpEntry> results = helpEntries.stream() 135 .filter(HelpEntry::shouldShow) 136 .sorted(Comparator.comparingInt(helpEntry -> helpEntry.getSearchScore() * -1)).iterator(); 137 if (!results.hasNext()) { 138 issuer.sendMessage(MessageType.ERROR, MessageKeys.NO_COMMAND_MATCHED_SEARCH, "{search}", ACFUtil.join(this.search, " ")); 139 helpEntries = getHelpEntries(); 140 results = helpEntries.iterator(); 141 } 142 this.totalResults = helpEntries.size(); 143 int min = (this.page - 1) * this.perPage; // TODO: per page configurable? 144 int max = min + this.perPage; 145 this.totalPages = (int) Math.ceil((float) totalResults / (float) this.perPage); 146 int i = 0; 147 if (min >= totalResults) { 148 issuer.sendMessage(MessageType.HELP, MessageKeys.HELP_NO_RESULTS); 149 return; 150 } 151 152 List<HelpEntry> printEntries = new ArrayList<>(); 153 while (results.hasNext()) { 154 HelpEntry e = results.next(); 155 if (i >= max) { 156 break; 157 } 158 if (i++ < min) { 159 continue; 160 } 161 printEntries.add(e); 162 } 163 this.lastPage = !(min > 0 || results.hasNext()); 164 165 CommandHelpFormatter formatter = manager.getHelpFormatter(); 166 if (search == null) { 167 formatter.printHelpHeader(this, issuer); 168 } else { 169 formatter.printSearchHeader(this, issuer); 170 } 171 172 for (HelpEntry e : printEntries) { 173 if (search == null) { 174 formatter.printHelpCommand(this, issuer, e); 175 } else { 176 formatter.printSearchEntry(this, issuer, e); 177 } 178 } 179 180 181 if (search == null) { 182 formatter.printHelpFooter(this, issuer); 183 } else { 184 formatter.printSearchFooter(this, issuer); 185 } 186 } 187 188 public void showDetailedHelp(HelpEntry entry, CommandIssuer issuer) { 189 // header 190 CommandHelpFormatter formatter = manager.getHelpFormatter(); 191 formatter.printDetailedHelpHeader(this, issuer, entry); 192 193 // normal help line 194 formatter.printDetailedHelpCommand(this, issuer, entry); 195 196 // additionally detailed help for params 197 for (CommandParameter param : entry.getParameters()) { 198 String description = param.getDescription(); 199 if (description != null && !description.isEmpty()) { 200 formatter.printDetailedParameter(this, issuer, entry, param); 201 } 202 } 203 204 // footer 205 formatter.printDetailedHelpFooter(this, issuer, entry); 206 } 207 208 public List<HelpEntry> getHelpEntries() { 209 return helpEntries; 210 } 211 212 public void setPerPage(int perPage) { 213 this.perPage = perPage; 214 } 215 216 public void setPage(int page) { 217 this.page = page; 218 } 219 220 public void setPage(int page, int perPage) { 221 this.setPage(page); 222 this.setPerPage(perPage); 223 } 224 225 public void setSearch(List<String> search) { 226 this.search = search; 227 getHelpEntries().forEach(this::updateSearchScore); 228 } 229 230 public CommandIssuer getIssuer() { 231 return issuer; 232 } 233 234 public String getCommandName() { 235 return commandName; 236 } 237 238 public String getCommandPrefix() { 239 return commandPrefix; 240 } 241 242 public int getPage() { 243 return page; 244 } 245 246 public int getPerPage() { 247 return perPage; 248 } 249 250 public List<String> getSearch() { 251 return search; 252 } 253 254 public HelpEntry getSelectedEntry() { 255 return selectedEntry; 256 } 257 258 public int getTotalResults() { 259 return totalResults; 260 } 261 262 public int getTotalPages() { 263 return totalPages; 264 } 265 266 public boolean isLastPage() { 267 return lastPage; 268 } 269}