diff --git a/biz.aQute.bndlib.tests/test/aQute/bnd/osgi/resource/CapReqBuilderTest.java b/biz.aQute.bndlib.tests/test/aQute/bnd/osgi/resource/CapReqBuilderTest.java index cc9d44616d..7238f1272c 100644 --- a/biz.aQute.bndlib.tests/test/aQute/bnd/osgi/resource/CapReqBuilderTest.java +++ b/biz.aQute.bndlib.tests/test/aQute/bnd/osgi/resource/CapReqBuilderTest.java @@ -8,6 +8,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.osgi.resource.Capability; import org.osgi.resource.Requirement; import aQute.bnd.header.Attrs; @@ -18,6 +19,7 @@ public class CapReqBuilderTest { + @Test public void testSimple() throws Exception { CapabilityBuilder cb = new CapabilityBuilder("test"); @@ -173,4 +175,41 @@ public void testNonAliasedRequirementUnchanged() throws Exception { assertTrue(original == unaliased, "unaliasing a normal requirement should return the original object"); } + @Test + public void testCapabilityToRequirementWithFilter() throws Exception { + CapReqBuilder cr = new CapReqBuilder("osgi.wiring.package"); + Attrs attrs = new Attrs(); + attrs.putTyped("bundle-symbolic-name", "org.example"); + attrs.putTyped("bundle-version", "1.7.23"); + attrs.putTyped("osgi.wiring.package", "org.example.foo"); + attrs.putTyped("version", "1.7.23"); + attrs.putTyped("bnd.hashes", "123, 456, 789"); + cr.addAttributes(attrs); + + Capability cap = cr.buildSyntheticCapability(); + + Requirement req = CapReqBuilder.createRequirementFromCapability(cap) + .buildSyntheticRequirement(); + + assertEquals("osgi.wiring.package", req.getNamespace()); + assertEquals( + "(&(bundle-symbolic-name=org.example)(bundle-version>=1.7.23)(osgi.wiring.package=org.example.foo)(version>=1.7.23)(bnd.hashes=123, 456, 789))", + req.getDirectives() + .get("filter")); + + Requirement reqFiltered = CapReqBuilder.createRequirementFromCapability(cap, (name) -> { + if (name.equals("bundle-symbolic-name") || name.equals("bundle-version") || name.equals("bnd.hashes")) { + return false; + } + + return true; + }) + .buildSyntheticRequirement(); + + assertEquals("osgi.wiring.package", reqFiltered.getNamespace()); + assertEquals("(&(osgi.wiring.package=org.example.foo)(version>=1.7.23))", + reqFiltered.getDirectives() + .get("filter")); + } + } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/resource/CapReqBuilder.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/resource/CapReqBuilder.java index 8491924591..a0fb415010 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/resource/CapReqBuilder.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/resource/CapReqBuilder.java @@ -12,6 +12,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.function.Predicate; import org.osgi.framework.Constants; import org.osgi.framework.Version; @@ -709,6 +710,18 @@ static Object toVersions(Object value) { } public static RequirementBuilder createRequirementFromCapability(Capability capability) { + return createRequirementFromCapability(capability, null); + } + + /** + * @param capability the capability to convert + * @param includeAttributesFilter predicate to control from the caller which + * attributes to include. if null all attributes are + * included. + * @return a RequirementBuilder from the capability + */ + public static RequirementBuilder createRequirementFromCapability(Capability capability, + Predicate includeAttributesFilter) { final String namespace = capability.getNamespace(); RequirementBuilder builder = new RequirementBuilder(namespace); final String versionAttrName = Optional.ofNullable(ResourceUtils.getVersionAttributeForNamespace(namespace)) @@ -720,6 +733,12 @@ public static RequirementBuilder createRequirementFromCapability(Capability capa .append('&'); } capAttributes.forEach((name, v) -> { + + if (includeAttributesFilter != null && !includeAttributesFilter.test(name)) { + // skip this attribute + return; + } + if (v instanceof Version || name.equals(versionAttrName) || (namespace.equals(PackageNamespace.PACKAGE_NAMESPACE) && name.equals(AbstractWiringNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE))) { diff --git a/bndtools.core/bnd.bnd b/bndtools.core/bnd.bnd index 3d82451ca8..b6d32b7b2f 100644 --- a/bndtools.core/bnd.bnd +++ b/bndtools.core/bnd.bnd @@ -109,7 +109,9 @@ Export-Package: org.osgi.service.metatype.annotations bndtools.utils;version=project;packages='*',\ slf4j.api,\ org.eclipse.ui.ide.application;version='1.3',\ - org.eclipse.ui.console + org.eclipse.ui.console,\ + org.eclipse.e4.core.services,\ + org.osgi.service.event -testpath: \ slf4j.api,\ diff --git a/bndtools.core/src/bndtools/views/ViewEventTopics.java b/bndtools.core/src/bndtools/views/ViewEventTopics.java new file mode 100644 index 0000000000..cfa4aec95c --- /dev/null +++ b/bndtools.core/src/bndtools/views/ViewEventTopics.java @@ -0,0 +1,30 @@ +package bndtools.views; + +/** + * Topics for the EventBroker which is used for communication between different + * views. For example if View1 sends an event to View2 wants to open a dialog in + * the other view. . + */ +public enum ViewEventTopics { + + /** + * Event to open the advances search of the repositories view. + */ + REPOSITORIESVIEW_OPEN_ADVANCED_SEARCH("EVENT/RepositoriesView/openAdvancedSearch"); + + private String eventtype; + + ViewEventTopics(String eventtype) { + this.eventtype = eventtype; + } + + public String topic() { + return eventtype; + } + + @Override + public String toString() { + return eventtype; + } + +} diff --git a/bndtools.core/src/bndtools/views/repository/AdvancedSearchDialog.java b/bndtools.core/src/bndtools/views/repository/AdvancedSearchDialog.java index d3c0c55198..e2e8eeeed8 100644 --- a/bndtools.core/src/bndtools/views/repository/AdvancedSearchDialog.java +++ b/bndtools.core/src/bndtools/views/repository/AdvancedSearchDialog.java @@ -22,6 +22,7 @@ import org.eclipse.swt.widgets.TabItem; import org.eclipse.ui.IMemento; import org.eclipse.ui.IPersistable; +import org.eclipse.ui.XMLMemento; import org.osgi.resource.Requirement; public class AdvancedSearchDialog extends TitleAreaDialog implements IPersistable { @@ -156,4 +157,19 @@ public void restoreState(IMemento memento) { } } + /** + * @param req a requirement + * @return a state object for the given requirement to open advancedSearch + * prefilled in the "Other" tab. + */ + public static IMemento toNamespaceSearchPanelMemento(Requirement req) { + XMLMemento memento = XMLMemento.createWriteRoot("search"); + memento.putInteger("tabIndex", 2); + IMemento other = memento.createChild("tab", "Other"); + other.putString("namespace", req.getNamespace()); + other.putString("filter", req.getDirectives() + .get("filter")); + return memento; + } + } diff --git a/bndtools.core/src/bndtools/views/repository/RepositoriesView.java b/bndtools.core/src/bndtools/views/repository/RepositoriesView.java index 7582809db9..41530690fd 100644 --- a/bndtools.core/src/bndtools/views/repository/RepositoriesView.java +++ b/bndtools.core/src/bndtools/views/repository/RepositoriesView.java @@ -39,6 +39,7 @@ import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.GroupMarker; import org.eclipse.jface.action.IAction; @@ -89,6 +90,7 @@ import org.eclipse.ui.part.ResourceTransfer; import org.eclipse.ui.part.ViewPart; import org.osgi.resource.Requirement; +import org.osgi.service.event.Event; import org.osgi.service.repository.Repository; import aQute.bnd.build.Workspace; @@ -118,6 +120,7 @@ import bndtools.utils.HierarchicalLabel; import bndtools.utils.HierarchicalMenu; import bndtools.utils.SelectionDragAdapter; +import bndtools.views.ViewEventTopics; import bndtools.wizards.workspace.AddFilesToRepositoryWizard; import bndtools.wizards.workspace.WorkspaceSetupWizard; @@ -143,6 +146,9 @@ public class RepositoriesView extends ViewPart implements RepositoriesViewRefres private Action downloadAction; private String advancedSearchState; private Action offlineAction; + private final IEventBroker eventBroker = PlatformUI.getWorkbench() + .getService(IEventBroker.class); + private final BndPreferences prefs = new BndPreferences(); @@ -467,6 +473,9 @@ public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { IActionBars actionBars = getViewSite().getActionBars(); actionBars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), refreshAction); + // Event subscription + eventBroker.subscribe(ViewEventTopics.REPOSITORIESVIEW_OPEN_ADVANCED_SEARCH.topic(), + event -> handleOpenAdvancedSearch(event)); } private void configureOfflineAction() { @@ -1221,4 +1230,22 @@ private HierarchicalLabel createContextMenueBsn(final RepositoryPlugin r })); } + private void handleOpenAdvancedSearch(Event event) { + + if (event == null) { + return; + } + + // Handle the event, open the dialog + if (event.getProperty(IEventBroker.DATA) instanceof Requirement req) { + + // fill and open advanced search + advancedSearchState = AdvancedSearchDialog.toNamespaceSearchPanelMemento(req) + .toString(); + advancedSearchAction.setChecked(true); + advancedSearchAction.run(); + + } + } + } diff --git a/bndtools.core/src/bndtools/views/resolution/ResolutionView.java b/bndtools.core/src/bndtools/views/resolution/ResolutionView.java index 9f18af264e..21d9f72f84 100644 --- a/bndtools.core/src/bndtools/views/resolution/ResolutionView.java +++ b/bndtools.core/src/bndtools/views/resolution/ResolutionView.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import org.bndtools.core.ui.icons.Icons; import org.eclipse.core.resources.IFile; @@ -23,6 +24,7 @@ import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; @@ -36,19 +38,25 @@ import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.util.LocalSelectionTransfer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; +import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.OpenEvent; +import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DragSourceEvent; import org.eclipse.swt.dnd.DragSourceListener; +import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; @@ -68,16 +76,19 @@ import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.ResourceUtil; import org.eclipse.ui.part.ViewPart; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; import org.osgi.resource.Resource; import org.osgi.service.repository.Repository; import aQute.bnd.build.model.EE; import aQute.bnd.osgi.Clazz; +import aQute.bnd.osgi.resource.CapReqBuilder; import aQute.bnd.osgi.resource.ResourceUtils; import aQute.bnd.unmodifiable.Sets; import aQute.lib.io.IO; @@ -97,6 +108,7 @@ import bndtools.tasks.ResourceCapReqLoader; import bndtools.utils.PartAdapter; import bndtools.utils.SelectionUtils; +import bndtools.views.ViewEventTopics; public class ResolutionView extends ViewPart implements ISelectionListener, IResourceChangeListener { @@ -119,6 +131,9 @@ public class ResolutionView extends ViewPart implements ISelectionListener, IRes private final Set filteredCapabilityNamespaces; + private final IEventBroker eventBroker = PlatformUI.getWorkbench() + .getService(IEventBroker.class); + public ResolutionView() { filteredCapabilityNamespaces = Sets.of(IdentityNamespace.IDENTITY_NAMESPACE, HostNamespace.HOST_NAMESPACE); loaders = Collections.emptySet(); @@ -200,6 +215,12 @@ public void createPartControl(Composite parent) { ColumnViewerToolTipSupport.enableFor(reqsViewer); reqsViewer.setLabelProvider(new RequirementWrapperLabelProvider(true)); reqsViewer.setContentProvider(new CapReqMapContentProvider()); + reqsViewer.addDoubleClickListener(event -> handleReqsViewerDoubleClickEvent(event)); + + reqsViewer.getControl() + .addKeyListener(createCopyToClipboardAdapter(reqsViewer, + (IStructuredSelection selection, StringBuilder clipboardContent) -> reqsCopyToClipboard(selection, + (RequirementWrapperLabelProvider) reqsViewer.getLabelProvider(), clipboardContent))); Composite capsPanel = new Composite(splitPanel, SWT.NONE); capsPanel.setBackground(parent.getBackground()); @@ -226,6 +247,13 @@ public boolean select(Viewer viewer, Object parent, Object element) { } }); + capsViewer.addDoubleClickListener(event -> handleCapsViewerDoubleClickEvent(event)); + + capsViewer.getTable() + .addKeyListener(createCopyToClipboardAdapter(capsViewer, + (IStructuredSelection selection1, StringBuilder clipboardContent1) -> capsCopyToClipboard(selection1, + (CapabilityLabelProvider) capsViewer.getLabelProvider(), clipboardContent1))); + hideSelfImportsFilter = new ViewerFilter() { @Override @@ -268,6 +296,11 @@ public boolean select(Viewer viewer, Object parentElement, Object element) { selectionChanged(activePart, activeSelection); } + + + + + private void openEditor(OpenEvent event) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); for (Iterator iter = selection.iterator(); iter.hasNext();) { @@ -597,4 +630,135 @@ public void dragSetData(DragSourceEvent event) { public void dragFinished(DragSourceEvent event) {} } + private void handleReqsViewerDoubleClickEvent(DoubleClickEvent event) { + if (!event.getSelection() + .isEmpty()) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + final Object element = selection.getFirstElement(); + + if (element instanceof RequirementWrapper rw) { + + // Open AdvanvedSearch of RepositoriesView + Requirement req = rw.requirement; + eventBroker.post(ViewEventTopics.REPOSITORIESVIEW_OPEN_ADVANCED_SEARCH.topic(), req); + } + + } + } + + private void handleCapsViewerDoubleClickEvent(DoubleClickEvent event) { + if (!event.getSelection() + .isEmpty()) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + final Object element = selection.getFirstElement(); + + if (element instanceof Capability cap) { + + // convert the capability to a requirement (without + // bundle-attributes and bnd.hashes) for better results in the + // advanced search e.g. + // (&(osgi.wiring.package=my.package.foo)(version>=1.7.23)) + Requirement req = CapReqBuilder.createRequirementFromCapability(cap, (name) -> { + if (name.equals("bundle-symbolic-name") || name.equals("bundle-version") + || name.equals("bnd.hashes")) { + return false; + } + + return true; + }) + .buildSyntheticRequirement(); + // Open AdvanvedSearch of RepositoriesView + eventBroker.post(ViewEventTopics.REPOSITORIESVIEW_OPEN_ADVANCED_SEARCH.topic(), req); + } + + } + } + + /** + * Generic copy to clipboard handling via Ctrl+C or MacOS: Cmd+C + * + * @param viewer the viewer + * @param clipboardContentExtractor handler to extract content from the + * selected items. + * @return a KeyAdapter copying content of the selected items to clipboard + */ + private KeyAdapter createCopyToClipboardAdapter(StructuredViewer viewer, + BiConsumer clipboardContentExtractor) { + return new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + // Check if Ctrl+C or MacOS: Cmd+C was pressed + if ((e.stateMask & SWT.MOD1) == SWT.MOD1 && e.keyCode == 'c') { + IStructuredSelection selection = viewer.getStructuredSelection(); + StringBuilder clipboardString = new StringBuilder(); + + clipboardContentExtractor.accept(selection, clipboardString); + + if (clipboardString.length() > 0) { + Clipboard clipboard = new Clipboard(Display.getCurrent()); + TextTransfer textTransfer = TextTransfer.getInstance(); + clipboard.setContents(new Object[] { + clipboardString.toString() + }, new Transfer[] { + textTransfer + }); + clipboard.dispose(); + } + } + } + + }; + } + + + private void reqsCopyToClipboard(IStructuredSelection selection, RequirementWrapperLabelProvider lp, + StringBuilder clipboardContent) { + + for (Iterator iterator = selection.iterator(); iterator.hasNext();) { + Object element = iterator.next(); + + if (element instanceof RequirementWrapper reqWrapper) { + + clipboardContent.append(lp.getToolTipText(reqWrapper)); + + if (iterator.hasNext()) { + clipboardContent.append(System.lineSeparator()); + } + + } else { + clipboardContent.append(element.toString()); + + if (iterator.hasNext()) { + clipboardContent.append(System.lineSeparator()); + } + + } + } + } + + + + private void capsCopyToClipboard(IStructuredSelection selection, CapabilityLabelProvider lp, + StringBuilder clipboardContent) { + + for (Iterator iterator = selection.iterator(); iterator.hasNext();) { + Object element = iterator.next(); + + if (element instanceof Capability cap) { + + clipboardContent.append(lp.getToolTipText(cap)); + + if (iterator.hasNext()) { + clipboardContent.append(System.lineSeparator()); + } + + } else { + clipboardContent.append(element.toString()); + } + + if (iterator.hasNext()) { + clipboardContent.append(System.lineSeparator()); + } + } + } }