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    }