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