1 /* 2 * Copyright (c) 2008-2019 Emmanuel Dupuy. 3 * This project is distributed under the GPLv3 license. 4 * This is a Copyleft license that gives the user the right to use, 5 * copy and modify the code freely for non-commercial purposes. 6 */ 7 8 package org.jd.gui.controller; 9 10 import org.jd.gui.api.API; 11 import org.jd.gui.api.feature.IndexesChangeListener; 12 import org.jd.gui.api.model.Container; 13 import org.jd.gui.api.model.Indexes; 14 import org.jd.gui.util.exception.ExceptionUtil; 15 import org.jd.gui.util.net.UriUtil; 16 import org.jd.gui.view.OpenTypeView; 17 18 import javax.swing.*; 19 import java.awt.*; 20 import java.net.URI; 21 import java.util.*; 22 import java.util.concurrent.Future; 23 import java.util.concurrent.ScheduledExecutorService; 24 import java.util.function.Consumer; 25 import java.util.regex.Pattern; 26 27 public class OpenTypeController implements IndexesChangeListener { 28 protected static final int CACHE_MAX_ENTRIES = 5*20; 29 30 protected API api; 31 protected ScheduledExecutorService executor; 32 protected Collection<Future<Indexes>> collectionOfFutureIndexes; 33 protected Consumer<URI> openCallback; 34 35 protected JFrame mainFrame; 36 protected OpenTypeView openTypeView; 37 protected SelectLocationController selectLocationController; 38 39 protected long indexesHashCode = 0L; 40 protected Map<String, Map<String, Collection>> cache; 41 OpenTypeController(API api, ScheduledExecutorService executor, JFrame mainFrame)42 public OpenTypeController(API api, ScheduledExecutorService executor, JFrame mainFrame) { 43 this.api = api; 44 this.executor = executor; 45 this.mainFrame = mainFrame; 46 // Create UI 47 openTypeView = new OpenTypeView(api, mainFrame, this::updateList, this::onTypeSelected); 48 selectLocationController = new SelectLocationController(api, mainFrame); 49 // Create result cache 50 cache = new LinkedHashMap<String, Map<String, Collection>>(CACHE_MAX_ENTRIES*3/2, 0.7f, true) { 51 @Override 52 protected boolean removeEldestEntry(Map.Entry<String, Map<String, Collection>> eldest) { 53 return size() > CACHE_MAX_ENTRIES; 54 } 55 }; 56 } 57 show(Collection<Future<Indexes>> collectionOfFutureIndexes, Consumer<URI> openCallback)58 public void show(Collection<Future<Indexes>> collectionOfFutureIndexes, Consumer<URI> openCallback) { 59 // Init attributes 60 this.collectionOfFutureIndexes = collectionOfFutureIndexes; 61 this.openCallback = openCallback; 62 // Refresh view 63 long hashCode = collectionOfFutureIndexes.hashCode(); 64 if (hashCode != indexesHashCode) { 65 // List of indexes has changed -> Refresh result list 66 updateList(openTypeView.getPattern()); 67 indexesHashCode = hashCode; 68 } 69 // Show 70 openTypeView.show(); 71 } 72 73 @SuppressWarnings("unchecked") updateList(String pattern)74 protected void updateList(String pattern) { 75 int patternLength = pattern.length(); 76 77 if (patternLength == 0) { 78 // Display 79 openTypeView.updateList(Collections.emptyMap()); 80 } else { 81 executor.execute(() -> { 82 // Waiting the end of indexation... 83 openTypeView.showWaitCursor(); 84 85 Pattern regExpPattern = createRegExpPattern(pattern); 86 Map<String, Collection<Container.Entry>> result = new HashMap<>(); 87 88 try { 89 for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) { 90 if (futureIndexes.isDone()) { 91 Indexes indexes = futureIndexes.get(); 92 String key = String.valueOf(indexes.hashCode()) + "***" + pattern; 93 Map<String, Collection> matchingEntries = cache.get(key); 94 95 if (matchingEntries != null) { 96 // Merge 'result' and 'matchingEntries' 97 for (Map.Entry<String, Collection> mapEntry : matchingEntries.entrySet()) { 98 Collection<Container.Entry> collection = result.get(mapEntry.getKey()); 99 if (collection == null) { 100 result.put(mapEntry.getKey(), collection = new HashSet<>()); 101 } 102 collection.addAll(mapEntry.getValue()); 103 } 104 } else { 105 // Waiting the end of indexation... 106 Map<String, Collection> index = indexes.getIndex("typeDeclarations"); 107 108 if ((index != null) && !index.isEmpty()) { 109 matchingEntries = new HashMap<>(); 110 111 // Filter 112 if (patternLength == 1) { 113 match(pattern.charAt(0), index, matchingEntries); 114 } else { 115 String lastKey = key.substring(0, patternLength - 1); 116 Map<String, Collection> lastResult = cache.get(lastKey); 117 118 if (lastResult != null) { 119 match(regExpPattern, lastResult, matchingEntries); 120 } else { 121 match(regExpPattern, index, matchingEntries); 122 } 123 } 124 125 // Store 'matchingEntries' 126 cache.put(key, matchingEntries); 127 128 // Merge 'result' and 'matchingEntries' 129 for (Map.Entry<String, Collection> mapEntry : matchingEntries.entrySet()) { 130 Collection<Container.Entry> collection = result.get(mapEntry.getKey()); 131 if (collection == null) { 132 result.put(mapEntry.getKey(), collection = new HashSet<>()); 133 } 134 collection.addAll(mapEntry.getValue()); 135 } 136 } 137 } 138 } 139 } 140 } catch (Exception e) { 141 assert ExceptionUtil.printStackTrace(e); 142 } 143 144 SwingUtilities.invokeLater(() -> { 145 openTypeView.hideWaitCursor(); 146 // Display 147 openTypeView.updateList(result); 148 }); 149 }); 150 } 151 } 152 153 @SuppressWarnings("unchecked") match(char c, Map<String, Collection> index, Map<String, Collection> result)154 protected static void match(char c, Map<String, Collection> index, Map<String, Collection> result) { 155 // Filter 156 if (Character.isLowerCase(c)) { 157 char upperCase = Character.toUpperCase(c); 158 159 for (Map.Entry<String, Collection> mapEntry : index.entrySet()) { 160 String typeName = mapEntry.getKey(); 161 Collection<Container.Entry> entries = mapEntry.getValue(); 162 // Search last package separator 163 int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1; 164 int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1; 165 int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex); 166 167 if (lastIndex < typeName.length()) { 168 char first = typeName.charAt(lastIndex); 169 170 if ((first == c) || (first == upperCase)) { 171 add(result, typeName, entries); 172 } 173 } 174 } 175 } else { 176 for (Map.Entry<String, Collection> mapEntry : index.entrySet()) { 177 String typeName = mapEntry.getKey(); 178 Collection<Container.Entry> entries = mapEntry.getValue(); 179 // Search last package separator 180 int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1; 181 int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1; 182 int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex); 183 184 if ((lastIndex < typeName.length()) && (typeName.charAt(lastIndex) == c)) { 185 add(result, typeName, entries); 186 } 187 } 188 } 189 } 190 191 /** 192 * Create a regular expression to match package, type and inner type name. 193 * 194 * Rules: 195 * '*' matches 0 ou N characters 196 * '?' matches 1 character 197 * lower case matches insensitive case 198 * upper case matches upper case 199 */ createRegExpPattern(String pattern)200 protected static Pattern createRegExpPattern(String pattern) { 201 // Create regular expression 202 int patternLength = pattern.length(); 203 StringBuilder sbPattern = new StringBuilder(patternLength * 4); 204 205 for (int i=0; i<patternLength; i++) { 206 char c = pattern.charAt(i); 207 208 if (Character.isUpperCase(c)) { 209 if (i > 1) { 210 sbPattern.append(".*"); 211 } 212 sbPattern.append(c); 213 } else if (Character.isLowerCase(c)) { 214 sbPattern.append('[').append(c).append(Character.toUpperCase(c)).append(']'); 215 } else if (c == '*') { 216 sbPattern.append(".*"); 217 } else if (c == '?') { 218 sbPattern.append("."); 219 } else { 220 sbPattern.append(c); 221 } 222 } 223 224 sbPattern.append(".*"); 225 226 return Pattern.compile(sbPattern.toString()); 227 } 228 229 @SuppressWarnings("unchecked") match(Pattern regExpPattern, Map<String, Collection> index, Map<String, Collection> result)230 protected static void match(Pattern regExpPattern, Map<String, Collection> index, Map<String, Collection> result) { 231 for (Map.Entry<String, Collection> mapEntry : index.entrySet()) { 232 String typeName = mapEntry.getKey(); 233 Collection<Container.Entry> entries = mapEntry.getValue(); 234 // Search last package separator 235 int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1; 236 int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1; 237 int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex); 238 239 if (regExpPattern.matcher(typeName.substring(lastIndex)).matches()) { 240 add(result, typeName, entries); 241 } 242 } 243 } 244 245 @SuppressWarnings("unchecked") add(Map<String, Collection> map, String key, Collection value)246 protected static void add(Map<String, Collection> map, String key, Collection value) { 247 Collection<Container.Entry> collection = map.get(key); 248 249 if (collection == null) { 250 map.put(key, collection = new HashSet<>()); 251 } 252 253 collection.addAll(value); 254 } 255 onTypeSelected(Point leftBottom, Collection<Container.Entry> entries, String typeName)256 protected void onTypeSelected(Point leftBottom, Collection<Container.Entry> entries, String typeName) { 257 if (entries.size() == 1) { 258 // Open the single entry uri 259 openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entries.iterator().next(), null, typeName)); 260 } else { 261 // Multiple entries -> Open a "Select location" popup 262 selectLocationController.show( 263 new Point(leftBottom.x+(16+2), leftBottom.y+2), 264 entries, 265 (entry) -> openCallback.accept(UriUtil.createURI(api, collectionOfFutureIndexes, entry, null, typeName)), // entry selected callback 266 () -> openTypeView.focus()); // popup close callback 267 } 268 } 269 270 // --- IndexesChangeListener --- // indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes)271 public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) { 272 if (openTypeView.isVisible()) { 273 // Update the list of containers 274 this.collectionOfFutureIndexes = collectionOfFutureIndexes; 275 // And refresh 276 updateList(openTypeView.getPattern()); 277 } 278 } 279 } 280