From f87024816c2c626bd7fb0416ba3dd8de78b09a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Mon, 31 Jul 2023 20:06:11 +0200 Subject: [PATCH] Add osgi-repository target location type The OSGi Specification defines a XML format for representing a repository as defined in "132 Repository Service Specification" that allows to hold "Resources" with requirements and capabilities. Such a repository usually contains a set of bundles and is very similar to what we have today with P2 updatesites, to improve interfacing of PDE with such repositories this adds a new target location type "Repository" where content can be used as usual. --- ui/org.eclipse.pde.core/META-INF/MANIFEST.MF | 1 + ui/org.eclipse.pde.core/plugin.xml | 8 + .../target/RepositoryBundleContainer.java | 185 +++++++++++ .../target/RepositoryLocationFactory.java | 68 ++++ ui/org.eclipse.pde.ui/META-INF/MANIFEST.MF | 4 +- .../icons/dview16/memory_view.png | Bin 0 -> 346 bytes .../icons/dview16/memory_view@2x.png | Bin 0 -> 608 bytes ui/org.eclipse.pde.ui/plugin.properties | 6 +- ui/org.eclipse.pde.ui/plugin.xml | 39 ++- .../eclipse/pde/internal/ui/PDEPlugin.java | 2 + .../pde/internal/ui/PDEPluginImages.java | 2 + .../target/EditRepositoryContainerPage.java | 290 ++++++++++++++++++ .../internal/ui/shared/target/Messages.java | 5 + ...positoryBundleContainerAdapterFactory.java | 212 +++++++++++++ .../target/RepositoryLocationWizard.java | 75 +++++ .../target/SelectRepositoryContentPage.java | 192 ++++++++++++ .../ui/shared/target/messages.properties | 9 +- 17 files changed, 1091 insertions(+), 7 deletions(-) create mode 100644 ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryBundleContainer.java create mode 100644 ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryLocationFactory.java create mode 100644 ui/org.eclipse.pde.ui/icons/dview16/memory_view.png create mode 100644 ui/org.eclipse.pde.ui/icons/dview16/memory_view@2x.png create mode 100644 ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditRepositoryContainerPage.java create mode 100644 ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryBundleContainerAdapterFactory.java create mode 100644 ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryLocationWizard.java create mode 100644 ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/SelectRepositoryContentPage.java diff --git a/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF index 3f2bee839e..efad311560 100644 --- a/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF +++ b/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF @@ -80,6 +80,7 @@ Export-Package: org.eclipse.pde.unittest.junit", org.eclipse.pde.internal.core.variables;x-internal:=true Import-Package: aQute.bnd.build;version="[4.4.0,5.0.0)", + aQute.bnd.header;version="2.5.0", aQute.bnd.osgi;version="[5.5.0,6.0.0)", aQute.bnd.osgi.repository;version="[3.0.0,4.0.0)", aQute.bnd.osgi.resource;version="[4.3.0,5.0.0)", diff --git a/ui/org.eclipse.pde.core/plugin.xml b/ui/org.eclipse.pde.core/plugin.xml index 0db7b0acc4..98dbd6bea9 100644 --- a/ui/org.eclipse.pde.core/plugin.xml +++ b/ui/org.eclipse.pde.core/plugin.xml @@ -437,4 +437,12 @@ class="org.eclipse.pde.internal.core.LocalMavenPluginSourcePathLocator" complexity="low"/> + + + + diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryBundleContainer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryBundleContainer.java new file mode 100644 index 0000000000..7cb47c74da --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryBundleContainer.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.core.target; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.equinox.internal.p2.repository.CacheManager; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.TargetBundle; +import org.eclipse.pde.core.target.TargetFeature; +import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.util.PDEXmlProcessorFactory; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import aQute.bnd.osgi.repository.ResourcesRepository; +import aQute.bnd.osgi.repository.XMLResourceParser; +import aQute.bnd.osgi.resource.ResourceUtils; +import aQute.bnd.osgi.resource.ResourceUtils.ContentCapability; + +@SuppressWarnings("restriction") +public class RepositoryBundleContainer extends AbstractBundleContainer { + public static final String ATTRIBUTE_URI = "uri"; //$NON-NLS-1$ + + public static final String ELEMENT_REQUIRE = "require"; //$NON-NLS-1$ + + public static final String TYPE = "Repository"; //$NON-NLS-1$ + + private final String uri; + + private final Collection requirements; + + public RepositoryBundleContainer(String uri, Collection requirements) { + this.uri = uri; + this.requirements = requirements; + } + + @Override + protected TargetBundle[] resolveBundles(ITargetDefinition definition, IProgressMonitor monitor) + throws CoreException { + ResourcesRepository repository = getRepository(monitor); + Map> providers = repository.findProviders(getRequirements()); + List bundles = new ArrayList<>(); + List contentCapabilities = providers.values().stream().flatMap(Collection::stream) + .map(Capability::getResource).distinct().map(ResourceUtils::getContentCapability) + .filter(Objects::nonNull).toList(); + CacheManager cacheManager = getCacheManager(); + for (ContentCapability content : contentCapabilities) { + URI url = content.url(); + try { + File file; + if (cacheManager != null) { + file = cacheManager.createCacheFromFile(url, monitor); + } else { + file = new File(FileLocator.toFileURL(url.toURL()).toURI()); + } + bundles.add(new TargetBundle(file)); + } catch (IOException | URISyntaxException e) { + throw new CoreException(Status.error("Can't fetch bundle from " + url, e)); + } + } + return bundles.toArray(TargetBundle[]::new); + } + + public String getUri() { + return uri; + } + + public Collection getRequirements() { + return requirements; + } + + public ResourcesRepository getRepository(IProgressMonitor monitor) throws CoreException { + String location = getLocation(true); + try { + URI base = new URI(location); + try { + CacheManager cacheManager = getCacheManager(); + if (cacheManager != null) { + File file = cacheManager.createCacheFromFile(base, monitor); + return new ResourcesRepository(XMLResourceParser.getResources(file, base)); + } + return new ResourcesRepository(XMLResourceParser.getResources(base)); + } catch (Exception e) { + if (e instanceof CoreException core) { + throw core; + } + if (e instanceof RuntimeException runtime) { + throw runtime; + } + throw new CoreException(Status.error("Loading repository from " + location + " failed: " + e, e)); + } + } catch (URISyntaxException e) { + throw new CoreException(Status.error("Invalid URI: " + location, e)); + } + } + + private CacheManager getCacheManager() throws CoreException { + return P2TargetUtils.getAgent().getService(CacheManager.class); + } + + @Override + protected TargetFeature[] resolveFeatures(ITargetDefinition definition, IProgressMonitor monitor) + throws CoreException { + return new TargetFeature[0]; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getLocation(boolean resolve) throws CoreException { + if (resolve) { + return resolveVariables(uri); + } + return uri; + } + + @Override + public String serialize() { + try { + DocumentBuilder docBuilder = PDEXmlProcessorFactory.createDocumentBuilderWithErrorOnDOCTYPE(); + Document document = docBuilder.newDocument(); + Element containerElement = document.createElement(TargetDefinitionPersistenceHelper.LOCATION); + containerElement.setAttribute(TargetDefinitionPersistenceHelper.ATTR_LOCATION_TYPE, TYPE); + containerElement.setAttribute(ATTRIBUTE_URI, getUri()); + for (Requirement requirement : requirements) { + Element requireElement = document.createElement(ELEMENT_REQUIRE); + requireElement.setTextContent(requirement.toString()); + containerElement.appendChild(requireElement); + } + document.appendChild(containerElement); + StreamResult result = new StreamResult(new StringWriter()); + TransformerFactory f = PDEXmlProcessorFactory.createTransformerFactoryWithErrorOnDOCTYPE(); + Transformer transformer = f.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ + transformer.transform(new DOMSource(document), result); + String xml = result.getWriter().toString(); + return xml; + } catch (Exception e) { + PDECore.log(e); + return null; + } + } + + public void reload() { + fResolutionStatus = null; + fBundles = null; + } +} diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryLocationFactory.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryLocationFactory.java new file mode 100644 index 0000000000..4cdd48268a --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/RepositoryLocationFactory.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.core.target; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.IntStream; + +import javax.xml.parsers.DocumentBuilder; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Status; +import org.eclipse.osgi.util.NLS; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.ITargetLocationFactory; +import org.eclipse.pde.internal.core.util.PDEXmlProcessorFactory; +import org.osgi.resource.Requirement; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import aQute.bnd.header.Parameters; +import aQute.bnd.osgi.resource.CapReqBuilder; + +public class RepositoryLocationFactory implements ITargetLocationFactory { + + @Override + public ITargetLocation getTargetLocation(String type, String serializedXML) throws CoreException { + if (!RepositoryBundleContainer.TYPE.equals(type)) { + throw new CoreException( + Status.error(NLS.bind(Messages.TargetRefrenceLocationFactory_Unsupported_Type, type))); + } + try { + DocumentBuilder docBuilder = PDEXmlProcessorFactory.createDocumentBuilderWithErrorOnDOCTYPE(); + Document document = docBuilder + .parse(new ByteArrayInputStream(serializedXML.getBytes(StandardCharsets.UTF_8))); + Element location = document.getDocumentElement(); + NodeList childNodes = location.getChildNodes(); + List requirements = IntStream.range(0, childNodes.getLength()).mapToObj(childNodes::item) + .filter(Element.class::isInstance).map(Element.class::cast) + .filter(element -> element.getNodeName() + .equalsIgnoreCase(RepositoryBundleContainer.ELEMENT_REQUIRE)) + .flatMap(element -> { + String textContent = element.getTextContent(); + Parameters parameters = new Parameters(textContent); + return CapReqBuilder.getRequirementsFrom(parameters).stream(); + }).toList(); + return new RepositoryBundleContainer( + location.getAttribute(RepositoryBundleContainer.ATTRIBUTE_URI), requirements); + } catch (Exception e) { + throw new CoreException( + Status.error(NLS.bind(Messages.TargetRefrenceLocationFactory_Parsing_Failed, e.getMessage()), e)); + } + } + +} diff --git a/ui/org.eclipse.pde.ui/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.ui/META-INF/MANIFEST.MF index f7693ec2fe..62c8423a3d 100644 --- a/ui/org.eclipse.pde.ui/META-INF/MANIFEST.MF +++ b/ui/org.eclipse.pde.ui/META-INF/MANIFEST.MF @@ -121,7 +121,9 @@ Require-Bundle: biz.aQute.bndlib;bundle-version="6.3.1" Import-Package: org.eclipse.jdt.debug.ui.console, org.eclipse.ui.internal.genericeditor, - org.osgi.service.event;version="[1.4,2.0.0)" + org.osgi.service.event;version="[1.4,2.0.0)", + org.osgi.service.repository;version="1.1.0", + org.osgi.util.promise;version="1.3.0" Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Automatic-Module-Name: org.eclipse.pde.ui diff --git a/ui/org.eclipse.pde.ui/icons/dview16/memory_view.png b/ui/org.eclipse.pde.ui/icons/dview16/memory_view.png new file mode 100644 index 0000000000000000000000000000000000000000..0e09f99274cf6772e08c984ad18a1f0bed684a0f GIT binary patch literal 346 zcmV-g0j2(lP)AHT)azz50%_iS&x9@41;&3>i zC<-J=0@rn6nkE=9kbq$r*zI=HgI(95sw%W?i@L7ic^-IA0&E5NzK?PmecxjkhG~+rED;0&cuxYVs$Kx&I9>p}CjmuKNPzbwAj>id z@cw^+)oMioye9!kl1PB}B(PjANr3m?0rzaPyoO46Nvh-)z*;oe)3kysCfOYnL4}`477;X}>@B2;yl0ec#48c<3h5Pz_!#xtw z;yNcHkrWSSIOn`G&nyfx8m(5V`Il%eN+1vjJPQVcABay>K6*T!JD1A^hRZ&m??~@I z6D7aj|AI1)&LZITdf&R;?xWJ!LvcEt-y9ByL6rdYEOOFI#9}ez^LaEH4K$lgRI63g z>vi;cJ@KwkC?Fb*ihoZ;-bo8km@7a$9>;h*#&kOUiP30;!C>&)Z;3<#N?x@9_iQqm zh>B;k85WBLR;v}3%cbc3a5(%$fJf?V0<;dqDDaT8z-F_-cDqHV(?PjhMy*y8Wu{ar zArgteVzHG%7h=dD%?l}ZKKY!<0hN~{6?-)6Hx$*UIN z;be-%qG+5O<@rAq;AzTaGDxS>LWCErY5}|54u%`&q3(9On9t{83J!+@cDo(c>$O-5 zJn!vxTTB6q@RTWeX#pNaMpi({Uj?KEtX3-+Spg+~xdObIFtP$l{wi>}0%o%rjI4l? zmliOYOkiXMl>Fri7>!0SvI0u}Dj+RjFc`iP2#3R>;y)3f`vEvh+^P|Hsn_do3EtE1 zxL17q=0oXzzyBe?2Ub2A-Q3)WqZ{x4oVn+fs*4F#@RXRIRQOOn63@?eHPh*IkEz1f u^bFq%eK~22%M0Q?eSbqd`Ts7be}nHI^^IR&ueEsq0000 + + + + + + + + + + + + + + + + @@ -2246,9 +2272,18 @@ class="org.eclipse.pde.internal.ui.shared.target.TargetReferenceLocationWizard" icon="icons/obj16/target_profile_xml_obj.png" id="org.eclipse.pde.ui.TargetReferenceProvisioner" - name="%locationProvider.name"> + name="%locationProvider.reference.name"> + + %locationProvider.reference.description + + + - %locationProvider.description + %locationProvider.repository.description diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPlugin.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPlugin.java index b9d12d6edc..4430ef44e8 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPlugin.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPlugin.java @@ -28,6 +28,7 @@ import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.pde.internal.core.PDEPreferencesManager; import org.eclipse.pde.internal.ui.launcher.PDELogFileProvider; +import org.eclipse.pde.internal.ui.shared.target.RepositoryBundleContainerAdapterFactory; import org.eclipse.pde.internal.ui.shared.target.TargetReferenceBundleContainerAdapterFactory; import org.eclipse.pde.internal.ui.shared.target.TargetStatus; import org.eclipse.pde.internal.ui.util.SWTUtil; @@ -233,6 +234,7 @@ public void stop(BundleContext context) throws Exception { Utilities.shutdown(); super.stop(context); TargetReferenceBundleContainerAdapterFactory.LABEL_PROVIDER.dispose(); + RepositoryBundleContainerAdapterFactory.LABEL_PROVIDER.dispose(); } public PDELabelProvider getLabelProvider() { diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPluginImages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPluginImages.java index 289bf3aabc..265f889b23 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPluginImages.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEPluginImages.java @@ -42,6 +42,7 @@ public class PDEPluginImages { private static final String PATH_OBJ = ICONS_PATH + "obj16/"; //$NON-NLS-1$ private static final String PATH_VIEW = ICONS_PATH + "view16/"; //$NON-NLS-1$ + private static final String PATH_DVIEW = ICONS_PATH + "dview16/"; //$NON-NLS-1$ private static final String PATH_LCL = ICONS_PATH + "elcl16/"; //$NON-NLS-1$ private static final String PATH_LCL_DISABLED = ICONS_PATH + "dlcl16/"; //$NON-NLS-1$ private static final String PATH_TOOL = ICONS_PATH + "etool16/"; //$NON-NLS-1$ @@ -316,6 +317,7 @@ public class PDEPluginImages { * View */ public static final ImageDescriptor DESC_ARGUMENT_TAB = create(PATH_VIEW, "variable_tab.png"); //$NON-NLS-1$ + public static final ImageDescriptor DESC_TARGET_REPO = create(PATH_DVIEW, "memory_view.png"); //$NON-NLS-1$ private static ImageDescriptor create(String prefix, String name) { return ImageDescriptor.createFromURL(makeImageURL(prefix, name)); diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditRepositoryContainerPage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditRepositoryContainerPage.java new file mode 100644 index 0000000000..667d4d516a --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditRepositoryContainerPage.java @@ -0,0 +1,290 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + * Christoph Läubrich - Bug 577184 - [target] Allow references to other targets inside a target-file + * Christoph Läubrich - add support for repositories + *******************************************************************************/ +package org.eclipse.pde.internal.ui.shared.target; + +import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.ui.StringVariableSelectionDialog; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.osgi.util.NLS; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.internal.core.target.RemoteTargetHandle; +import org.eclipse.pde.internal.core.target.RepositoryBundleContainer; +import org.eclipse.pde.internal.ui.PDEPlugin; +import org.eclipse.pde.internal.ui.SWTFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; + +/** + * Wizard page for creating a new directory bundle container. + * + * @see AddBundleContainerWizard + * @see AddBundleContainerSelectionPage + * @see ITargetLocation + */ +public class EditRepositoryContainerPage extends WizardPage implements IEditBundleContainerPage { + + /** + * How long to wait before validating the directory + */ + protected static final int TYPING_DELAY = 200; + + protected Combo furiLocation; + protected RepositoryBundleContainer fContainer; + + /** + * Dialog settings key for the most recent location + */ + private static final String SETTINGS_LOCATION_1 = "location1"; //$NON-NLS-1$ + + /** + * Dialog settings key for the second most recent location + */ + private static final String SETTINGS_LOCATION_2 = "location2"; //$NON-NLS-1$ + + /** + * Dialog settings key for the third most recent location + */ + private static final String SETTINGS_LOCATION_3 = "location3"; //$NON-NLS-1$ + + public EditRepositoryContainerPage(RepositoryBundleContainer bundleContainer) { + super("EditRepositoryContainerPage"); //$NON-NLS-1$ + fContainer = bundleContainer; + } + + @Override + public void createControl(Composite parent) { + setMessage(getDefaultMessage()); + setTitle(getDefaultTitle()); + setPageComplete(false); + Composite comp = SWTFactory.createComposite(parent, 1, 1, GridData.FILL_BOTH, 0, 0); + createLocationArea(comp); + setControl(comp); + initializeInputFields(fContainer); + } + + /** + * @return the default title for this wizard page + */ + protected String getDefaultTitle() { + if (fContainer == null) { + return Messages.EditRepositoryContainerPage_Add_Title; + } + return Messages.EditRepositoryContainerPage_Edit_Title; + } + + /** + * @return the default message for this wizard page + */ + protected String getDefaultMessage() { + return Messages.EditRepositoryContainerPage_Message; + } + + /** + * Creates the area at the top of the page. Contains an entry form for a + * location path. This method may be overridden by subclasses to provide + * custom widgets + * + * @param parent + * parent composite + */ + protected void createLocationArea(Composite parent) { + Composite locationComp = SWTFactory.createComposite(parent, 2, 1, GridData.FILL_HORIZONTAL, 0, 0); + + SWTFactory.createLabel(locationComp, Messages.AddDirectoryContainerPage_2, 1); + + furiLocation = SWTFactory.createCombo(locationComp, SWT.BORDER, 1, getLocationComboItems()); + furiLocation.addModifyListener(e -> { + setPageComplete(validateInput()); + }); + try { + String location = fContainer != null ? fContainer.getLocation(false) : ""; //$NON-NLS-1$ + furiLocation.setText(location); + } catch (CoreException e) { + setErrorMessage(e.getMessage()); + } + + Composite buttonComp = SWTFactory.createComposite(locationComp, 2, 2, GridData.CENTER, 0, 0); + GridData gd = (GridData) buttonComp.getLayoutData(); + gd.horizontalAlignment = SWT.RIGHT; + + Button variablesButton = SWTFactory.createPushButton(buttonComp, Messages.EditDirectoryContainerPage_1, null); + variablesButton.addSelectionListener(widgetSelectedAdapter(e -> { + StringVariableSelectionDialog dialog = new StringVariableSelectionDialog(getShell()); + dialog.open(); + String variable = dialog.getVariableExpression(); + if (variable != null) { + furiLocation.setText(furiLocation.getText() + variable); + } + })); + } + + /** + * Initializes the fields use to describe the container. They should be + * filled in using the given container or set to default values if the + * container is null. + * + * @param container + * bundle container being edited, possibly null + */ + protected void initializeInputFields(ITargetLocation container) { + try { + String currentLocation = fContainer != null ? fContainer.getLocation(false) : ""; //$NON-NLS-1$ + boolean found = false; + String[] items = furiLocation.getItems(); + for (String item : items) { + if (item.equals(currentLocation)) { + found = true; + break; + } + } + if (!found) { + furiLocation.add(currentLocation); + } + furiLocation.setText(currentLocation); + + setPageComplete(validateInput()); + } catch (CoreException e) { + PDEPlugin.log(e); + } + } + + @Override + public boolean isPageComplete() { + return !furiLocation.getText().isBlank(); + } + + /** + * @return a list of previous locations from settings plus the default + * location + */ + private String[] getLocationComboItems() { + List previousLocations = new ArrayList<>(4); + IDialogSettings settings = getDialogSettings(); + if (settings != null) { + String location = settings.get(SETTINGS_LOCATION_1); + if (location != null) { + previousLocations.add(location); + } + location = settings.get(SETTINGS_LOCATION_2); + if (location != null) { + previousLocations.add(location); + } + location = settings.get(SETTINGS_LOCATION_3); + if (location != null) { + previousLocations.add(location); + } + } + return previousLocations.toArray(new String[previousLocations.size()]); + } + + @Override + public void storeSettings() { + String newLocation = furiLocation.getText().trim(); + + int length = newLocation.length(); + if (length > 0 && newLocation.charAt(length - 1) == File.separatorChar) { + newLocation = newLocation.substring(0, length - 1); + } + String[] items = furiLocation.getItems(); + for (String item : items) { + if (item.equals(newLocation)) { + // Already have this location stored + return; + } + } + IDialogSettings settings = getDialogSettings(); + if (settings != null) { + String location = settings.get(SETTINGS_LOCATION_2); + if (location != null) { + settings.put(SETTINGS_LOCATION_3, location); + } + location = settings.get(SETTINGS_LOCATION_1); + if (location != null) { + settings.put(SETTINGS_LOCATION_2, location); + } + settings.put(SETTINGS_LOCATION_1, newLocation); + } + } + + @Override + public RepositoryBundleContainer getBundleContainer() { + String rawUri = furiLocation != null & !furiLocation.isDisposed() ? furiLocation.getText().trim() : ""; //$NON-NLS-1$ + return new RepositoryBundleContainer(rawUri, fContainer == null ? List.of() : fContainer.getRequirements()); + } + + /** + * Validate the input fields before a container is created/edited. The + * page's enablement, message and completion should be updated. + * + * @return whether the finish button should be enabled and container + * creation should continue + */ + protected boolean validateInput() { + if (furiLocation.isDisposed()) + return false; + + // Check if the text field is blank + if (furiLocation.getText().trim().length() == 0) { + setMessage(getDefaultMessage()); + return false; + } + + // Resolve any variables + URI location; + try { + location = RemoteTargetHandle.getEffectiveUri(furiLocation.getText().trim()); + } catch (CoreException e) { + setMessage(e.getMessage(), IMessageProvider.WARNING); + return true; + } catch (URISyntaxException e) { + setMessage(e.getMessage(), IMessageProvider.ERROR); + return false; + } + try { + // and be converted to an URL + URL url = location.toURL(); + if ("file".equalsIgnoreCase(url.getProtocol())) { //$NON-NLS-1$ + File file = new File(location); + if (!file.isFile()) { + setMessage(NLS.bind(Messages.EditTargetContainerPage_Not_A_File, file.getAbsolutePath()), + IMessageProvider.ERROR); + return true; + } + } + } catch (MalformedURLException | RuntimeException e) { + setMessage(e.getMessage(), IMessageProvider.ERROR); + return false; + } + setMessage(getDefaultMessage()); + return true; + } + +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java index 7e44530752..68fe25980e 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java @@ -152,6 +152,11 @@ public class Messages extends NLS { public static String EditTargetContainerPage_Edit_Title; public static String EditTargetContainerPage_Message; public static String EditTargetContainerPage_Not_A_File; + public static String EditRepositoryContainerPage_Message; + public static String EditRepositoryContainerPage_Add_Title; + public static String EditRepositoryContainerPage_Edit_Title; + public static String SelectRepositoryContentPage_Title; + public static String SelectRepositoryContentPage_Description; static { // initialize resource bundle diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryBundleContainerAdapterFactory.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryBundleContainerAdapterFactory.java new file mode 100644 index 0000000000..46e804f28e --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryBundleContainerAdapterFactory.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.ui.shared.target; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdapterFactory; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.wizard.IWizard; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.internal.core.target.RepositoryBundleContainer; +import org.eclipse.pde.internal.launching.IPDEConstants; +import org.eclipse.pde.internal.ui.PDEPluginImages; +import org.eclipse.pde.ui.target.ITargetLocationHandler; +import org.eclipse.swt.graphics.Image; +import org.osgi.resource.Requirement; + +public class RepositoryBundleContainerAdapterFactory implements IAdapterFactory { + + private static final Object[] EMPTY_OBJECTS = new Object[0]; + + public static final ILabelProvider LABEL_PROVIDER = new LabelProvider() { + + private Image repositoryImage; + private Image requirementImage; + + @Override + public String getText(Object element) { + if (element instanceof RepositoryBundleContainer container) { + try { + return container.getLocation(true); + } catch (CoreException e) { + return container.getUri(); + } + } + if (element instanceof RequirementNode requirement) { + return requirement.requirement().toString(); + } + return null; + } + + @Override + public Image getImage(Object element) { + if (element instanceof RepositoryBundleContainer) { + if (repositoryImage == null) { + repositoryImage = PDEPluginImages.DESC_TARGET_REPO.createImage(); + } + return repositoryImage; + } + if (element instanceof RequirementNode) { + if (requirementImage == null) { + requirementImage = PDEPluginImages.DESC_FILTER.createImage(); + } + return requirementImage; + } + return null; + } + + @Override + public void dispose() { + super.dispose(); + if (repositoryImage != null) { + repositoryImage.dispose(); + repositoryImage = null; + } + } + }; + + private static final ITargetLocationHandler LOCATION_HANDLER = new ITargetLocationHandler() { + + @Override + public IStatus reload(ITargetDefinition target, ITargetLocation[] targetLocations, IProgressMonitor monitor) { + for (ITargetLocation location : targetLocations) { + if (location instanceof RepositoryBundleContainer container) { + container.reload(); + } + } + return Status.OK_STATUS; + } + + @Override + public IWizard getEditWizard(ITargetDefinition target, TreePath treePath) { + Object segment = treePath.getLastSegment(); + if (segment instanceof RequirementNode node) { + // TODO maybe we can support a requirements editor? + segment = node.container; + } + if (segment instanceof RepositoryBundleContainer container) { + RepositoryLocationWizard wizard = new RepositoryLocationWizard(); + wizard.setTarget(target); + wizard.setBundleContainer(container); + return wizard; + } + return null; + } + + @Override + public boolean canEdit(ITargetDefinition target, TreePath treePath) { + Object segment = treePath.getLastSegment(); + return segment instanceof RepositoryBundleContainer || segment instanceof RequirementNode; + } + + public boolean canRemove(ITargetDefinition target, TreePath treePath) { + return treePath.getLastSegment() instanceof RequirementNode; + } + + public IStatus remove(ITargetDefinition target, TreePath[] treePaths) { + boolean reload = false; + for (TreePath path : treePaths) { + Object lastSegment = path.getLastSegment(); + if (lastSegment instanceof RequirementNode node) { + RepositoryBundleContainer container = node.container; + RepositoryBundleContainer newContainer = new RepositoryBundleContainer(container.getUri(), + container.getRequirements().stream().filter(req -> req != node.requirement()).toList()); + ITargetLocation[] targetLocations = target.getTargetLocations(); + for (int i = 0; i < targetLocations.length; i++) { + ITargetLocation loc = targetLocations[i]; + if (loc == container) { + targetLocations[i] = newContainer; + } + + } + } + } + return reload + ? new Status(IStatus.OK, IPDEConstants.UI_PLUGIN_ID, ITargetLocationHandler.STATUS_FORCE_RELOAD, + "reloaded", null) //$NON-NLS-1$ + : Status.OK_STATUS; + } + }; + + private static final ITreeContentProvider TREE_CONTENT_PROVIDER = new ITreeContentProvider() { + + @Override + public boolean hasChildren(Object element) { + if (element instanceof RepositoryBundleContainer container) { + return !container.getRequirements().isEmpty(); + } + return false; + } + + @Override + public Object getParent(Object element) { + if (element instanceof RequirementNode node) { + return node.container; + } + return null; + } + + @Override + public Object[] getElements(Object inputElement) { + return EMPTY_OBJECTS; // will never be called... + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof RepositoryBundleContainer container) { + return container.getRequirements().stream().map(req -> new RequirementNode(req, container)).toArray(); + } + return EMPTY_OBJECTS; + } + }; + + @Override + public T getAdapter(Object adaptableObject, Class adapterType) { + if (adaptableObject instanceof RepositoryBundleContainer + || adaptableObject instanceof RequirementNode) { + if (adapterType == ILabelProvider.class) { + return adapterType.cast(LABEL_PROVIDER); + } + if (adapterType == ITargetLocationHandler.class) { + return adapterType.cast(LOCATION_HANDLER); + } + if (adapterType == ITreeContentProvider.class) { + return adapterType.cast(TREE_CONTENT_PROVIDER); + } + } + return null; + } + + @Override + public Class[] getAdapterList() { + return new Class[] { ILabelProvider.class, ITargetLocationHandler.class, ITreeContentProvider.class }; + } + + /** + * Simple wrapper class to identify it uniquly when adaption take place and + * to record the parent + */ + public static final record RequirementNode(Requirement requirement, RepositoryBundleContainer container) { + + } + + +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryLocationWizard.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryLocationWizard.java new file mode 100644 index 0000000000..65018a8083 --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/RepositoryLocationWizard.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.ui.shared.target; + +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.internal.core.target.RepositoryBundleContainer; +import org.eclipse.pde.ui.target.ITargetLocationWizard; + +public class RepositoryLocationWizard extends Wizard implements ITargetLocationWizard { + + private EditRepositoryContainerPage repositoryPage; + private ITargetLocation wizardLocation; + private ITargetDefinition target; + private RepositoryBundleContainer bundleContainer; + private SelectRepositoryContentPage selectionPage; + + public RepositoryLocationWizard() { + setNeedsProgressMonitor(true); + } + + @Override + public void setTarget(ITargetDefinition target) { + this.target = target; + } + + @Override + public void addPages() { + addPage(repositoryPage = new EditRepositoryContainerPage(bundleContainer)); + addPage(selectionPage = new SelectRepositoryContentPage(repositoryPage)); + + setWindowTitle(repositoryPage.getDefaultTitle()); + } + + @Override + public ITargetLocation[] getLocations() { + if (wizardLocation == null) { + return new ITargetLocation[0]; + } + return new ITargetLocation[] { wizardLocation }; + } + + @Override + public boolean performFinish() { + wizardLocation = selectionPage.getBundleContainer(); + if (target != null && bundleContainer != null) { + ITargetLocation[] locations = target.getTargetLocations(); + for (int i = 0; i < locations.length; i++) { + ITargetLocation location = locations[i]; + if (location == bundleContainer) { + locations[i] = wizardLocation; + } + } + target.setTargetLocations(locations); + } + return true; + } + + public void setBundleContainer(RepositoryBundleContainer bundleContainer) { + this.bundleContainer = bundleContainer; + } + +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/SelectRepositoryContentPage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/SelectRepositoryContentPage.java new file mode 100644 index 0000000000..c1646697a0 --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/SelectRepositoryContentPage.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.ui.shared.target; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.ICheckStateProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.internal.core.target.RepositoryBundleContainer; +import org.eclipse.pde.internal.ui.PDEPluginImages; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +import aQute.bnd.osgi.repository.ResourcesRepository; +import aQute.bnd.osgi.resource.CapReqBuilder; +import aQute.bnd.osgi.resource.ResourceUtils; +import aQute.bnd.osgi.resource.ResourceUtils.IdentityCapability; + +public class SelectRepositoryContentPage extends WizardPage implements IEditBundleContainerPage { + + private EditRepositoryContainerPage repositoryPage; + private String lastLocation; + private CheckboxTableViewer viewer; + private Collection requirements; + private Set selected = ConcurrentHashMap.newKeySet(); + + protected SelectRepositoryContentPage(EditRepositoryContainerPage repositoryPage) { + super("SelectRepositoryContentPage"); //$NON-NLS-1$ + this.repositoryPage = repositoryPage; + setTitle(Messages.SelectRepositoryContentPage_Title); + setDescription(Messages.SelectRepositoryContentPage_Description); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + try { + RepositoryBundleContainer container = repositoryPage.getBundleContainer(); + if (requirements == null) { + // only init the requirements once... + requirements = new HashSet<>(container.getRequirements()); + } + String location = container.getLocation(true); + if (lastLocation != location) { + // Load the repository!! + getContainer().run(true, true, monitor -> { + try { + ResourcesRepository repository = container.getRepository(monitor); + selected.clear(); + List resources = repository.getResources(); + repository.findProviders(requirements).values().stream().flatMap(Collection::stream) + .map(Capability::getResource).distinct().forEach(selected::add); + if (viewer != null) { + viewer.getControl().getDisplay().execute(() -> { + if (viewer.getControl().isDisposed()) { + return; + } + viewer.setInput(resources); + }); + } + } catch (CoreException e) { + throw new InvocationTargetException(e); + } + }); + } + setErrorMessage(null); + } catch (CoreException e) { + setErrorMessage(e.getStatus().getMessage()); + } catch (InvocationTargetException e) { + setErrorMessage(e.getMessage()); + } catch (InterruptedException e) { + return; + } + } + super.setVisible(visible); + } + + @Override + public void createControl(Composite parent) { + + viewer = CheckboxTableViewer.newCheckList(parent, SWT.NONE); + viewer.setCheckStateProvider(new ICheckStateProvider() { + + @Override + public boolean isGrayed(Object element) { + return false; + } + + @Override + public boolean isChecked(Object element) { + if (element instanceof Resource resource) { + return selected.contains(resource); + } + return false; + } + }); + viewer.setContentProvider(ArrayContentProvider.getInstance()); + viewer.setLabelProvider(new LabelProvider() { + private Image pluginImage; + + @Override + public String getText(Object element) { + if (element instanceof Resource resource) { + return resource.toString(); + } + return ""; //$NON-NLS-1$ + } + + @Override + public Image getImage(Object element) { + if (element instanceof Resource resource) { + if (ResourceUtils.getBundleCapability(resource) != null) { + if (pluginImage == null) { + pluginImage = PDEPluginImages.DESC_PLUGIN_OBJ.createImage(); + viewer.getControl().addDisposeListener(e -> pluginImage.dispose()); + } + return pluginImage; + } + } + return null; + } + }); + viewer.addCheckStateListener(new ICheckStateListener() { + + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + Object element = event.getElement(); + if (element instanceof Resource resource) { + Requirement requirement = getRequirement(resource); + if (requirement != null) { + if (event.getChecked()) { + selected.add(resource); + requirements.add(requirement); + } else { + selected.remove(resource); + requirements.remove(requirement); + } + } + } + } + }); + setControl(viewer.getControl()); + } + + protected Requirement getRequirement(Resource resource) { + IdentityCapability identity = ResourceUtils.getIdentityCapability(resource); + String v = identity.version().toString(); + return CapReqBuilder.createSimpleRequirement(identity.getNamespace(), identity.osgi_identity(), + String.format("[%s,%s]", v, v)) //$NON-NLS-1$ + .setResource(resource).buildRequirement(); + } + + @Override + public ITargetLocation getBundleContainer() { + RepositoryBundleContainer container = repositoryPage.getBundleContainer(); + return new RepositoryBundleContainer(container.getUri(), + requirements == null ? container.getRequirements() : List.copyOf(requirements)); + } + + @Override + public void storeSettings() { + + } + +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties index 83a8650ae7..84a8f4dee4 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties @@ -143,5 +143,10 @@ UpdateTargetJob_UpdateJobName=Update Target Definition UpdateTargetJob_UpdatingTarget=Updating Target EditTargetContainerPage_Add_Title=Create target reference EditTargetContainerPage_Edit_Title=Edit target reference -EditTargetContainerPage_Message=Please enter an URI to the target to be referenced -EditTargetContainerPage_Not_A_File={0} is not a file \ No newline at end of file +EditTargetContainerPage_Message=Please enter a URI to the target to be referenced +EditTargetContainerPage_Not_A_File={0} is not a file +EditRepositoryContainerPage_Message=Please enter a URI that points to the repository +EditRepositoryContainerPage_Add_Title=Create OSGi Repository Location +EditRepositoryContainerPage_Edit_Title=Edit OSGi Repository Location +SelectRepositoryContentPage_Title=Select Content +SelectRepositoryContentPage_Description=Select the content that should be included in this location \ No newline at end of file