/* * Copyright (c) 2008-2019 Emmanuel Dupuy. * This project is distributed under the GPLv3 license. * This is a Copyleft license that gives the user the right to use, * copy and modify the code freely for non-commercial purposes. */ package org.jd.gui.view.component; import org.fife.ui.rsyntaxtextarea.DocumentRange; import org.fife.ui.rtextarea.Marker; import org.jd.gui.api.API; import org.jd.gui.api.feature.FocusedTypeGettable; import org.jd.gui.api.feature.IndexesChangeListener; import org.jd.gui.api.feature.UriGettable; import org.jd.gui.api.feature.UriOpenable; import org.jd.gui.api.model.Container; import org.jd.gui.api.model.Indexes; import org.jd.gui.api.model.Type; import org.jd.gui.util.exception.ExceptionUtil; import org.jd.gui.util.index.IndexesUtil; import org.jd.gui.util.matcher.DescriptorMatcher; import java.awt.*; import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.List; import java.util.concurrent.Future; import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; public abstract class TypePage extends CustomLineNumbersPage implements UriGettable, UriOpenable, IndexesChangeListener, FocusedTypeGettable { protected API api; protected Container.Entry entry; protected Collection> collectionOfFutureIndexes = Collections.emptyList(); protected HashMap declarations = new HashMap<>(); protected TreeMap typeDeclarations = new TreeMap<>(); protected ArrayList references = new ArrayList<>(); protected ArrayList strings = new ArrayList<>(); public TypePage(API api, Container.Entry entry) { // Init attributes this.api = api; this.entry = entry; } @Override protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { return ((HyperlinkReferenceData)hyperlinkData).reference.enabled; } @Override @SuppressWarnings("unchecked") protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) { HyperlinkReferenceData hyperlinkReferenceData = (HyperlinkReferenceData)hyperlinkData; if (hyperlinkReferenceData.reference.enabled) { try { // Save current position in history Point location = textArea.getLocationOnScreen(); int offset = textArea.viewToModel(new Point(x - location.x, y - location.y)); URI uri = entry.getUri(); api.addURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), "position=" + offset, null)); // Open link ReferenceData reference = hyperlinkReferenceData.reference; String typeName = reference.typeName; List entries = IndexesUtil.findInternalTypeName(collectionOfFutureIndexes, typeName); String fragment = typeName; if (reference.name != null) { fragment += '-' + reference.name; } if (reference.descriptor != null) { fragment += '-' + reference.descriptor; } if (entries.contains(entry)) { api.openURI(new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), fragment)); } else { String rootUri = entry.getContainer().getRoot().getUri().toString(); ArrayList sameContainerEntries = new ArrayList<>(); for (Container.Entry entry : entries) { if (entry.getUri().toString().startsWith(rootUri)) { sameContainerEntries.add(entry); } } if (sameContainerEntries.size() > 0) { api.openURI(x, y, sameContainerEntries, null, fragment); } else if (entries.size() > 0) { api.openURI(x, y, entries, null, fragment); } } } catch (URISyntaxException e) { assert ExceptionUtil.printStackTrace(e); } } } // --- UriGettable --- // @Override public URI getUri() { return entry.getUri(); } // --- UriOpenable --- // /** * @param uri for URI format, @see jd.gui.api.feature.UriOpenable */ @Override public boolean openUri(URI uri) { ArrayList ranges = new ArrayList<>(); String fragment = uri.getFragment(); String query = uri.getQuery(); Marker.clearMarkAllHighlights(textArea); if (fragment != null) { matchFragmentAndAddDocumentRange(fragment, declarations, ranges); } if (query != null) { Map parameters = parseQuery(query); if (parameters.containsKey("lineNumber")) { String lineNumber = parameters.get("lineNumber"); try { goToLineNumber(Integer.parseInt(lineNumber)); return true; } catch (NumberFormatException e) { assert ExceptionUtil.printStackTrace(e); } } else if (parameters.containsKey("position")) { String position = parameters.get("position"); try { int pos = Integer.parseInt(position); if (textArea.getDocument().getLength() > pos) { ranges.add(new DocumentRange(pos, pos)); } } catch (NumberFormatException e) { assert ExceptionUtil.printStackTrace(e); } } else { matchQueryAndAddDocumentRange(parameters, declarations, hyperlinks, strings, ranges); } } if ((ranges != null) && !ranges.isEmpty()) { textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR); Marker.markAll(textArea, ranges); ranges.sort(null); setCaretPositionAndCenter(ranges.get(0)); } return true; } public static void matchFragmentAndAddDocumentRange(String fragment, HashMap declarations, List ranges) { if ((fragment.indexOf('?') != -1) || (fragment.indexOf('*') != -1)) { // Unknown type and/or descriptor ==> Select all and scroll to the first one int lastDash = fragment.lastIndexOf('-'); if (lastDash == -1) { // Search types String slashAndTypeName = fragment.substring(1); String typeName = fragment.substring(2); for (Map.Entry entry : declarations.entrySet()) { if (entry.getKey().endsWith(slashAndTypeName) || entry.getKey().equals(typeName)) { ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition)); } } } else { String prefix = fragment.substring(0, lastDash+1); String suffix = fragment.substring(lastDash+1); BiFunction matchDescriptors; if (suffix.charAt(0) == '(') { matchDescriptors = DescriptorMatcher::matchMethodDescriptors; } else { matchDescriptors = DescriptorMatcher::matchFieldDescriptors; } if (fragment.charAt(0) == '*') { // Unknown type String slashAndTypeNameAndName = prefix.substring(1); String typeNameAndName = prefix.substring(2); for (Map.Entry entry : declarations.entrySet()) { String key = entry.getKey(); if ((key.indexOf(slashAndTypeNameAndName) != -1) || (key.startsWith(typeNameAndName))) { int index = key.lastIndexOf('-') + 1; if (matchDescriptors.apply(suffix, key.substring(index))) { ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition)); } } } } else { // Known type for (Map.Entry entry : declarations.entrySet()) { String key = entry.getKey(); if (key.startsWith(prefix)) { int index = key.lastIndexOf('-') + 1; if (matchDescriptors.apply(suffix, key.substring(index))) { ranges.add(new DocumentRange(entry.getValue().startPosition, entry.getValue().endPosition)); } } } } } } else { // Known type and descriptor ==> Search and high light item DeclarationData data = declarations.get(fragment); if (data != null) { ranges.add(new DocumentRange(data.startPosition, data.endPosition)); } else if (fragment.endsWith("--()V")) { // 'static' bloc not found ==> Select type declaration String typeName = fragment.substring(0, fragment.indexOf('-')); data = declarations.get(typeName); ranges.add(new DocumentRange(data.startPosition, data.endPosition)); } } } public static void matchQueryAndAddDocumentRange( Map parameters, HashMap declarations, TreeMap hyperlinks, ArrayList strings, List ranges) { String highlightFlags = parameters.get("highlightFlags"); String highlightPattern = parameters.get("highlightPattern"); if ((highlightFlags != null) && (highlightPattern != null)) { String highlightScope = parameters.get("highlightScope"); String regexp = createRegExp(highlightPattern); Pattern pattern = Pattern.compile(regexp + ".*"); if (highlightFlags.indexOf('s') != -1) { // Highlight strings Pattern patternForString = Pattern.compile(regexp); for (StringData data : strings) { if (matchScope(highlightScope, data.owner)) { Matcher matcher = patternForString.matcher(data.text); int offset = data.startPosition; while(matcher.find()) { ranges.add(new DocumentRange(offset + matcher.start(), offset + matcher.end())); } } } } boolean t = (highlightFlags.indexOf('t') != -1); // Highlight types boolean f = (highlightFlags.indexOf('f') != -1); // Highlight fields boolean m = (highlightFlags.indexOf('m') != -1); // Highlight methods boolean c = (highlightFlags.indexOf('c') != -1); // Highlight constructors if (highlightFlags.indexOf('d') != -1) { // Highlight declarations for (Map.Entry entry : declarations.entrySet()) { DeclarationData declaration = entry.getValue(); if (matchScope(highlightScope, declaration.typeName)) { if ((t && declaration.isAType()) || (c && declaration.isAConstructor())) { matchAndAddDocumentRange(pattern, getMostInnerTypeName(declaration.typeName), declaration.startPosition, declaration.endPosition, ranges); } if ((f && declaration.isAField()) || (m && declaration.isAMethod())) { matchAndAddDocumentRange(pattern, declaration.name, declaration.startPosition, declaration.endPosition, ranges); } } } } if (highlightFlags.indexOf('r') != -1) { // Highlight references for (Map.Entry entry : hyperlinks.entrySet()) { HyperlinkData hyperlink = entry.getValue(); ReferenceData reference = ((HyperlinkReferenceData)hyperlink).reference; if (matchScope(highlightScope, reference.owner)) { if ((t && reference.isAType()) || (c && reference.isAConstructor())) { matchAndAddDocumentRange(pattern, getMostInnerTypeName(reference.typeName), hyperlink.startPosition, hyperlink.endPosition, ranges); } if ((f && reference.isAField()) || (m && reference.isAMethod())) { matchAndAddDocumentRange(pattern, reference.name, hyperlink.startPosition, hyperlink.endPosition, ranges); } } } } } } public static boolean matchScope(String scope, String type) { if ((scope == null) || scope.isEmpty()) return true; if (scope.charAt(0) == '*') return type.endsWith(scope.substring(1)) || type.equals(scope.substring(2)); return type.equals(scope); } public static void matchAndAddDocumentRange(Pattern pattern, String text, int start, int end, List ranges) { if (pattern.matcher(text).matches()) { ranges.add(new DocumentRange(start, end)); } } public static String getMostInnerTypeName(String typeName) { int lastPackageSeparatorIndex = typeName.lastIndexOf('/') + 1; int lastTypeNameSeparatorIndex = typeName.lastIndexOf('$') + 1; int lastIndex = Math.max(lastPackageSeparatorIndex, lastTypeNameSeparatorIndex); return typeName.substring(lastIndex); } // --- FocusedTypeGettable --- // @Override public String getFocusedTypeName() { Map.Entry entry = typeDeclarations.floorEntry(textArea.getCaretPosition()); if (entry != null) { DeclarationData data = entry.getValue(); if (data != null) { return data.typeName; } } return null; } @Override public Container.Entry getEntry() { return entry; } // --- IndexesChangeListener --- // @Override public void indexesChanged(Collection> collectionOfFutureIndexes) { // Update the list of containers this.collectionOfFutureIndexes = collectionOfFutureIndexes; // Refresh links boolean refresh = false; for (ReferenceData reference : references) { String typeName = reference.typeName; boolean enabled; if (reference.name == null) { enabled = false; try { for (Future futureIndexes : collectionOfFutureIndexes) { if (futureIndexes.isDone()) { Map index = futureIndexes.get().getIndex("typeDeclarations"); if ((index != null) && (index.get(typeName) != null)) { enabled = true; break; } } } } catch (Exception e) { assert ExceptionUtil.printStackTrace(e); } } else { try { // Recursive search typeName = searchTypeHavingMember(typeName, reference.name, reference.descriptor, entry); if (typeName != null) { // Replace type with the real type having the referenced member reference.typeName = typeName; enabled = true; } else { enabled = false; } } catch (Error e) { // Catch StackOverflowError or OutOfMemoryError assert ExceptionUtil.printStackTrace(e); enabled = false; } } if (reference.enabled != enabled) { reference.enabled = enabled; refresh = true; } } if (refresh) { textArea.repaint(); } } @SuppressWarnings("unchecked") protected String searchTypeHavingMember(String typeName, String name, String descriptor, Container.Entry entry) { ArrayList entries = new ArrayList<>(); try { for (Future futureIndexes : collectionOfFutureIndexes) { if (futureIndexes.isDone()) { Map index = futureIndexes.get().getIndex("typeDeclarations"); if (index != null) { Collection collection = index.get(typeName); if (collection != null) { entries.addAll(collection); } } } } } catch (Exception e) { assert ExceptionUtil.printStackTrace(e); } String rootUri = entry.getContainer().getRoot().getUri().toString(); ArrayList sameContainerEntries = new ArrayList<>(); for (Container.Entry e : entries) { if (e.getUri().toString().startsWith(rootUri)) { sameContainerEntries.add(e); } } if (sameContainerEntries.size() > 0) { return searchTypeHavingMember(typeName, name, descriptor, sameContainerEntries); } else { return searchTypeHavingMember(typeName, name, descriptor, entries); } } protected String searchTypeHavingMember(String typeName, String name, String descriptor, List entries) { for (Container.Entry entry : entries) { Type type = api.getTypeFactory(entry).make(api, entry, typeName); if (type != null) { if (descriptor.indexOf('(') == -1) { // Search a field for (Type.Field field : type.getFields()) { if (field.getName().equals(name) && DescriptorMatcher.matchFieldDescriptors(field.getDescriptor(), descriptor)) { // Field found return typeName; } } } else { // Search a method for (Type.Method method : type.getMethods()) { if (method.getName().equals(name) && DescriptorMatcher.matchMethodDescriptors(method.getDescriptor(), descriptor)) { // Method found return typeName; } } } // Not found -> Search in super type String typeOwnerName = searchTypeHavingMember(type.getSuperName(), name, descriptor, entry); if (typeOwnerName != null) { return typeOwnerName; } } } return null; } public static class StringData { int startPosition; int endPosition; String text; String owner; public StringData(int startPosition, int length, String text, String owner) { this.startPosition = startPosition; this.endPosition = startPosition + length; this.text = text; this.owner = owner; } } public static class DeclarationData { int startPosition; int endPosition; String typeName; /** * Field or method name or null for type */ String name; String descriptor; public DeclarationData(int startPosition, int length, String typeName, String name, String descriptor) { this.startPosition = startPosition; this.endPosition = startPosition + length; this.typeName = typeName; this.name = name; this.descriptor = descriptor; } public boolean isAType() { return name == null; } public boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; } public boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; } public boolean isAConstructor() { return "".equals(name); } } public static class HyperlinkReferenceData extends HyperlinkData { public ReferenceData reference; public HyperlinkReferenceData(int startPosition, int length, ReferenceData reference) { super(startPosition, startPosition+length); this.reference = reference; } } protected static class ReferenceData { public String typeName; /** * Field or method name or null for type */ public String name; /** * Field or method descriptor or null for type */ public String descriptor; /** * Internal type name containing reference or null for "import" statement. * Used to high light items matching with URI like "file://dir1/dir2/file?highlightPattern=hello&highlightFlags=drtcmfs&highlightScope=type". */ public String owner; /** * "Enabled" flag for link of reference */ public boolean enabled = false; public ReferenceData(String typeName, String name, String descriptor, String owner) { this.typeName = typeName; this.name = name; this.descriptor = descriptor; this.owner = owner; } boolean isAType() { return name == null; } boolean isAField() { return (descriptor != null) && descriptor.charAt(0) != '('; } boolean isAMethod() { return (descriptor != null) && descriptor.charAt(0) == '('; } boolean isAConstructor() { return "".equals(name); } } }