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 package org.jd.gui.view.component;
8 
9 import org.fife.ui.rsyntaxtextarea.DocumentRange;
10 import org.fife.ui.rtextarea.Marker;
11 import org.jd.gui.api.API;
12 import org.jd.gui.api.feature.FocusedTypeGettable;
13 import org.jd.gui.api.feature.IndexesChangeListener;
14 import org.jd.gui.api.feature.UriGettable;
15 import org.jd.gui.api.feature.UriOpenable;
16 import org.jd.gui.api.model.Container;
17 import org.jd.gui.api.model.Indexes;
18 import org.jd.gui.api.model.Type;
19 import org.jd.gui.util.exception.ExceptionUtil;
20 import org.jd.gui.util.index.IndexesUtil;
21 import org.jd.gui.util.matcher.DescriptorMatcher;
22 
23 import java.awt.*;
24 import java.net.URI;
25 import java.net.URISyntaxException;
26 import java.util.*;
27 import java.util.List;
28 import java.util.concurrent.Future;
29 import java.util.function.BiFunction;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 
33 public abstract class TypePage extends CustomLineNumbersPage implements UriGettable, UriOpenable, IndexesChangeListener, FocusedTypeGettable {
34     protected API api;
35     protected Container.Entry entry;
36     protected Collection<Future<Indexes>> collectionOfFutureIndexes = Collections.emptyList();
37 
38     protected HashMap<String, DeclarationData> declarations = new HashMap<>();
39     protected TreeMap<Integer, DeclarationData> typeDeclarations = new TreeMap<>();
40     protected ArrayList<ReferenceData> references = new ArrayList<>();
41     protected ArrayList<StringData> strings = new ArrayList<>();
42 
TypePage(API api, Container.Entry entry)43     public TypePage(API api, Container.Entry entry) {
44         // Init attributes
45         this.api = api;
46         this.entry = entry;
47     }
48 
49     @Override
isHyperlinkEnabled(HyperlinkData hyperlinkData)50     protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) {
51         return ((HyperlinkReferenceData)hyperlinkData).reference.enabled;
52     }
53 
54     @Override
55     @SuppressWarnings("unchecked")
openHyperlink(int x, int y, HyperlinkData hyperlinkData)56     protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) {
57         HyperlinkReferenceData hyperlinkReferenceData = (HyperlinkReferenceData)hyperlinkData;
58 
59         if (hyperlinkReferenceData.reference.enabled) {
60             try {
61                 // Save current position in history
62                 Point location = textArea.getLocationOnScreen();
63                 int offset = textArea.viewToModel(new Point(x - location.x, y - location.y));
64                 URI uri = entry.getUri();
65                 api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null));
66 
67                 // Open link
68                 ReferenceData reference = hyperlinkReferenceData.reference;
69                 String typeName = reference.typeName;
70                 List<Container.Entry> entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, typeName);
71                 String fragment = typeName;
72 
73                 if (reference.name != null) {
74                     fragment += '-' + reference.name;
75                 }
76                 if (reference.descriptor != null) {
77                     fragment += '-' + reference.descriptor;
78                 }
79 
80                 if (entries.contains(entry)) {
81                     api.openURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), fragment));
82                 } else {
83                     String rootUri = entry.getContainer().getRoot().getUri().toString();
84                     ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
85 
86                     for (Container.Entry entry : entries) {
87                         if (entry.getUri().toString().startsWith(rootUri)) {
88                             sameContainerEntries.add(entry);
89                         }
90                     }
91 
92                     if (sameContainerEntries.size() > 0) {
93                         api.openURI(x, y, sameContainerEntries, null, fragment);
94                     } else if (entries.size() > 0) {
95                         api.openURI(x, y, entries, null, fragment);
96                     }
97                 }
98             } catch (URISyntaxException e) {
99                 assert ExceptionUtil.printStackTrace(e);
100             }
101         }
102     }
103 
104     // --- UriGettable --- //
getUri()105     @Override public URI getUri() { return entry.getUri(); }
106 
107     // --- UriOpenable --- //
108     /**
109      * @param uri for URI format, @see jd.gui.api.feature.UriOpenable
110      */
111     @Override
openUri(URI uri)112     public boolean openUri(URI uri) {
113         ArrayList<DocumentRange> ranges = new ArrayList<>();
114         String fragment = uri.getFragment();
115         String query = uri.getQuery();
116 
117         Marker.clearMarkAllHighlights(textArea);
118 
119         if (fragment != null) {
120             matchFragmentAndAddDocumentRange(fragment, declarations, ranges);
121         }
122 
123         if (query != null) {
124             Map<String, String> parameters = parseQuery(query);
125 
126             if (parameters.containsKey("lineNumber")) {
127                 String lineNumber = parameters.get("lineNumber");
128 
129                 try {
130                     goToLineNumber(Integer.parseInt(lineNumber));
131                     return true;
132                 } catch (NumberFormatException e) {
133                     assert ExceptionUtil.printStackTrace(e);
134                 }
135             } else if (parameters.containsKey("position")) {
136                 String position = parameters.get("position");
137 
138                 try {
139                     int pos = Integer.parseInt(position);
140                     if (textArea.getDocument().getLength() > pos) {
141                         ranges.add(new DocumentRange(pos, pos));
142                     }
143                 } catch (NumberFormatException e) {
144                     assert ExceptionUtil.printStackTrace(e);
145                 }
146             } else {
147                 matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges);
148             }
149         }
150 
151         if ((ranges != null) && !ranges.isEmpty()) {
152             textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
153             Marker.markAll(textArea, ranges);
154             ranges.sort(null);
155             setCaretPositionAndCenter(ranges.get(0));
156         }
157 
158         return true;
159     }
160 
matchFragmentAndAddDocumentRange(String fragment, HashMap<String, DeclarationData> declarations, List<DocumentRange> ranges)161     public static void matchFragmentAndAddDocumentRange(String fragment, HashMap<String, DeclarationData> declarations, List<DocumentRange> ranges) {
162         if ((fragment.indexOf('?') != -1) || (fragment.indexOf('*') != -1)) {
163             // Unknown type and/or descriptor ==> Select all and scroll to the first one
164             int lastDash = fragment.lastIndexOf('-');
165 
166             if (lastDash == -1) {
167                 // Search types
168                 String slashAndTypeName = fragment.substring(1);
169                 String typeName = fragment.substring(2);
170 
171                 for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
172                     if (entry.getKey().endsWith(slashAndTypeName) || entry.getKey().equals(typeName)) {
173                         ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
174                     }
175                 }
176             } else {
177                 String prefix = fragment.substring(0, lastDash+1);
178                 String suffix = fragment.substring(lastDash+1);
179                 BiFunction<String, String, Boolean> matchDescriptors;
180 
181                 if (suffix.charAt(0) == '(') {
182                     matchDescriptors = DescriptorMatcher::matchMethodDescriptors;
183                 } else {
184                     matchDescriptors = DescriptorMatcher::matchFieldDescriptors;
185                 }
186 
187                 if (fragment.charAt(0) == '*') {
188                     // Unknown type
189                     String slashAndTypeNameAndName = prefix.substring(1);
190                     String typeNameAndName = prefix.substring(2);
191 
192                     for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
193                         String key = entry.getKey();
194                         if ((key.indexOf(slashAndTypeNameAndName) != -1) || (key.startsWith(typeNameAndName))) {
195                             int index = key.lastIndexOf('-') + 1;
196                             if (matchDescriptors.apply(suffix, key.substring(index))) {
197                                 ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
198                             }
199                         }
200                     }
201                 } else {
202                     // Known type
203                     for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
204                         String key = entry.getKey();
205                         if (key.startsWith(prefix)) {
206                             int index = key.lastIndexOf('-') + 1;
207                             if (matchDescriptors.apply(suffix, key.substring(index))) {
208                                 ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition));
209                             }
210                         }
211                     }
212                 }
213             }
214         } else {
215             // Known type and descriptor ==> Search and high light item
216             DeclarationData data = declarations.get(fragment);
217             if (data != null) {
218                 ranges.add(new DocumentRange(data.startPosition, data.endPosition));
219             } else if (fragment.endsWith("-<clinit>-()V")) {
220                 // 'static' bloc not found ==> Select type declaration
221                 String typeName = fragment.substring(0, fragment.indexOf('-'));
222                 data = declarations.get(typeName);
223                 ranges.add(new DocumentRange(data.startPosition, data.endPosition));
224             }
225         }
226     }
227 
matchQueryAndAddDocumentRange( Map<String, String> parameters, HashMap<String, DeclarationData> declarations, TreeMap<Integer, HyperlinkData> hyperlinks, ArrayList<StringData> strings, List<DocumentRange> ranges)228     public static void matchQueryAndAddDocumentRange(
229             Map<String, String> parameters,
230             HashMap<String, DeclarationData> declarations, TreeMap<Integer, HyperlinkData> hyperlinks, ArrayList<StringData> strings,
231             List<DocumentRange> ranges) {
232 
233         String highlightFlags = parameters.get("highlightFlags");
234         String highlightPattern = parameters.get("highlightPattern");
235 
236         if ((highlightFlags != null) && (highlightPattern != null)) {
237             String highlightScope = parameters.get("highlightScope");
238             String regexp = createRegExp(highlightPattern);
239             Pattern pattern = Pattern.compile(regexp + ".*");
240 
241             if (highlightFlags.indexOf('s') != -1) {
242                 // Highlight strings
243                 Pattern patternForString = Pattern.compile(regexp);
244 
245                 for (StringData data : strings) {
246                     if (matchScope(highlightScope, data.owner)) {
247                         Matcher matcher = patternForString.matcher(data.text);
248                         int offset = data.startPosition;
249 
250                         while(matcher.find()) {
251                             ranges.add(new DocumentRange(offset + matcher.start(), offset + matcher.end()));
252                         }
253                     }
254                 }
255             }
256 
257             boolean t = (highlightFlags.indexOf('t') != -1); // Highlight types
258             boolean f = (highlightFlags.indexOf('f') != -1); // Highlight fields
259             boolean m = (highlightFlags.indexOf('m') != -1); // Highlight methods
260             boolean c = (highlightFlags.indexOf('c') != -1); // Highlight constructors
261 
262             if (highlightFlags.indexOf('d') != -1) {
263                 // Highlight declarations
264                 for (Map.Entry<String, DeclarationData> entry : declarations.entrySet()) {
265                     DeclarationData declaration = entry.getValue();
266 
267                     if (matchScope(highlightScope, declaration.typeName)) {
268                         if ((t && declaration.isAType()) || (c && declaration.isAConstructor())) {
269                             matchAndAddDocumentRange(pattern, getMostInnerTypeName(declaration.typeName), declaration.startPosition, declaration.endPosition, ranges);
270                         }
271                         if ((f && declaration.isAField()) || (m && declaration.isAMethod())) {
272                             matchAndAddDocumentRange(pattern, declaration.name, declaration.startPosition, declaration.endPosition, ranges);
273                         }
274                     }
275                 }
276             }
277 
278             if (highlightFlags.indexOf('r') != -1) {
279                 // Highlight references
280                 for (Map.Entry<Integer, HyperlinkData> entry : hyperlinks.entrySet()) {
281                     HyperlinkData hyperlink = entry.getValue();
282                     ReferenceData reference = ((HyperlinkReferenceData)hyperlink).reference;
283 
284                     if (matchScope(highlightScope, reference.owner)) {
285                         if ((t && reference.isAType()) || (c && reference.isAConstructor())) {
286                             matchAndAddDocumentRange(pattern, getMostInnerTypeName(reference.typeName), hyperlink.startPosition, hyperlink.endPosition, ranges);
287                         }
288                         if ((f && reference.isAField()) || (m && reference.isAMethod())) {
289                             matchAndAddDocumentRange(pattern, reference.name, hyperlink.startPosition, hyperlink.endPosition, ranges);
290                         }
291                     }
292                 }
293             }
294         }
295     }
296 
matchScope(String scope, String type)297     public static boolean matchScope(String scope, String type) {
298         if ((scope == null) || scope.isEmpty())
299             return true;
300         if (scope.charAt(0) == '*')
301             return type.endsWith(scope.substring(1)) || type.equals(scope.substring(2));
302         return type.equals(scope);
303     }
304 
matchAndAddDocumentRange(Pattern pattern, String text, int start, int end, List<DocumentRange> ranges)305     public static void matchAndAddDocumentRange(Pattern pattern, String text, int start, int end, List<DocumentRange> ranges) {
306         if (pattern.matcher(text).matches()) {
307             ranges.add(new DocumentRange(start, end));
308         }
309     }
310 
getMostInnerTypeName(String typeName)311     public static String getMostInnerTypeName(String typeName) {
312         int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1;
313         int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1;
314         int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex);
315         return typeName.substring(lastIndex);
316     }
317 
318     // --- FocusedTypeGettable --- //
getFocusedTypeName()319     @Override public String getFocusedTypeName() {
320         Map.Entry<Integer, DeclarationData> entry = typeDeclarations.floorEntry(textArea.getCaretPosition());
321 
322         if (entry != null) {
323             DeclarationData data = entry.getValue();
324             if (data != null) {
325                 return data.typeName;
326             }
327         }
328 
329         return null;
330     }
331 
getEntry()332     @Override public Container.Entry getEntry() { return entry; }
333 
334     // --- IndexesChangeListener --- //
335     @Override
indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes)336     public void indexesChanged(Collection<Future<Indexes>> collectionOfFutureIndexes) {
337         // Update the list of containers
338         this.collectionOfFutureIndexes = collectionOfFutureIndexes;
339         // Refresh links
340         boolean refresh = false;
341 
342         for (ReferenceData reference : references) {
343             String typeName = reference.typeName;
344             boolean enabled;
345 
346             if (reference.name == null) {
347                 enabled = false;
348 
349                 try {
350                     for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
351                         if (futureIndexes.isDone()) {
352                             Map<String, Collection> index = futureIndexes.get().getIndex("typeDeclarations");
353                             if ((index != null) && (index.get(typeName) != null)) {
354                                 enabled = true;
355                                 break;
356                             }
357                         }
358                     }
359                 } catch (Exception e) {
360                     assert ExceptionUtil.printStackTrace(e);
361                 }
362             } else {
363                 try {
364                     // Recursive search
365                     typeName = searchTypeHavingMember(typeName, reference.name, reference.descriptor, entry);
366                     if (typeName != null) {
367                         // Replace type with the real type having the referenced member
368                         reference.typeName = typeName;
369                         enabled = true;
370                     } else {
371                         enabled = false;
372                     }
373                 } catch (Error e) {
374                     // Catch StackOverflowError or OutOfMemoryError
375                     assert ExceptionUtil.printStackTrace(e);
376                     enabled = false;
377                 }
378             }
379 
380             if (reference.enabled != enabled) {
381                 reference.enabled = enabled;
382                 refresh = true;
383             }
384         }
385 
386         if (refresh) {
387             textArea.repaint();
388         }
389     }
390 
391     @SuppressWarnings("unchecked")
searchTypeHavingMember(String typeName, String name, String descriptor, Container.Entry entry)392     protected String searchTypeHavingMember(String typeName, String name, String descriptor, Container.Entry entry) {
393         ArrayList<Container.Entry> entries = new ArrayList<>();
394 
395         try {
396             for (Future<Indexes> futureIndexes : collectionOfFutureIndexes) {
397                 if (futureIndexes.isDone()) {
398                     Map<String, Collection> index = futureIndexes.get().getIndex("typeDeclarations");
399                     if (index != null) {
400                         Collection<Container.Entry> collection = index.get(typeName);
401                         if (collection != null) {
402                             entries.addAll(collection);
403                         }
404                     }
405                 }
406             }
407         } catch (Exception e) {
408             assert ExceptionUtil.printStackTrace(e);
409         }
410 
411         String rootUri = entry.getContainer().getRoot().getUri().toString();
412         ArrayList<Container.Entry> sameContainerEntries = new ArrayList<>();
413 
414         for (Container.Entry e : entries) {
415             if (e.getUri().toString().startsWith(rootUri)) {
416                 sameContainerEntries.add(e);
417             }
418         }
419 
420         if (sameContainerEntries.size() > 0) {
421             return searchTypeHavingMember(typeName, name, descriptor, sameContainerEntries);
422         } else {
423             return searchTypeHavingMember(typeName, name, descriptor, entries);
424         }
425     }
426 
searchTypeHavingMember(String typeName, String name, String descriptor, List<Container.Entry> entries)427     protected String searchTypeHavingMember(String typeName, String name, String descriptor, List<Container.Entry> entries) {
428         for (Container.Entry entry : entries) {
429             Type type = api.getTypeFactory(entry).make(api, entry, typeName);
430 
431             if (type != null) {
432                 if (descriptor.indexOf('(') == -1) {
433                     // Search a field
434                     for (Type.Field field : type.getFields()) {
435                         if (field.getName().equals(name) && DescriptorMatcher.matchFieldDescriptors(field.getDescriptor(), descriptor)) {
436                             // Field found
437                             return typeName;
438                         }
439                     }
440                 } else {
441                     // Search a method
442                     for (Type.Method method : type.getMethods()) {
443                         if (method.getName().equals(name) && DescriptorMatcher.matchMethodDescriptors(method.getDescriptor(), descriptor)) {
444                             // Method found
445                             return typeName;
446                         }
447                     }
448                 }
449 
450                 // Not found -> Search in super type
451                 String typeOwnerName = searchTypeHavingMember(type.getSuperName(), name, descriptor, entry);
452                 if (typeOwnerName != null) {
453                     return typeOwnerName;
454                 }
455             }
456         }
457 
458         return null;
459     }
460 
461     public static class StringData {
462         int startPosition;
463         int endPosition;
464         String text;
465         String owner;
466 
StringData(int startPosition, int length, String text, String owner)467         public StringData(int startPosition, int length, String text, String owner) {
468             this.startPosition = startPosition;
469             this.endPosition = startPosition + length;
470             this.text = text;
471             this.owner = owner;
472         }
473     }
474 
475     public static class DeclarationData {
476         int startPosition;
477         int endPosition;
478         String typeName;
479         /**
480          * Field or method name or null for type
481          */
482         String name;
483         String descriptor;
484 
DeclarationData(int startPosition, int length, String typeName, String name, String descriptor)485         public DeclarationData(int startPosition, int length, String typeName, String name, String descriptor) {
486             this.startPosition = startPosition;
487             this.endPosition = startPosition + length;
488             this.typeName = typeName;
489             this.name = name;
490             this.descriptor = descriptor;
491         }
492 
isAType()493         public boolean isAType() { return name == null; }
isAField()494         public boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; }
isAMethod()495         public boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; }
isAConstructor()496         public boolean isAConstructor() { return "<init>".equals(name); }
497     }
498 
499     public static class HyperlinkReferenceData extends HyperlinkData {
500         public ReferenceData reference;
501 
HyperlinkReferenceData(int startPosition, int length, ReferenceData reference)502         public HyperlinkReferenceData(int startPosition, int length, ReferenceData reference) {
503             super(startPosition, startPosition+length);
504             this.reference = reference;
505         }
506     }
507 
508     protected static class ReferenceData {
509         public String typeName;
510         /**
511          * Field or method name or null for type
512          */
513         public String name;
514         /**
515          * Field or method descriptor or null for type
516          */
517         public String descriptor;
518         /**
519          * Internal type name containing reference or null for "import" statement.
520          * Used to high light items matching with URI like "file://dir1/dir2/file?highlightPattern=hello&highlightFlags=drtcmfs&highlightScope=type".
521          */
522         public String owner;
523         /**
524          * "Enabled" flag for link of reference
525          */
526         public boolean enabled = false;
527 
ReferenceData(String typeName, String name, String descriptor, String owner)528         public ReferenceData(String typeName, String name, String descriptor, String owner) {
529             this.typeName = typeName;
530             this.name = name;
531             this.descriptor = descriptor;
532             this.owner = owner;
533         }
534 
isAType()535         boolean isAType() { return name == null; }
isAField()536         boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; }
isAMethod()537         boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; }
isAConstructor()538         boolean isAConstructor() { return "<init>".equals(name); }
539     }
540 }
541