diff --git a/ui/org.eclipse.pde.bnd.ui/icons/workspacerepo.png b/ui/org.eclipse.pde.bnd.ui/icons/workspacerepo.png new file mode 100644 index 00000000000..3d09261a26e Binary files /dev/null and b/ui/org.eclipse.pde.bnd.ui/icons/workspacerepo.png differ diff --git a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/RepositoryUtils.java b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/RepositoryUtils.java index 0d3de43b5c3..7f60257157a 100644 --- a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/RepositoryUtils.java +++ b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/RepositoryUtils.java @@ -35,6 +35,7 @@ import org.osgi.util.tracker.ServiceTracker; import aQute.bnd.build.Workspace; +import aQute.bnd.build.WorkspaceLayout; import aQute.bnd.memoize.Memoize; import aQute.bnd.service.RegistryPlugin; import aQute.bnd.service.RepositoryPlugin; @@ -53,6 +54,7 @@ public class RepositoryUtils { return tracker.orElse(null); }, Objects::nonNull); } + public static List listRepositories(final Workspace localWorkspace, final boolean hideCache) { if (localWorkspace == null) { @@ -70,6 +72,14 @@ public static List listRepositories(final Workspace localWorks // Workspace bndWorkspace = Central.getWorkspaceIfPresent(); // if ((bndWorkspace == localWorkspace) && !bndWorkspace.isDefaultWorkspace()) // repos.add(Central.getWorkspaceRepository()); + + // TODO this is not perfect, because it is only working + // if you are selecting a bnd project. Would be better if bnd WorkspaceRepository is added always + // e.g. if there is at least one bnd project + if (WorkspaceLayout.BND == localWorkspace.getLayout() && !localWorkspace.isDefaultWorkspace()) { + repos.add(localWorkspace.getWorkspaceRepository()); + } + // Add the repos from the provided workspace for (RepositoryPlugin plugin : plugins) { diff --git a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/RepositoryTreeContentProvider.java b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/RepositoryTreeContentProvider.java index b205b584b46..f9f2d5dd79a 100644 --- a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/RepositoryTreeContentProvider.java +++ b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/RepositoryTreeContentProvider.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -68,6 +69,11 @@ public class RepositoryTreeContentProvider implements ITreeContentProvider { private String rawFilter = null; private String wildcardFilter = null; + /** + * Number of filter results to keep per repo. This is to avoid memory leaks + * if you search with lots of different filter strings. + */ + private static final int MAX_CACHED_FILTER_RESULTS = 10; private boolean showRepos = true; private Requirement requirementFilter = null; @@ -306,7 +312,8 @@ Object[] getRepositoryBundles(final RepositoryPlugin repoPlugin) { * this node and the next time this method gets called the 'results' * will be available in the cache */ - Map listResults = repoPluginListResults.computeIfAbsent(repoPlugin, p -> new HashMap<>()); + Map listResults = repoPluginListResults.computeIfAbsent(repoPlugin, + p -> createLRUMap(MAX_CACHED_FILTER_RESULTS)); result = listResults.get(wildcardFilter); @@ -335,7 +342,7 @@ protected IStatus run(IProgressMonitor monitor) { } Map listResults = repoPluginListResults.computeIfAbsent(repoPlugin, - p -> new HashMap<>()); + p -> createLRUMap(MAX_CACHED_FILTER_RESULTS)); listResults.put(wildcardFilter, jobresult); Display.getDefault() @@ -362,7 +369,7 @@ protected IStatus run(IProgressMonitor monitor) { if (status != null && status.isOK()) { Map fastResults = repoPluginListResults.computeIfAbsent(repoPlugin, - p -> new HashMap<>()); + p -> createLRUMap(MAX_CACHED_FILTER_RESULTS)); result = fastResults.get(wildcardFilter); } else { Object[] loading = new Object[] { @@ -392,4 +399,19 @@ private Object[] searchR5Repository(RepositoryPlugin repoPlugin, Repository osgi result = resultSet.toArray(); return result; } + + + // Define a LRU-like inner map (max n entries) + private static Map createLRUMap(int n) { + return new LinkedHashMap(n + 1, 1.0f, true) { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // Auto-remove oldest when size > n + // but always keep the 'null' key which is '*' + return size() > n && eldest.getKey() != null; + } + }; + } } diff --git a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/SearchableRepositoryTreeContentProvider.java b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/SearchableRepositoryTreeContentProvider.java index 7eafb0689da..6d06a5d8094 100644 --- a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/SearchableRepositoryTreeContentProvider.java +++ b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/model/repo/SearchableRepositoryTreeContentProvider.java @@ -14,6 +14,12 @@ *******************************************************************************/ package org.eclipse.pde.bnd.ui.model.repo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + import aQute.bnd.service.RepositoryPlugin; import aQute.bnd.service.repository.SearchableRepository; @@ -42,4 +48,32 @@ Object[] getRepositoryBundles(RepositoryPlugin repo) { return result; } + + public List allRepoBundleVersions(final RepositoryPlugin rp) { + Object[] result = getChildren(rp); + + List allChildren = new ArrayList<>(); + Queue queue = new LinkedList<>(); + + if (result != null) { + queue.addAll(Arrays.asList(result)); + } + + while (!queue.isEmpty()) { + Object currentChild = queue.poll(); + + if (currentChild instanceof RepositoryBundleVersion rpv) { + allChildren.add(rpv); + } + else if (currentChild instanceof RepositoryResourceElement rre) { + allChildren.add(rre.getRepositoryBundleVersion()); + } + + Object[] childrenOfChild = getChildren(currentChild); + if (childrenOfChild != null) { + queue.addAll(Arrays.asList(childrenOfChild)); + } + } + return allChildren; + } } diff --git a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/views/repository/RepositoriesView.java b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/views/repository/RepositoriesView.java index da25152ad4a..223199a4340 100644 --- a/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/views/repository/RepositoriesView.java +++ b/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/views/repository/RepositoriesView.java @@ -39,6 +39,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Iterator; @@ -84,6 +85,7 @@ import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerDropAdapter; @@ -151,6 +153,8 @@ import aQute.bnd.build.Workspace; import aQute.bnd.exceptions.Exceptions; import aQute.bnd.http.HttpClient; +import aQute.bnd.osgi.resource.FilterParser.PackageExpression; +import aQute.bnd.osgi.resource.ResourceUtils; import aQute.bnd.service.Actionable; import aQute.bnd.service.Refreshable; import aQute.bnd.service.Registry; @@ -198,6 +202,9 @@ public void workspaceOfflineChanged(boolean offline) { private final IObservableValue workspaceName = new WritableValue<>(); private final IObservableValue workspaceDescription = new WritableValue<>(); + private Object[] lastExpandedElements; + private TreePath[] lastExpandedPaths; + @Override public void createPartControl(final Composite parent) { // CREATE CONTROLS @@ -601,11 +608,28 @@ boolean addFilesToRepository(RepositoryPlugin repo, File[] files) { } private void updatedFilter(String filterString) { - contentProvider.setFilter(filterString); - viewer.refresh(); - if (filterString != null) { - viewer.expandToLevel(2); - } + viewer.getTree() + .setRedraw(false); + + try { + if (filterString == null || filterString.isEmpty()) { + // Restore previous state when clearing filter + contentProvider.setFilter(null); + viewer.refresh(); // Required to clear filter + restoreExpansionState(); + viewer.refresh(); + } else { + // Save state before applying new filter + saveExpansionState(); + contentProvider.setFilter(filterString); + viewer.refresh(); + viewer.expandToLevel(2); + } + + } finally { + viewer.getTree() + .setRedraw(true); + } } void createActions() { @@ -1209,10 +1233,78 @@ private void addCopyToClipboardSubMenueEntries(Actionable act, final RepositoryP if ((act instanceof Repository) || (act instanceof RepositoryPlugin)) { hmenu.add(createContextMenueCopyInfoRepo(act, rp, clipboard)); + hmenu.add(createContextMenueCopyBundlesWithSelfImports(act, rp, clipboard)); } } + + private HierarchicalLabel createContextMenueCopyBundlesWithSelfImports(Actionable act, final RepositoryPlugin rp, + final Clipboard clipboard) { + return new HierarchicalLabel("Copy to clipboard :: Bundles with substitution packages (self-imports)", + (label) -> createAction(label.getLeaf(), + "Add list of bundles containing packages which are imported and exported in their Manifest.", true, + false, rp, () -> { + + final StringBuilder sb = new StringBuilder( + "Shows list of bundles in the repository '" + rp.getName() + + "' containing substitution packages / self-imports (i.e. same package imported and exported) in their Manifest. \n" + + "Note: a missing version range can cause wiring / resolution problems.\n" + + "See https://docs.osgi.org/specification/osgi.core/8.0.0/framework.module.html#i3238802 " + + "and https://docs.osgi.org/specification/osgi.core/8.0.0/framework.module.html#framework.module-import.export.same.package " + + "for more information." + + "\n\n"); + + for (RepositoryBundleVersion rpv : contentProvider.allRepoBundleVersions(rp)) { + org.osgi.resource.Resource r = rpv.getResource(); + Collection selfImports = ResourceUtils + .getSubstitutionPackages(r); + + if (!selfImports.isEmpty()) { + long numWithoutRange = selfImports.stream() + .filter(pckExp -> pckExp.getRangeExpression() == null) + .count(); + + // Main package information + sb.append(r.toString()) + .append("\n"); + sb.append(" Substitution packages: ") + .append(selfImports.size()); + + // Additional information about packages without + // version range + if (numWithoutRange > 0) { + sb.append(" (") + .append(numWithoutRange) + .append(" without version range)"); + } + sb.append("\n"); + + // List of substitution packages + sb.append(" [\n"); + for (PackageExpression pckExp : selfImports) { + sb.append(" ") + .append(pckExp.toString()) + .append(",\n"); + } + // Remove the last comma and newline + if (!selfImports.isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append("\n ]\n\n"); + } + + } + + if (sb.isEmpty()) { + clipboard.copy("-Empty-"); + } else { + clipboard.copy(sb.toString()); + } + + })); + } + private HierarchicalLabel createContextMenueCopyInfoRepo(Actionable act, final RepositoryPlugin rp, final Clipboard clipboard) { return new HierarchicalLabel("Copy to clipboard :: Copy info", (label) -> createAction(label.getLeaf(), @@ -1330,6 +1422,21 @@ private void handleOpenAdvancedSearch(Event event) { } } + private void saveExpansionState() { + lastExpandedElements = viewer.getExpandedElements(); + lastExpandedPaths = viewer.getExpandedTreePaths(); + } + + private void restoreExpansionState() { + if (lastExpandedElements != null) { + viewer.setExpandedElements(lastExpandedElements); + } + if (lastExpandedPaths != null) { + viewer.setExpandedTreePaths(lastExpandedPaths); + } + } + + @Override public Workspace getWorkspace() { return this.workspace;