001 package org.bukkit.command.defaults;
002
003 import java.util.ArrayList;
004 import java.util.Arrays;
005 import java.util.HashMap;
006 import java.util.List;
007 import java.util.Map;
008 import java.util.Set;
009 import java.util.TreeSet;
010
011 import org.apache.commons.lang.ArrayUtils;
012 import org.apache.commons.lang.StringUtils;
013 import org.apache.commons.lang.Validate;
014 import org.apache.commons.lang.math.NumberUtils;
015 import org.bukkit.Bukkit;
016 import org.bukkit.ChatColor;
017 import org.bukkit.command.CommandSender;
018 import org.bukkit.command.ConsoleCommandSender;
019 import org.bukkit.help.HelpMap;
020 import org.bukkit.help.HelpTopic;
021 import org.bukkit.help.HelpTopicComparator;
022 import org.bukkit.help.IndexHelpTopic;
023 import org.bukkit.util.ChatPaginator;
024
025 import com.google.common.collect.ImmutableList;
026
027 public class HelpCommand extends VanillaCommand {
028 public HelpCommand() {
029 super("help");
030 this.description = "Shows the help menu";
031 this.usageMessage = "/help <pageNumber>\n/help <topic>\n/help <topic> <pageNumber>";
032 this.setAliases(Arrays.asList(new String[] { "?" }));
033 this.setPermission("bukkit.command.help");
034 }
035
036 @Override
037 public boolean execute(CommandSender sender, String currentAlias, String[] args) {
038 if (!testPermission(sender)) return true;
039
040 String command;
041 int pageNumber;
042 int pageHeight;
043 int pageWidth;
044
045 if (args.length == 0) {
046 command = "";
047 pageNumber = 1;
048 } else if (NumberUtils.isDigits(args[args.length - 1])) {
049 command = StringUtils.join(ArrayUtils.subarray(args, 0, args.length - 1), " ");
050 try {
051 pageNumber = NumberUtils.createInteger(args[args.length - 1]);
052 } catch (NumberFormatException exception) {
053 pageNumber = 1;
054 }
055 if (pageNumber <= 0) {
056 pageNumber = 1;
057 }
058 } else {
059 command = StringUtils.join(args, " ");
060 pageNumber = 1;
061 }
062
063 if (sender instanceof ConsoleCommandSender) {
064 pageHeight = ChatPaginator.UNBOUNDED_PAGE_HEIGHT;
065 pageWidth = ChatPaginator.UNBOUNDED_PAGE_WIDTH;
066 } else {
067 pageHeight = ChatPaginator.CLOSED_CHAT_PAGE_HEIGHT - 1;
068 pageWidth = ChatPaginator.GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH;
069 }
070
071 HelpMap helpMap = Bukkit.getServer().getHelpMap();
072 HelpTopic topic = helpMap.getHelpTopic(command);
073
074 if (topic == null) {
075 topic = helpMap.getHelpTopic("/" + command);
076 }
077
078 if (topic == null) {
079 topic = findPossibleMatches(command);
080 }
081
082 if (topic == null || !topic.canSee(sender)) {
083 sender.sendMessage(ChatColor.RED + "No help for " + command);
084 return true;
085 }
086
087 ChatPaginator.ChatPage page = ChatPaginator.paginate(topic.getFullText(sender), pageNumber, pageWidth, pageHeight);
088
089 StringBuilder header = new StringBuilder();
090 header.append(ChatColor.YELLOW);
091 header.append("--------- ");
092 header.append(ChatColor.WHITE);
093 header.append("Help: ");
094 header.append(topic.getName());
095 header.append(" ");
096 if (page.getTotalPages() > 1) {
097 header.append("(");
098 header.append(page.getPageNumber());
099 header.append("/");
100 header.append(page.getTotalPages());
101 header.append(") ");
102 }
103 header.append(ChatColor.YELLOW);
104 for (int i = header.length(); i < ChatPaginator.GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH; i++) {
105 header.append("-");
106 }
107 sender.sendMessage(header.toString());
108
109 sender.sendMessage(page.getLines());
110
111 return true;
112 }
113
114 @Override
115 public List<String> tabComplete(CommandSender sender, String alias, String[] args) {
116 Validate.notNull(sender, "Sender cannot be null");
117 Validate.notNull(args, "Arguments cannot be null");
118 Validate.notNull(alias, "Alias cannot be null");
119
120 if (args.length == 1) {
121 List<String> matchedTopics = new ArrayList<String>();
122 String searchString = args[0];
123 for (HelpTopic topic : Bukkit.getServer().getHelpMap().getHelpTopics()) {
124 String trimmedTopic = topic.getName().startsWith("/") ? topic.getName().substring(1) : topic.getName();
125
126 if (trimmedTopic.startsWith(searchString)) {
127 matchedTopics.add(trimmedTopic);
128 }
129 }
130 return matchedTopics;
131 }
132 return ImmutableList.of();
133 }
134
135 protected HelpTopic findPossibleMatches(String searchString) {
136 int maxDistance = (searchString.length() / 5) + 3;
137 Set<HelpTopic> possibleMatches = new TreeSet<HelpTopic>(HelpTopicComparator.helpTopicComparatorInstance());
138
139 if (searchString.startsWith("/")) {
140 searchString = searchString.substring(1);
141 }
142
143 for (HelpTopic topic : Bukkit.getServer().getHelpMap().getHelpTopics()) {
144 String trimmedTopic = topic.getName().startsWith("/") ? topic.getName().substring(1) : topic.getName();
145
146 if (trimmedTopic.length() < searchString.length()) {
147 continue;
148 }
149
150 if (Character.toLowerCase(trimmedTopic.charAt(0)) != Character.toLowerCase(searchString.charAt(0))) {
151 continue;
152 }
153
154 if (damerauLevenshteinDistance(searchString, trimmedTopic.substring(0, searchString.length())) < maxDistance) {
155 possibleMatches.add(topic);
156 }
157 }
158
159 if (possibleMatches.size() > 0) {
160 return new IndexHelpTopic("Search", null, null, possibleMatches, "Search for: " + searchString);
161 } else {
162 return null;
163 }
164 }
165
166 /**
167 * Computes the Dameraur-Levenshtein Distance between two strings. Adapted
168 * from the algorithm at <a href="http://en.wikipedia.org/wiki/Damerau?Levenshtein_distance">Wikipedia: Damerau?Levenshtein distance</a>
169 *
170 * @param s1 The first string being compared.
171 * @param s2 The second string being compared.
172 * @return The number of substitutions, deletions, insertions, and
173 * transpositions required to get from s1 to s2.
174 */
175 protected static int damerauLevenshteinDistance(String s1, String s2) {
176 if (s1 == null && s2 == null) {
177 return 0;
178 }
179 if (s1 != null && s2 == null) {
180 return s1.length();
181 }
182 if (s1 == null && s2 != null) {
183 return s2.length();
184 }
185
186 int s1Len = s1.length();
187 int s2Len = s2.length();
188 int[][] H = new int[s1Len + 2][s2Len + 2];
189
190 int INF = s1Len + s2Len;
191 H[0][0] = INF;
192 for (int i = 0; i <= s1Len; i++) {
193 H[i + 1][1] = i;
194 H[i + 1][0] = INF;
195 }
196 for (int j = 0; j <= s2Len; j++) {
197 H[1][j + 1] = j;
198 H[0][j + 1] = INF;
199 }
200
201 Map<Character, Integer> sd = new HashMap<Character, Integer>();
202 for (char Letter : (s1 + s2).toCharArray()) {
203 if (!sd.containsKey(Letter)) {
204 sd.put(Letter, 0);
205 }
206 }
207
208 for (int i = 1; i <= s1Len; i++) {
209 int DB = 0;
210 for (int j = 1; j <= s2Len; j++) {
211 int i1 = sd.get(s2.charAt(j - 1));
212 int j1 = DB;
213
214 if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
215 H[i + 1][j + 1] = H[i][j];
216 DB = j;
217 } else {
218 H[i + 1][j + 1] = Math.min(H[i][j], Math.min(H[i + 1][j], H[i][j + 1])) + 1;
219 }
220
221 H[i + 1][j + 1] = Math.min(H[i + 1][j + 1], H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1));
222 }
223 sd.put(s1.charAt(i - 1), i);
224 }
225
226 return H[s1Len + 1][s2Len + 1];
227 }
228 }