diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.classpath b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.classpath
new file mode 100644
index 00000000000..375961e4d61
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.gitignore b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.gitignore
new file mode 100644
index 00000000000..14435d8efa9
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.gitignore
@@ -0,0 +1 @@
+OSGI-INF/org.eclipse.*.xml
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.project b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.project
new file mode 100644
index 00000000000..c4b531be21f
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.project
@@ -0,0 +1,34 @@
+
+
+ org.eclipse.core.runtime.bundlegroups
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.pde.api.tools.apiAnalysisBuilder
+
+
+
+
+
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+ org.eclipse.pde.api.tools.apiAnalysisNature
+
+
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.resources.prefs b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.runtime.prefs b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 00000000000..5a0ad22d2a7
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.core.prefs b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..3a8fc308afb
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,126 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=17
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=warning
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=warning
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=ignore
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=disabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=disabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=ignore
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=error
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=error
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=error
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=17
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.ui.prefs b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 00000000000..81f11511252
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,147 @@
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+sp_cleanup.add_all=false
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.also_simplify_lambda=true
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.array_with_curly=false
+sp_cleanup.arrays_fill=false
+sp_cleanup.bitwise_conditional_expression=false
+sp_cleanup.boolean_literal=false
+sp_cleanup.boolean_value_rather_than_comparison=true
+sp_cleanup.break_loop=false
+sp_cleanup.collection_cloning=false
+sp_cleanup.comparing_on_criteria=false
+sp_cleanup.comparison_statement=false
+sp_cleanup.controlflow_merge=false
+sp_cleanup.convert_functional_interfaces=true
+sp_cleanup.convert_to_enhanced_for_loop=true
+sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true
+sp_cleanup.convert_to_switch_expressions=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.do_while_rather_than_while=false
+sp_cleanup.double_negation=false
+sp_cleanup.else_if=false
+sp_cleanup.embedded_if=false
+sp_cleanup.evaluate_nullable=false
+sp_cleanup.extract_increment=false
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=true
+sp_cleanup.hash=false
+sp_cleanup.if_condition=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.instanceof=false
+sp_cleanup.instanceof_keyword=false
+sp_cleanup.invert_equals=false
+sp_cleanup.join=false
+sp_cleanup.lazy_logical_operator=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.map_cloning=false
+sp_cleanup.merge_conditional_blocks=false
+sp_cleanup.multi_catch=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.no_string_creation=false
+sp_cleanup.no_super=false
+sp_cleanup.number_suffix=false
+sp_cleanup.objects_equals=false
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false
+sp_cleanup.operand_factorization=false
+sp_cleanup.organize_imports=true
+sp_cleanup.overridden_assignment=false
+sp_cleanup.overridden_assignment_move_decl=true
+sp_cleanup.plain_replacement=false
+sp_cleanup.precompile_regex=false
+sp_cleanup.primitive_comparison=false
+sp_cleanup.primitive_parsing=false
+sp_cleanup.primitive_rather_than_wrapper=true
+sp_cleanup.primitive_serialization=false
+sp_cleanup.pull_out_if_from_if_else=false
+sp_cleanup.pull_up_assignment=false
+sp_cleanup.push_down_negation=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=true
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.reduce_indentation=false
+sp_cleanup.redundant_comparator=false
+sp_cleanup.redundant_falling_through_block_end=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_modifiers=false
+sp_cleanup.remove_redundant_semicolons=true
+sp_cleanup.remove_redundant_type_arguments=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_array_creation=false
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=true
+sp_cleanup.remove_unused_imports=true
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_method_parameters=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.replace_deprecated_calls=false
+sp_cleanup.return_expression=false
+sp_cleanup.simplify_lambda_expression_and_method_ref=true
+sp_cleanup.single_used_field=false
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.standard_comparison=false
+sp_cleanup.static_inner_class=false
+sp_cleanup.strictly_equal_or_different=false
+sp_cleanup.stringbuffer_to_stringbuilder=false
+sp_cleanup.stringbuilder=false
+sp_cleanup.stringbuilder_for_local_vars=true
+sp_cleanup.stringconcat_stringbuffer_stringbuilder=false
+sp_cleanup.stringconcat_to_textblock=false
+sp_cleanup.substring=false
+sp_cleanup.switch=false
+sp_cleanup.system_property=false
+sp_cleanup.system_property_boolean=false
+sp_cleanup.system_property_file_encoding=false
+sp_cleanup.system_property_file_separator=false
+sp_cleanup.system_property_line_separator=false
+sp_cleanup.system_property_path_separator=false
+sp_cleanup.ternary_operator=false
+sp_cleanup.try_with_resource=false
+sp_cleanup.unlooped_while=false
+sp_cleanup.unreachable_block=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_autoboxing=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_directly_map_method=false
+sp_cleanup.use_lambda=true
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_string_is_blank=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_unboxing=false
+sp_cleanup.use_var=false
+sp_cleanup.useless_continue=false
+sp_cleanup.useless_return=false
+sp_cleanup.valueof_rather_than_instantiation=false
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.pde.ds.annotations.prefs b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.pde.ds.annotations.prefs
new file mode 100644
index 00000000000..5faf08b7d5c
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/.settings/org.eclipse.pde.ds.annotations.prefs
@@ -0,0 +1,7 @@
+dsVersion=V1_4
+eclipse.preferences.version=1
+enabled=true
+generateBundleActivationPolicyLazy=true
+path=OSGI-INF
+validationErrorLevel=error
+validationErrorLevel.missingImplicitUnbindMethod=error
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/META-INF/MANIFEST.MF b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..ca423529e0a
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/META-INF/MANIFEST.MF
@@ -0,0 +1,16 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: %pluginName
+Bundle-SymbolicName: org.eclipse.core.runtime.bundlegroups;singleton:=true
+Bundle-Version: 1.0.0.qualifier
+Bundle-Vendor: %providerName
+Bundle-Localization: plugin
+Require-Bundle: org.eclipse.core.runtime;bundle-version="3.29.0"
+Bundle-RequiredExecutionEnvironment: JavaSE-21
+Import-Package: javax.xml.parsers,
+ org.w3c.dom,
+ org.xml.sax,
+ org.xml.sax.helpers
+Bundle-ActivationPolicy: lazy
+Automatic-Module-Name: org.eclipse.core.runtime.bundlegroups
+Service-Component: OSGI-INF/org.eclipse.core.internal.runtime.bundlegroups.FeatureBundleGroupProvider.xml
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/about.html b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/about.html
new file mode 100644
index 00000000000..164f781a8fd
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/about.html
@@ -0,0 +1,36 @@
+
+
+
+
+About
+
+
+ About This Content
+
+ November 30, 2017
+ License
+
+
+ The Eclipse Foundation makes available all content in this plug-in
+ ("Content"). Unless otherwise indicated below, the Content
+ is provided to you under the terms and conditions of the Eclipse
+ Public License Version 2.0 ("EPL"). A copy of the EPL is
+ available at http://www.eclipse.org/legal/epl-2.0.
+ For purposes of the EPL, "Program" will mean the Content.
+
+
+
+ If you did not receive this Content directly from the Eclipse
+ Foundation, the Content is being redistributed by another party
+ ("Redistributor") and different terms and conditions may
+ apply to your use of any object code in the Content. Check the
+ Redistributor's license that was provided with the Content. If no such
+ license exists, contact the Redistributor. Unless otherwise indicated
+ below, the terms and conditions of the EPL still apply to any source
+ code in the Content and such source code may be obtained at http://www.eclipse.org.
+
+
+
+
\ No newline at end of file
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/build.properties b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/build.properties
new file mode 100644
index 00000000000..9b2f9357689
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/build.properties
@@ -0,0 +1,21 @@
+###############################################################################
+# Copyright (c) 2026 Lars Vogel 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:
+# Lars Vogel - initial API and implementation
+###############################################################################
+bin.includes = META-INF/,\
+ .,\
+ about.html,\
+ plugin.properties,\
+ OSGI-INF/
+src.includes = about.html
+source.. = src/
+output.. = bin/
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/plugin.properties b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/plugin.properties
new file mode 100644
index 00000000000..756b8e0331e
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/plugin.properties
@@ -0,0 +1,15 @@
+###############################################################################
+# Copyright (c) 2026 Lars Vogel 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:
+# Lars Vogel - initial API and implementation
+###############################################################################
+pluginName= Installed Feature Bundle Groups
+providerName= Eclipse.org
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java
new file mode 100644
index 00000000000..0169c63b8ee
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java
@@ -0,0 +1,256 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2017 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.net.URL;
+import java.util.Hashtable;
+
+import org.eclipse.core.runtime.IPath;
+
+
+/**
+ * The information within this object is obtained from the about INI file.
+ * This file resides within an install configurations directory and must be a
+ * standard java property file.
+ *
+ * This class is not intended to be instantiated or subclassed by clients.
+ *
+ */
+public final class AboutInfo {
+ private final static String INI_FILENAME = "about.ini"; //$NON-NLS-1$
+ private final static String PROPERTIES_FILENAME = "about.properties"; //$NON-NLS-1$
+ private final static String MAPPINGS_FILENAME = "about.mappings"; //$NON-NLS-1$
+
+ private final String featureId;
+ private String versionId = ""; //$NON-NLS-1$
+ private String featurePluginLabel;
+ private String providerName;
+ private String appName;
+ private URL windowImageURL;
+ private URL[] windowImagesURLs;
+ private URL aboutImageURL;
+ private URL featureImageURL;
+ private URL welcomePageURL;
+ private String aboutText;
+ private String welcomePerspective;
+ private String tipsAndTricksHref;
+
+
+ /*
+ * Create a new about info for a feature with the given id.
+ */
+ /* package */ AboutInfo(String featureId) {
+ super();
+ this.featureId = featureId;
+ }
+
+ /**
+ * Returns the configuration information for the feature with the
+ * given id.
+ *
+ * @param featureId the feature id
+ * @param versionId the version id (of the feature)
+ * @param pluginId the plug-in id
+ * @return the configuration information for the feature
+ */
+ public static AboutInfo readFeatureInfo(String featureId, String versionId, String pluginId) {
+// Assert.isNotNull(featureId);
+// Assert.isNotNull(versionId);
+// Assert.isNotNull(pluginId);
+ IniFileReader reader = new IniFileReader(featureId, pluginId, INI_FILENAME, PROPERTIES_FILENAME, MAPPINGS_FILENAME);
+ reader.load();
+// bug 78031
+// if (!status.isOK()) {
+// //return null;
+// return new AboutInfo(featureId); // dummy about info
+// }
+
+ AboutInfo info = new AboutInfo(featureId);
+ Hashtable runtimeMappings = new Hashtable<>();
+ runtimeMappings.put("{featureVersion}", versionId); //$NON-NLS-1$
+ info.versionId = versionId;
+ info.featurePluginLabel = reader.getFeaturePluginLabel();
+ info.providerName = reader.getProviderName();
+ info.appName = reader.getString("appName", true, runtimeMappings); //$NON-NLS-1$
+ info.aboutText = reader.getString("aboutText", true, runtimeMappings); //$NON-NLS-1$
+ info.windowImageURL = reader.getURL("windowImage"); //$NON-NLS-1$
+ // look for the newer array, but if its not there then use the older,
+ // single image definition
+ info.windowImagesURLs = reader.getURLs("windowImages"); //$NON-NLS-1$
+ info.aboutImageURL = reader.getURL("aboutImage"); //$NON-NLS-1$
+ info.featureImageURL = reader.getURL("featureImage"); //$NON-NLS-1$
+ info.welcomePageURL = reader.getURL("welcomePage"); //$NON-NLS-1$
+ info.welcomePerspective = reader.getString("welcomePerspective", false, runtimeMappings); //$NON-NLS-1$
+ info.tipsAndTricksHref = reader.getString("tipsAndTricksHref", false, runtimeMappings); //$NON-NLS-1$
+ return info;
+ }
+
+ /**
+ * Returns the URL for an image which can be shown in an "about" dialog
+ * for this product. Products designed to run "headless" typically would not
+ * have such an image.
+ *
+ * @return the URL for an about image, or null if none
+ */
+ public URL getAboutImageURL() {
+ return aboutImageURL;
+ }
+
+ /**
+ * Returns the URL for an image which can be shown in an "about features"
+ * dialog. Products designed to run "headless" typically would not have such an image.
+ *
+ * @return the URL for a feature image, or null if none
+ */
+ public URL getFeatureImageURL() {
+ return featureImageURL;
+ }
+
+ /**
+ * Returns the simple name of the feature image file.
+ *
+ * @return the simple name of the feature image file,
+ * or null if none
+ */
+ public String getFeatureImageName() {
+ if (featureImageURL != null) {
+ IPath path = IPath.fromOSString(featureImageURL.getPath());
+ return path.lastSegment();
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns a label for the feature plugin, or null.
+ */
+ public String getFeatureLabel() {
+ return featurePluginLabel;
+ }
+
+ /**
+ * Returns the id for this feature.
+ *
+ * @return the feature id
+ */
+ public String getFeatureId() {
+ return featureId;
+ }
+
+ /**
+ * Returns the text to show in an "about" dialog for this product.
+ * Products designed to run "headless" typically would not have such text.
+ *
+ * @return the about text, or null if none
+ */
+ public String getAboutText() {
+ return aboutText;
+ }
+
+ /**
+ * Returns the application name or null.
+ * Note this is never shown to the user.
+ * It is used to initialize the SWT Display.
+ *
+ * On Motif, for example, this can be used
+ * to set the name used for resource lookup.
+ *
+ *
+ * @return the application name, or null
+ */
+ public String getAppName() {
+ return appName;
+ }
+
+ /**
+ * Returns the product name or null.
+ * This is shown in the window title and the About action.
+ *
+ * @return the product name, or null
+ */
+ public String getProductName() {
+ return featurePluginLabel;
+ }
+
+ /**
+ * Returns the provider name or null.
+ *
+ * @return the provider name, or null
+ */
+ public String getProviderName() {
+ return providerName;
+ }
+
+ /**
+ * Returns the feature version id.
+ *
+ * @return the version id of the feature
+ */
+ public String getVersionId() {
+ return versionId;
+ }
+
+ /**
+ * Returns a URL for the welcome page.
+ * Products designed to run "headless" typically would not have such an page.
+ *
+ * @return the welcome page, or null if none
+ */
+ public URL getWelcomePageURL() {
+ return welcomePageURL;
+ }
+
+ /**
+ * Returns the ID of a perspective in which to show the welcome page.
+ * May be null.
+ *
+ * @return the welcome page perspective id, or null if none
+ */
+ public String getWelcomePerspectiveId() {
+ return welcomePerspective;
+ }
+
+ /**
+ * Returns a String for the tips and trick href.
+ *
+ * @return the tips and tricks href, or null if none
+ */
+ public String getTipsAndTricksHref() {
+ return tipsAndTricksHref;
+ }
+
+ /**
+ * Returns the image url for the window image to use for this product.
+ * Products designed to run "headless" typically would not have such an image.
+ *
+ * @return the image url for the window image, or null if none
+ */
+ public URL getWindowImageURL() {
+ return windowImageURL;
+ }
+
+ /**
+ * Return an array of image URLs for the window images to use for
+ * this product. The expectations is that the elements will be the same
+ * image rendered at different sizes. Products designed to run "headless"
+ * typically would not have such images.
+ *
+ * @return an array of the image descriptors for the window images, or
+ * null if none
+ * @since 3.0
+ */
+ public URL[] getWindowImagesURLs() {
+ return windowImagesURLs;
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroup.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroup.java
new file mode 100644
index 00000000000..bd8fcc7c985
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroup.java
@@ -0,0 +1,262 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Lars Vogel 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:
+ * Lars Vogel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.StringTokenizer;
+
+import org.eclipse.core.runtime.IBundleGroup;
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.Platform;
+import org.osgi.framework.Bundle;
+
+/**
+ * A bundle group backed by an installed feature. The feature identity comes
+ * from platform.xml, everything else is read from the feature and its branding
+ * bundle on demand.
+ */
+public class FeatureBundleGroup implements IBundleGroup, IBundleGroupConstants, IProductConstants {
+
+ private static final String FEATURE_XML = "feature.xml"; //$NON-NLS-1$
+ private static final String FEATURE_PROPERTIES = "feature"; //$NON-NLS-1$
+ private static final String KEY_PREFIX = "%"; //$NON-NLS-1$
+ private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$
+
+ private final String id;
+ private final String version;
+ private final String pluginIdentifier;
+ private final String pluginVersion;
+ private final URI featureLocation;
+
+ private AboutInfo branding;
+ private FeatureManifest manifest;
+ private boolean fullyParsed;
+ private ResourceBundle resourceBundle;
+ private boolean resourceBundleLoaded;
+
+ FeatureBundleGroup(String id, String version, String pluginIdentifier, String pluginVersion, URI featureLocation) {
+ this.id = id;
+ this.version = version;
+ this.pluginIdentifier = pluginIdentifier;
+ this.pluginVersion = pluginVersion;
+ this.featureLocation = featureLocation;
+ }
+
+ /**
+ * A feature contributes a bundle group only when its branding bundle is
+ * installed.
+ */
+ boolean hasBranding() {
+ String bundleId = getFeaturePluginIdentifier();
+ return bundleId != null && Platform.getBundle(bundleId) != null;
+ }
+
+ private String getFeaturePluginIdentifier() {
+ return pluginIdentifier != null && !pluginIdentifier.isEmpty() ? pluginIdentifier : id;
+ }
+
+ @Override
+ public String getIdentifier() {
+ return id;
+ }
+
+ @Override
+ public String getVersion() {
+ return version;
+ }
+
+ @Override
+ public String getName() {
+ return getBranding().getProductName();
+ }
+
+ @Override
+ public String getProviderName() {
+ return getBranding().getProviderName();
+ }
+
+ @Override
+ public String getDescription() {
+ FeatureManifest parsed = fullParse();
+ return parsed == null ? null : getResourceString(parsed.description);
+ }
+
+ @Override
+ public Bundle[] getBundles() {
+ FeatureManifest parsed = fullParse();
+ if (parsed == null) {
+ return new Bundle[0];
+ }
+ List bundles = new ArrayList<>(parsed.pluginIds.size());
+ for (String pluginId : parsed.pluginIds) {
+ Bundle bundle = Platform.getBundle(pluginId);
+ if (bundle != null) {
+ bundles.add(bundle);
+ }
+ }
+ return bundles.toArray(new Bundle[bundles.size()]);
+ }
+
+ @Override
+ public String getProperty(String key) {
+ if (key == null) {
+ return null;
+ }
+ AboutInfo info = getBranding();
+
+ // IBundleGroupConstants
+ if (key.equals(FEATURE_IMAGE)) {
+ return toExternalForm(info.getFeatureImageURL());
+ } else if (key.equals(TIPS_AND_TRICKS_HREF)) {
+ return info.getTipsAndTricksHref();
+ } else if (key.equals(IBundleGroupConstants.WELCOME_PAGE)) { // same value is used by product and bundle group
+ return toExternalForm(info.getWelcomePageURL());
+ } else if (key.equals(WELCOME_PERSPECTIVE)) {
+ return info.getWelcomePerspectiveId();
+ } else if (key.equals(BRANDING_BUNDLE_ID)) {
+ return pluginIdentifier;
+ } else if (key.equals(BRANDING_BUNDLE_VERSION)) {
+ return pluginVersion;
+ } else if (key.equals(APP_NAME)) {
+ return info.getAppName();
+ } else if (key.equals(ABOUT_TEXT)) {
+ return info.getAboutText();
+ } else if (key.equals(ABOUT_IMAGE)) {
+ return toExternalForm(info.getAboutImageURL());
+ } else if (key.equals(WINDOW_IMAGE)) {
+ return toExternalForm(info.getWindowImageURL());
+ } else if (key.equals(WINDOW_IMAGES)) {
+ URL[] urls = info.getWindowImagesURLs();
+ if (urls == null) {
+ return null;
+ }
+ StringBuilder windowImagesURLs = new StringBuilder();
+ for (int i = 0; i < urls.length; i++) {
+ windowImagesURLs.append(urls[i].toExternalForm());
+ if (i != urls.length - 1) {
+ windowImagesURLs.append(',');
+ }
+ }
+ return windowImagesURLs.toString();
+ } else if (key.equals(LICENSE_HREF)) {
+ return getLicenseURL();
+ }
+
+ return null;
+ }
+
+ private static String toExternalForm(URL url) {
+ return url == null ? null : url.toExternalForm();
+ }
+
+ private synchronized AboutInfo getBranding() {
+ if (branding == null) {
+ branding = AboutInfo.readFeatureInfo(id, version, getFeaturePluginIdentifier());
+ }
+ return branding;
+ }
+
+ private String getLicenseURL() {
+ FeatureManifest parsed = fullParse();
+ if (parsed == null || parsed.licenseURL == null) {
+ return null;
+ }
+ String resolved = getResourceString(parsed.licenseURL);
+ if (resolved.startsWith("http://") || resolved.startsWith("https://")) { //$NON-NLS-1$ //$NON-NLS-2$
+ return resolved;
+ }
+ return featureLocation.resolve(resolved).toString();
+ }
+
+ private synchronized FeatureManifest fullParse() {
+ if (!fullyParsed) {
+ fullyParsed = true;
+ try {
+ manifest = FeatureXmlParser.parse(featureLocation.resolve(FEATURE_XML).toURL(), false);
+ } catch (MalformedURLException | IllegalArgumentException e) {
+ ILog.of(FeatureBundleGroup.class).error("Cannot locate the feature.xml of " + id, e); //$NON-NLS-1$
+ }
+ }
+ return manifest;
+ }
+
+ /*
+ * Resolves a %key reference against the feature.properties of this feature.
+ */
+ private String getResourceString(String value) {
+ if (value == null) {
+ return null;
+ }
+ String s = value.trim();
+ if (s.isEmpty() || !s.startsWith(KEY_PREFIX)) {
+ return value;
+ }
+ if (s.startsWith(KEY_DOUBLE_PREFIX)) {
+ return s.substring(1);
+ }
+
+ int index = s.indexOf(' ');
+ String key = index == -1 ? s : s.substring(0, index);
+ String defaultValue = index == -1 ? s : s.substring(index + 1);
+
+ ResourceBundle bundle = getResourceBundle();
+ if (bundle == null) {
+ return defaultValue;
+ }
+ try {
+ return bundle.getString(key.substring(1));
+ } catch (MissingResourceException e) {
+ return defaultValue;
+ }
+ }
+
+ private synchronized ResourceBundle getResourceBundle() {
+ if (!resourceBundleLoaded) {
+ resourceBundleLoaded = true;
+ try {
+ ClassLoader loader = new URLClassLoader(new URL[] { featureLocation.toURL() }, null);
+ resourceBundle = ResourceBundle.getBundle(FEATURE_PROPERTIES, getDefaultLocale(), loader);
+ } catch (MissingResourceException | MalformedURLException | IllegalArgumentException e) {
+ // features are not required to be translated
+ }
+ }
+ return resourceBundle;
+ }
+
+ private static Locale getDefaultLocale() {
+ String nl = Platform.getNL();
+ if (nl == null) {
+ return Locale.getDefault();
+ }
+ StringTokenizer locales = new StringTokenizer(nl, "_"); //$NON-NLS-1$
+ switch (locales.countTokens()) {
+ case 1:
+ return Locale.of(locales.nextToken());
+ case 2:
+ return Locale.of(locales.nextToken(), locales.nextToken());
+ case 3:
+ return Locale.of(locales.nextToken(), locales.nextToken(), locales.nextToken());
+ default:
+ return Locale.getDefault();
+ }
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroupProvider.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroupProvider.java
new file mode 100644
index 00000000000..c4f6fec1577
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureBundleGroupProvider.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Lars Vogel 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:
+ * Lars Vogel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.io.File;
+import java.io.FileFilter;
+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.internal.runtime.bundlegroups.platformxml.Configuration;
+import org.eclipse.core.internal.runtime.bundlegroups.platformxml.ConfigurationParser;
+import org.eclipse.core.internal.runtime.bundlegroups.platformxml.Feature;
+import org.eclipse.core.internal.runtime.bundlegroups.platformxml.Site;
+import org.eclipse.core.runtime.IBundleGroup;
+import org.eclipse.core.runtime.IBundleGroupProvider;
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.osgi.service.datalocation.Location;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Contributes the installed features as bundle groups. The feature list comes
+ * from the platform.xml written at provisioning time, so no plug-in has to be
+ * inspected to build it.
+ */
+@Component(service = IBundleGroupProvider.class)
+public class FeatureBundleGroupProvider implements IBundleGroupProvider {
+
+ private static final String PLATFORM_XML = "org.eclipse.update/platform.xml"; //$NON-NLS-1$
+ private static final String FEATURES = "features"; //$NON-NLS-1$
+ private static final String FEATURE_XML = "feature.xml"; //$NON-NLS-1$
+
+ private List cachedFeatures;
+ private long cachedTimestamp = -1;
+
+ @Override
+ public String getName() {
+ return Messages.BundleGroupProvider;
+ }
+
+ @Override
+ public synchronized IBundleGroup[] getBundleGroups() {
+ File platformXml = findPlatformXml();
+ long timestamp = platformXml == null ? 0 : platformXml.lastModified();
+ if (cachedFeatures == null || timestamp != cachedTimestamp) {
+ cachedFeatures = readFeatures(platformXml);
+ cachedTimestamp = timestamp;
+ }
+ // the branding bundle of a feature can be installed after the feature was read
+ return cachedFeatures.stream().filter(FeatureBundleGroup::hasBranding).toArray(IBundleGroup[]::new);
+ }
+
+ private static List readFeatures(File platformXml) {
+ URL installArea = getInstallArea();
+ List features = new ArrayList<>();
+ Configuration configuration = platformXml == null ? null
+ : ConfigurationParser.parse(platformXml, installArea);
+ if (configuration == null) {
+ collectFromInstallArea(installArea, features);
+ } else {
+ for (Site site : configuration.getSites()) {
+ collectFromSite(site, features);
+ }
+ }
+ return features;
+ }
+
+ private static void collectFromSite(Site site, List features) {
+ URI siteLocation = toURI(site.getUrl());
+ if (siteLocation == null) {
+ return;
+ }
+ for (Feature feature : site.getFeatures()) {
+ if (feature.getId() == null) {
+ continue;
+ }
+ String featureURL = feature.getUrl();
+ if (featureURL == null) {
+ featureURL = FEATURES + "/" + feature.getId() + "_" + feature.getVersion() + "/"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+ features.add(new FeatureBundleGroup(feature.getId(), feature.getVersion(), feature.getPluginIdentifier(),
+ feature.getPluginVersion(), siteLocation.resolve(featureURL)));
+ }
+ }
+
+ /*
+ * Without a platform.xml the installed features can only be found by looking at
+ * the features directory of the install area.
+ */
+ private static void collectFromInstallArea(URL installArea, List features) {
+ if (installArea == null || !"file".equals(installArea.getProtocol())) { //$NON-NLS-1$
+ return;
+ }
+ File featuresDir = new File(installArea.getFile().replace('/', File.separatorChar), FEATURES);
+ File[] dirs = featuresDir
+ .listFiles((FileFilter) f -> f.isDirectory() && new File(f, FEATURE_XML).exists());
+ if (dirs == null) {
+ return;
+ }
+ for (File dir : dirs) {
+ try {
+ FeatureManifest manifest = FeatureXmlParser.parse(new File(dir, FEATURE_XML).toURI().toURL(), true);
+ if (manifest != null) {
+ features.add(new FeatureBundleGroup(manifest.id, manifest.version, manifest.pluginIdentifier,
+ manifest.version, dir.toURI()));
+ }
+ } catch (MalformedURLException e) {
+ ILog.of(FeatureBundleGroupProvider.class).error("Cannot read the feature in " + dir, e); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /*
+ * The platform.xml of the configuration area, falling back to the one of a
+ * shared install.
+ */
+ private static File findPlatformXml() {
+ Location configuration = Platform.getConfigurationLocation();
+ if (configuration == null) {
+ return null;
+ }
+ File file = toPlatformXml(configuration);
+ if (file != null && file.exists()) {
+ return file;
+ }
+ Location parent = configuration.getParentLocation();
+ File parentFile = parent == null ? null : toPlatformXml(parent);
+ return parentFile != null && parentFile.exists() ? parentFile : null;
+ }
+
+ private static File toPlatformXml(Location location) {
+ URL url = location.getURL();
+ if (url == null || !"file".equals(url.getProtocol())) { //$NON-NLS-1$
+ return null;
+ }
+ return new File(url.getFile().replace('/', File.separatorChar), PLATFORM_XML);
+ }
+
+ private static URL getInstallArea() {
+ Location install = Platform.getInstallLocation();
+ return install == null ? null : install.getURL();
+ }
+
+ private static URI toURI(String encodedURI) {
+ if (encodedURI == null) {
+ return null;
+ }
+ try {
+ return new URI(encodedURI);
+ } catch (URISyntaxException e) {
+ ILog.of(FeatureBundleGroupProvider.class).error("Cannot resolve the site " + encodedURI, e); //$NON-NLS-1$
+ return null;
+ }
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureManifest.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureManifest.java
new file mode 100644
index 00000000000..9c8a52faa72
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureManifest.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Lars Vogel 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:
+ * Lars Vogel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The contents of a feature.xml file, as far as a bundle group needs them.
+ */
+class FeatureManifest {
+
+ String id;
+ String version;
+ String pluginIdentifier;
+ String application;
+ boolean primary;
+ String description;
+ String licenseURL;
+ final List pluginIds = new ArrayList<>();
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureXmlParser.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureXmlParser.java
new file mode 100644
index 00000000000..4a8f2cd27bb
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/FeatureXmlParser.java
@@ -0,0 +1,195 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2017 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.StringTokenizer;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.osgi.util.NLS;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Parses a feature.xml file. In header mode parsing stops after the feature
+ * element, which is all the platform.xml fallback needs.
+ */
+class FeatureXmlParser extends DefaultHandler {
+
+ private static final SAXException STOP_PARSING = new SAXException(""); //$NON-NLS-1$
+
+ private final boolean headerOnly;
+ private final FeatureManifest manifest = new FeatureManifest();
+ private final StringBuilder description = new StringBuilder();
+ private boolean isDescription;
+
+ static FeatureManifest parse(URL featureXML, boolean headerOnly) {
+ FeatureXmlParser handler = new FeatureXmlParser(headerOnly);
+ SAXParser parser;
+ try {
+ parser = createParser();
+ } catch (ParserConfigurationException | SAXException e) {
+ ILog.of(FeatureXmlParser.class).error("Failed to create a parser for " + featureXML, e); //$NON-NLS-1$
+ return null;
+ }
+ try (InputStream in = featureXML.openStream()) {
+ parser.parse(new InputSource(in), handler);
+ } catch (SAXException e) {
+ if (e != STOP_PARSING) {
+ return handler.manifest.id == null ? null : handler.manifest;
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ return handler.manifest.id == null ? null : handler.manifest;
+ }
+
+ @SuppressWarnings("restriction")
+ private static SAXParser createParser() throws ParserConfigurationException, SAXException {
+ return org.eclipse.core.internal.runtime.XmlProcessorFactory.createSAXParserWithErrorOnDOCTYPE(true);
+ }
+
+ private FeatureXmlParser(boolean headerOnly) {
+ this.headerOnly = headerOnly;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ if ("feature".equals(localName)) { //$NON-NLS-1$
+ processFeature(attributes);
+ if (headerOnly) {
+ throw STOP_PARSING;
+ }
+ } else if ("plugin".equals(localName)) { //$NON-NLS-1$
+ processPlugin(attributes);
+ } else if ("description".equals(localName)) { //$NON-NLS-1$
+ isDescription = true;
+ } else if ("license".equals(localName)) { //$NON-NLS-1$
+ manifest.licenseURL = attributes.getValue("url"); //$NON-NLS-1$
+ }
+ }
+
+ private void processFeature(Attributes attributes) {
+ String id = attributes.getValue("id"); //$NON-NLS-1$
+ String version = attributes.getValue("version"); //$NON-NLS-1$
+ if (id == null || id.trim().isEmpty() || version == null || version.trim().isEmpty()) {
+ ILog.of(FeatureXmlParser.class).warn(NLS.bind(Messages.FeatureParser_IdOrVersionInvalid, id, version));
+ return;
+ }
+ if (!isValidEnvironment(attributes)) {
+ return;
+ }
+ manifest.id = id;
+ manifest.version = version;
+ manifest.pluginIdentifier = attributes.getValue("plugin"); //$NON-NLS-1$
+ manifest.application = attributes.getValue("application"); //$NON-NLS-1$
+ manifest.primary = "true".equals(attributes.getValue("primary")); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ private void processPlugin(Attributes attributes) {
+ String id = attributes.getValue("id"); //$NON-NLS-1$
+ String version = attributes.getValue("version"); //$NON-NLS-1$
+ if (id == null || id.trim().isEmpty() || version == null || version.trim().isEmpty()) {
+ ILog.of(FeatureXmlParser.class).warn(NLS.bind(Messages.FeatureParser_IdOrVersionInvalid, id, version));
+ return;
+ }
+ if (!isValidEnvironment(attributes)) {
+ return;
+ }
+ manifest.pluginIds.add(id);
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ if (isDescription) {
+ description.append(ch, start, length);
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if ("description".equals(localName)) { //$NON-NLS-1$
+ isDescription = false;
+ manifest.description = description.toString().trim();
+ }
+ }
+
+ private static boolean isValidEnvironment(Attributes attributes) {
+ String os = attributes.getValue("os"); //$NON-NLS-1$
+ String ws = attributes.getValue("ws"); //$NON-NLS-1$
+ String arch = attributes.getValue("arch"); //$NON-NLS-1$
+ String nl = attributes.getValue("nl"); //$NON-NLS-1$
+ if (os != null && !isMatching(os, Platform.getOS())) {
+ return false;
+ }
+ if (ws != null && !isMatching(ws, Platform.getWS())) {
+ return false;
+ }
+ if (arch != null && !isMatching(arch, Platform.getOSArch())) {
+ return false;
+ }
+ if (nl != null && !isMatchingLocale(nl, Platform.getNL())) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isMatching(String candidateValues, String siteValues) {
+ if (siteValues == null) {
+ return false;
+ }
+ if ("*".equalsIgnoreCase(candidateValues)) { //$NON-NLS-1$
+ return true;
+ }
+ siteValues = siteValues.toUpperCase();
+ StringTokenizer stok = new StringTokenizer(candidateValues, ","); //$NON-NLS-1$
+ while (stok.hasMoreTokens()) {
+ String token = stok.nextToken().toUpperCase();
+ if (siteValues.contains(token)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isMatchingLocale(String candidateValues, String locale) {
+ if (locale == null) {
+ return false;
+ }
+ if ("*".equalsIgnoreCase(candidateValues)) { //$NON-NLS-1$
+ return true;
+ }
+ locale = locale.toUpperCase();
+ candidateValues = candidateValues.toUpperCase();
+ StringTokenizer stok = new StringTokenizer(candidateValues, ","); //$NON-NLS-1$
+ while (stok.hasMoreTokens()) {
+ String candidate = stok.nextToken();
+ if (locale.indexOf(candidate) == 0) {
+ return true;
+ }
+ if (candidate.indexOf(locale) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java
new file mode 100644
index 00000000000..bcedef3c09c
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2025 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+
+/**
+ * These constants define the set of properties that the UI expects to
+ * be available via IBundleGroup.getProperty(String).
+ *
+ * @since 3.0
+ * @see org.eclipse.core.runtime.IBundleGroup#getProperty(String)
+ */
+public interface IBundleGroupConstants {
+ /**
+ * An image which can be shown in an "about features" dialog (32x32).
+ */
+ public static final String FEATURE_IMAGE = "featureImage"; //$NON-NLS-1$
+
+ /**
+ * A help reference for the feature's tips and tricks page (optional).
+ */
+ public static final String TIPS_AND_TRICKS_HREF = "tipsAndTricksHref"; //$NON-NLS-1$
+
+ /**
+ * A URL for the feature's welcome page (special XML-based format)
+ * ($nl$/ prefix to permit locale-specific translations of entire file).
+ * Products designed to run "headless" typically would not have such a page.
+ */
+ public static final String WELCOME_PAGE = "welcomePage"; //$NON-NLS-1$
+
+ /**
+ * The id of a perspective in which to show the welcome page
+ * (optional).
+ */
+ public static final String WELCOME_PERSPECTIVE = "welcomePerspective"; //$NON-NLS-1$
+
+ /**
+ * The URL of the license page for the feature
+ * (optional).
+ */
+ public static final String LICENSE_HREF = "licenseHref"; //$NON-NLS-1$
+
+ /**
+ * The id of the bundle group's branding bundle.
+ *
+ * @since 3.3
+ */
+ public static final String BRANDING_BUNDLE_ID= "brandingBundleId"; //$NON-NLS-1$
+
+ /**
+ * The version of the bundle group's branding bundle.
+ *
+ * @since 3.3
+ */
+ public static final String BRANDING_BUNDLE_VERSION= "brandingBundleVersion"; //$NON-NLS-1$
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java
new file mode 100644
index 00000000000..7111b183518
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2005 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+/**
+ * These constants define the set of properties that the UI expects to
+ * be available via IProduct.getProperty(String).
+ *
+ * @since 3.0
+ * @see org.eclipse.core.runtime.IProduct#getProperty(String)
+ */
+public interface IProductConstants {
+ /**
+ * The application name, used to initialize the SWT Display. This
+ * value is distinct from the string displayed in the application
+ * title bar.
+ *
+ * E.g., On motif, this can be used to set the name used for
+ * resource lookup.
+ *
+ */
+ public static final String APP_NAME = "appName"; //$NON-NLS-1$
+
+ /**
+ * The text to show in an "about" dialog for this product.
+ * Products designed to run "headless" typically would not
+ * have such text.
+ */
+ public static final String ABOUT_TEXT = "aboutText"; //$NON-NLS-1$
+
+ /**
+ * An image which can be shown in an "about" dialog for this
+ * product. Products designed to run "headless" typically would not
+ * have such an image.
+ *
+ * A full-sized product image (no larger than 500x330 pixels) is
+ * shown without the "aboutText" blurb. A half-sized product image
+ * (no larger than 250x330 pixels) is shown with the "aboutText"
+ * blurb beside it.
+ */
+ public static final String ABOUT_IMAGE = "aboutImage"; //$NON-NLS-1$
+
+ /**
+ * An image to be used as the window icon for this product (16x16).
+ * Products designed to run "headless" typically would not have such an image.
+ *
+ * If the WINDOW_IMAGES property is given, then it supercedes
+ * this one.
+ *
+ */
+ public static final String WINDOW_IMAGE = "windowImage"; //$NON-NLS-1$
+
+ /**
+ * An array of one or more images to be used for this product. The
+ * expectation is that the array will contain the same image rendered
+ * at different sizes (16x16 and 32x32).
+ * Products designed to run "headless" typically would not have such images.
+ *
+ * If this property is given, then it supercedes WINDOW_IMAGE.
+ *
+ */
+ public static final String WINDOW_IMAGES = "windowImages"; //$NON-NLS-1$
+ /**
+ * A URL for the products's welcome page (special XML-based format)
+ * ($nl$/ prefix to permit locale-specific translations of entire file).
+ * Products designed to run "headless" typically would not have such a page.
+ */
+ public static final String WELCOME_PAGE = "welcomePage"; //$NON-NLS-1$
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java
new file mode 100644
index 00000000000..f4d48bef45d
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java
@@ -0,0 +1,389 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2017 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.text.MessageFormat; // Can't use ICU, possible launch problem?
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.MissingResourceException;
+import java.util.Properties;
+import java.util.PropertyResourceBundle;
+import java.util.StringTokenizer;
+
+import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.osgi.util.NLS;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+/**
+ * Reads the information found in an "INI" file. This file must be in a
+ * standard Java properties format. A properties file may also be provided
+ * to NL values in the INI file - values must start with the % prefix. A
+ * mapping file may also be provided that contains "fill-ins" for the
+ * properties file - format being "n = some text", where n is a number.
+ */
+public class IniFileReader {
+ private static final String PID = "org.eclipse.core.runtime.bundlegroups"; //$NON-NLS-1$
+ private static final Status OK_STATUS = new Status(IStatus.OK, PID, 0, "", null); //$NON-NLS-1$
+ private static final String KEY_PREFIX = "%"; //$NON-NLS-1$
+ private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$
+ private static final String NLS_TAG = "$nl$"; //$NON-NLS-1$
+
+ private final String featureId;
+ private final String pluginId;
+ private final String iniFilename;
+ private final String propertiesFilename;
+ private final String mappingsFilename;
+ private Properties ini = null;
+ private PropertyResourceBundle properties = null;
+ private Object[] mappings = null;
+ private Bundle bundle;
+
+ /**
+ * Creates an INI file reader that can parse the contents into key,value pairs.
+ *
+ * @param featureId the unique identifier of the feature, must not be null
+ * @param pluginId the unique identifier of the feature plug-in, must not be null
+ * @param iniFilename the INI file name, must not be null
+ * @param propertiesFilename the properties filename, can be null if not required
+ * @param mappingsFilename the mappings filename, can be null if not required
+ */
+ public IniFileReader(String featureId, String pluginId, String iniFilename, String propertiesFilename, String mappingsFilename) {
+ super();
+
+ if (featureId == null || pluginId == null || iniFilename == null) {
+ throw new IllegalArgumentException();
+ }
+
+ this.featureId = featureId;
+ this.pluginId = pluginId;
+ this.iniFilename = iniFilename;
+ this.propertiesFilename = propertiesFilename;
+ this.mappingsFilename = mappingsFilename;
+ }
+
+ /**
+ * Read the contents of the INI, properties, and mappings files.
+ * Does nothing if the content has already been read and parsed.
+ *
+ * @return an IStatus indicating the success or failure
+ * of reading and parsing the INI file content
+ */
+ public IStatus load() {
+ if (ini != null) {
+ return OK_STATUS;
+ }
+
+ // attempt to locate the corresponding plugin
+ bundle = Platform.getBundle(pluginId);
+ if (bundle == null || bundle.getState() == Bundle.UNINSTALLED || bundle.getState() == Bundle.INSTALLED) {
+ bundle = null; // make it null for other test down the road
+ String message = NLS.bind(Messages.IniFileReader_MissingDesc, featureId);
+ return new Status(IStatus.ERROR, PID, 0, message, null);
+ }
+
+ // Determine the ini file location
+ URL iniURL = null;
+ IOException ioe = null;
+ iniURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(iniFilename), null);
+ if (iniURL == null) {
+ String message = NLS.bind(Messages.IniFileReader_OpenINIError, iniFilename);
+ return new Status(IStatus.ERROR, PID, 0, message, ioe);
+ }
+
+ // Determine the properties file location
+ URL propertiesURL = null;
+ if (propertiesFilename != null && !propertiesFilename.isEmpty()) {
+ propertiesURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(propertiesFilename), null);
+ }
+
+ // Determine the mappings file location
+ URL mappingsURL = null;
+ if (mappingsFilename != null && mappingsFilename.length() > 0) {
+ mappingsURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(mappingsFilename), null);
+ }
+
+ // OK to pass null properties and/or mapping file
+ return load(iniURL, propertiesURL, mappingsURL);
+ }
+
+ /**
+ * Returns the string value for the given key, or null.
+ * The string value is NLS if requested.
+ *
+ * @return the string value for the given key, or null
+ */
+ public String getString(String key, boolean doNls, Hashtable runtimeMappings) {
+ if (ini == null) {
+ return null;
+ }
+ String value = ini.getProperty(key);
+ if (value != null && doNls) {
+ return getResourceString(value, runtimeMappings);
+ }
+ return value;
+ }
+
+
+ /**
+ * Returns a URL for the given key, or null.
+ *
+ * @return a URL for the given key, or null
+ */
+ public URL getURL(String key) {
+ if (ini == null) {
+ return null;
+ }
+
+ URL url = null;
+ String fileName = ini.getProperty(key);
+ if (fileName != null) {
+ if (bundle == null) {
+ return null;
+ }
+ url = FileLocator.find(bundle, IPath.fromOSString(fileName), null);
+ }
+ return url;
+ }
+
+ /**
+ * Returns a array of URL for the given key, or null. The
+ * property value should be a comma separated list of urls, tokens for
+ * which bundle cannot build an url will have a null entry.
+ *
+ * @param key name of the property containing the requested urls
+ * @return a URL for the given key, or null
+ * @since 3.0
+ */
+ public URL[] getURLs(String key) {
+ if (ini == null || bundle == null) {
+ return null;
+ }
+
+ String value = ini.getProperty(key);
+ if (value == null) {
+ return null;
+ }
+
+ StringTokenizer tokens = new StringTokenizer(value, ","); //$NON-NLS-1$
+ ArrayList array = new ArrayList<>(10);
+ while (tokens.hasMoreTokens()) {
+ String str = tokens.nextToken().trim();
+ array.add(FileLocator.find(bundle, IPath.fromOSString(str), null));
+ }
+
+ URL[] urls = new URL[array.size()];
+ array.toArray(urls);
+ return urls;
+ }
+
+ /**
+ * Returns the feature plugin label, or null.
+ *
+ * @return the feature plugin label, or null if none.
+ */
+ public String getFeaturePluginLabel() {
+ if (bundle == null) {
+ return null;
+ }
+ return bundle.getHeaders().get(Constants.BUNDLE_NAME);
+ }
+
+ /**
+ * Returns the provider name for this feature, or null.
+ *
+ * @return the provider name for this feature, or null
+ */
+ public String getProviderName() {
+ if (bundle == null) {
+ return null;
+ }
+ return bundle.getHeaders().get(Constants.BUNDLE_VENDOR);
+ }
+
+ /*
+ * Returns a resource string corresponding to the given argument
+ * value and bundle.
+ * If the argument value specifies a resource key, the string
+ * is looked up in the given resource bundle. If the argument does not
+ * specify a valid key, the argument itself is returned as the
+ * resource string. The key lookup is performed against the
+ * specified resource bundle. If a resource string
+ * corresponding to the key is not found in the resource bundle
+ * the key value, or any default text following the key in the
+ * argument value is returned as the resource string.
+ * A key is identified as a string begining with the "%" character.
+ * Note that the "%" character is stripped off prior to lookup
+ * in the resource bundle.
+ *
+ * For example, assume resource bundle plugin.properties contains
+ * name = Project Name
+ *
+ *
getResourceString("Hello World") returns "Hello World"
+ * getResourceString("%name") returns "Project Name"
+ * getResourceString("%name Hello World") returns "Project Name"
+ * getResourceString("%abcd Hello World") returns "Hello World"
+ * getResourceString("%abcd") returns "%abcd"
+ * getResourceString("%%name") returns "%name"
+ * getResourceString(null) returns null
+ *
+ *
+ *
+ * @param value the value or null
+ * @param runtimeMappings runtime mappings or null
+ * @return the resource string
+ */
+ public String getResourceString(String value, Hashtable runtimeMappings) {
+
+ if (value == null) {
+ return null;
+ }
+ String s = value.trim();
+
+ if (!s.startsWith(KEY_PREFIX)) {
+ return s;
+ }
+
+ if (s.startsWith(KEY_DOUBLE_PREFIX)) {
+ return s.substring(1);
+ }
+
+ int ix = s.indexOf(" "); //$NON-NLS-1$
+ String key = ix == -1 ? s : s.substring(0, ix);
+ String dflt = ix == -1 ? s : s.substring(ix + 1);
+
+ if (properties == null) {
+ return dflt;
+ }
+
+ String result = null;
+ try {
+ result = properties.getString(key.substring(1));
+ } catch (MissingResourceException e) {
+ return dflt;
+ }
+ if (runtimeMappings != null) {
+ for (Enumeration e = runtimeMappings.keys(); e.hasMoreElements();) {
+ String keyValue = e.nextElement();
+ int i = result.indexOf(keyValue);
+ if (i != -1) {
+ String s1 = result.substring(0,i);
+ String s2 = runtimeMappings.get(keyValue);
+ String s3 = result.substring(i+keyValue.length());
+ result = s1 + s2 + s3;
+ }
+ }
+ }
+
+ if (result.indexOf('{') != -1) {
+ // We test for the curly braces since due to NL issues we do not
+ // want to use MessageFormat unless we have to.
+ try {
+ result = MessageFormat.format(result, mappings);
+ } catch (IllegalArgumentException e) {
+ //ignore and return string without bound parameters
+ }
+ }
+
+ return result;
+ }
+
+ /*
+ * Read the contents of the ini, properties, and mappings files.
+ */
+ private IStatus load(URL iniURL, URL propertiesURL, URL mappingsURL) {
+
+ InputStream is = null;
+ try {
+ is = iniURL.openStream();
+ ini = new Properties();
+ ini.load(is);
+ } catch (IOException e) {
+ ini = null;
+ String message = NLS.bind(Messages.IniFileReader_ReadIniError, iniURL.toExternalForm());
+ return new Status(IStatus.ERROR, PID, 0, message, e);
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+
+ if (propertiesURL != null) {
+ is = null;
+ try {
+ is = propertiesURL.openStream();
+ properties = new PropertyResourceBundle(is);
+ } catch (IOException e) {
+ properties = null;
+ String message = NLS.bind(Messages.IniFileReader_ReadPropError, propertiesURL.toExternalForm());
+ return new Status(IStatus.ERROR, PID, 0, message, e);
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ PropertyResourceBundle bundle = null;
+ if (mappingsURL != null) {
+ is = null;
+ try {
+ is = mappingsURL.openStream();
+ bundle = new PropertyResourceBundle(is);
+ } catch (IOException e) {
+ bundle = null;
+ String message = NLS.bind(Messages.IniFileReader_ReadMapError, mappingsURL.toExternalForm());
+ return new Status(IStatus.ERROR, PID, 0, message, e);
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ ArrayList mappingsList = new ArrayList<>();
+ if (bundle != null) {
+ boolean found = true;
+ int i = 0;
+ while (found) {
+ try {
+ mappingsList.add(bundle.getString(Integer.toString(i)));
+ } catch (MissingResourceException e) {
+ found = false;
+ }
+ i++;
+ }
+ }
+ mappings = mappingsList.toArray(new String[mappingsList.size()]);
+
+ return OK_STATUS;
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/Messages.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/Messages.java
new file mode 100644
index 00000000000..3c08954706e
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/Messages.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2008 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups;
+
+import org.eclipse.osgi.util.NLS;
+
+public final class Messages extends NLS {
+
+ private static final String BUNDLE_NAME = "org.eclipse.core.internal.runtime.bundlegroups.messages";//$NON-NLS-1$
+
+ private Messages() {
+ // Do not instantiate
+ }
+
+ public static String BundleGroupProvider;
+ public static String FeatureParser_IdOrVersionInvalid;
+ public static String IniFileReader_MissingDesc;
+ public static String IniFileReader_OpenINIError;
+ public static String IniFileReader_ReadIniError;
+ public static String IniFileReader_ReadPropError;
+ public static String IniFileReader_ReadMapError;
+
+ static {
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/messages.properties b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/messages.properties
new file mode 100644
index 00000000000..67242172d1b
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/messages.properties
@@ -0,0 +1,22 @@
+###############################################################################
+# Copyright (c) 2000, 2008 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
+###############################################################################
+BundleGroupProvider=Installed Features
+
+FeatureParser_IdOrVersionInvalid= Error parsing feature stream. The unique identifier or the version is null or empty: unique identifier=\"{0}\" version=\"{1}\".
+
+IniFileReader_MissingDesc = Cannot find plugin for feature {0}
+IniFileReader_OpenINIError = Cannot open branding file {0}
+IniFileReader_ReadIniError = Error reading branding file {0}
+IniFileReader_ReadPropError = Error reading branding properties file {0}
+IniFileReader_ReadMapError = Error reading mapping files {0}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Configuration.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Configuration.java
new file mode 100644
index 00000000000..5753448e72c
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Configuration.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2010 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups.platformxml;
+
+import java.io.File;
+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.URIUtil;
+
+/**
+ * The contents of a platform.xml file.
+ */
+public class Configuration {
+
+ private final List sites = new ArrayList<>();
+ private String sharedUR;
+ private URL osgiInstallArea;
+
+ void setSharedUR(String value) {
+ sharedUR = value;
+ }
+
+ void setOsgiInstallArea(URL osgiInstallArea) {
+ this.osgiInstallArea = osgiInstallArea;
+ }
+
+ /**
+ * The sites of this configuration, followed by those of the shared
+ * configuration it links to, if any.
+ */
+ public List getSites() {
+ if (sharedUR == null) {
+ return sites;
+ }
+ List result = new ArrayList<>(sites);
+ Configuration parent = loadParent();
+ if (parent != null) {
+ result.addAll(parent.getSites());
+ }
+ return result;
+ }
+
+ private Configuration loadParent() {
+ File location;
+ try {
+ location = URIUtil.toFile(new URI(sharedUR));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ if (location == null) {
+ return null;
+ }
+ if (!location.isAbsolute()) {
+ File installArea = toFile(osgiInstallArea);
+ if (installArea == null) {
+ return null;
+ }
+ location = new File(installArea, location.getPath());
+ }
+ return ConfigurationParser.parse(location, osgiInstallArea);
+ }
+
+ private static File toFile(URL url) {
+ try {
+ return url == null ? null : URIUtil.toFile(URIUtil.toURI(url));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ void add(Site site) {
+ sites.add(site);
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationConstants.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationConstants.java
new file mode 100644
index 00000000000..d241a1c4fd2
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationConstants.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2008 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups.platformxml;
+
+/**
+ * Element and attribute names used in a platform.xml file.
+ */
+public interface ConfigurationConstants {
+
+ String ELEMENT_CONFIG = "config"; //$NON-NLS-1$
+ String ELEMENT_FEATURE = "feature"; //$NON-NLS-1$
+ String ELEMENT_SITE = "site"; //$NON-NLS-1$
+
+ String ATTRIBUTE_ID = "id"; //$NON-NLS-1$
+ String ATTRIBUTE_PLUGIN_IDENTIFIER = "plugin-identifier"; //$NON-NLS-1$
+ String ATTRIBUTE_PLUGIN_VERSION = "plugin-version"; //$NON-NLS-1$
+ String ATTRIBUTE_SHARED_UR = "shared_ur"; //$NON-NLS-1$
+ String ATTRIBUTE_URL = "url"; //$NON-NLS-1$
+ String ATTRIBUTE_VERSION = "version"; //$NON-NLS-1$
+
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationParser.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationParser.java
new file mode 100644
index 00000000000..dc9cf4bf4d4
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/ConfigurationParser.java
@@ -0,0 +1,227 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2015 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups.platformxml;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.URIUtil;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Parser for platform.xml files.
+ */
+public class ConfigurationParser implements ConfigurationConstants {
+ static final String PLATFORM_BASE = "platform:/base/"; //$NON-NLS-1$
+ private final URL osgiInstallArea;
+
+ /*
+ * Parse the given file handle which points to a platform.xml file and return a
+ * configuration object. Returns null if the file doesn't exist or cannot be read.
+ */
+ public static Configuration parse(File file, URL osgiInstallArea) {
+ return new ConfigurationParser(osgiInstallArea).internalParse(file);
+ }
+
+ private ConfigurationParser(URL osgiInstallArea) {
+ this.osgiInstallArea = osgiInstallArea;
+ }
+
+ /*
+ * Create a feature object based on the given DOM node. Return the new feature.
+ */
+ private static Feature createFeature(Node node) {
+ Feature result = new Feature();
+ String id = getAttribute(node, ATTRIBUTE_ID);
+ if (id != null) {
+ result.setId(id);
+ }
+ String url = getAttribute(node, ATTRIBUTE_URL);
+ if (url != null) {
+ result.setUrl(url);
+ }
+ String version = getAttribute(node, ATTRIBUTE_VERSION);
+ if (version != null) {
+ result.setVersion(version);
+ }
+ String pluginIdentifier = getAttribute(node, ATTRIBUTE_PLUGIN_IDENTIFIER);
+ if (pluginIdentifier != null) {
+ result.setPluginIdentifier(pluginIdentifier);
+ }
+ String pluginVersion = getAttribute(node, ATTRIBUTE_PLUGIN_VERSION);
+ // plug-in version is the same as the feature version if it is missing
+ if (pluginVersion == null) {
+ pluginVersion = version;
+ }
+ if (pluginVersion != null) {
+ result.setPluginVersion(pluginVersion);
+ }
+ return result;
+ }
+
+ /*
+ * Create the features from the given DOM node.
+ */
+ private static void createFeatures(Node node, Site site) {
+ NodeList children = node.getChildNodes();
+ int size = children.getLength();
+ for (int i = 0; i < size; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ if (!ELEMENT_FEATURE.equalsIgnoreCase(child.getNodeName())) {
+ continue;
+ }
+ site.addFeature(createFeature(child));
+ }
+ }
+
+ /*
+ * Create a site based on the given DOM node.
+ */
+ private Site createSite(Node node) {
+ Site result = new Site();
+ String url = getAttribute(node, ATTRIBUTE_URL);
+ if (url != null) {
+ try {
+ // do this to ensure the location is an encoded URI
+ URI uri = URIUtil.fromString(url);
+ URI osgiURI = osgiInstallArea != null ? URIUtil.toURI(osgiInstallArea) : null;
+ result.setUrl(getLocation(uri, osgiURI).toString());
+ } catch (URISyntaxException e) {
+ result.setUrl(url);
+ }
+ }
+ createFeatures(node, result);
+ return result;
+ }
+
+ /*
+ * Convert the given url string to an absolute url. If the string is
+ * platform:/base/ then return a string which represents the osgi install area.
+ */
+ private static URI getLocation(URI location, URI osgiArea) {
+ if (osgiArea == null) {
+ return location;
+ }
+ if (PLATFORM_BASE.equals(location.toString())) {
+ return osgiArea;
+ }
+ return URIUtil.makeAbsolute(location, osgiArea);
+ }
+
+ /*
+ * Return the attribute with the given name, or null if it does not exist.
+ */
+ private static String getAttribute(Node node, String name) {
+ NamedNodeMap attributes = node.getAttributes();
+ Node temp = attributes.getNamedItem(name);
+ return temp == null ? null : temp.getNodeValue();
+ }
+
+ /*
+ * Load the given file into a DOM document.
+ */
+ @SuppressWarnings("restriction")
+ private static Document load(InputStream input) throws ParserConfigurationException, IOException, SAXException {
+ DocumentBuilderFactory factory = org.eclipse.core.internal.runtime.XmlProcessorFactory
+ .createDocumentBuilderFactoryWithErrorOnDOCTYPE();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ try (InputStream buffered = new BufferedInputStream(input)) {
+ return builder.parse(buffered);
+ }
+ }
+
+ private Configuration internalParse(File file) {
+ if (!file.exists()) {
+ return null;
+ }
+ try (InputStream input = new FileInputStream(file)) {
+ Configuration result = process(load(input));
+ if (result != null) {
+ result.setOsgiInstallArea(osgiInstallArea);
+ }
+ return result;
+ } catch (IOException | ParserConfigurationException | SAXException e) {
+ ILog.of(ConfigurationParser.class).error("Failed to read " + file, e); //$NON-NLS-1$
+ return null;
+ }
+ }
+
+ /*
+ * Process the given DOM document and create the appropriate site objects.
+ */
+ private Configuration process(Document document) {
+ Node node = getConfigElement(document);
+ if (node == null) {
+ return null;
+ }
+ Configuration configuration = createConfiguration(node);
+ NodeList children = node.getChildNodes();
+ int size = children.getLength();
+ for (int i = 0; i < size; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ if (!ELEMENT_SITE.equalsIgnoreCase(child.getNodeName())) {
+ continue;
+ }
+ Site site = createSite(child);
+ if (site != null) {
+ configuration.add(site);
+ }
+ }
+ return configuration;
+ }
+
+ private static Configuration createConfiguration(Node node) {
+ Configuration result = new Configuration();
+ String value = getAttribute(node, ATTRIBUTE_SHARED_UR);
+ if (value != null) {
+ result.setSharedUR(value);
+ }
+ return result;
+ }
+
+ private static Node getConfigElement(Document doc) {
+ NodeList children = doc.getChildNodes();
+ int size = children.getLength();
+ for (int i = 0; i < size; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ if (ELEMENT_CONFIG.equalsIgnoreCase(child.getNodeName())) {
+ return child;
+ }
+ }
+ return null;
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Feature.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Feature.java
new file mode 100644
index 00000000000..222cf79a969
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Feature.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2008 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups.platformxml;
+
+/*
+ * Represents a feature entry in a platform.xml file.
+ */
+public class Feature {
+
+ private String id;
+ private String url;
+ private String version;
+ private String pluginIdentifier;
+ private String pluginVersion;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getPluginIdentifier() {
+ return pluginIdentifier;
+ }
+
+ public String getPluginVersion() {
+ return pluginVersion;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setPluginIdentifier(String pluginIdentifier) {
+ this.pluginIdentifier = pluginIdentifier;
+ }
+
+ public void setPluginVersion(String pluginVersion) {
+ this.pluginVersion = pluginVersion;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+}
diff --git a/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Site.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Site.java
new file mode 100644
index 00000000000..8348e99a4dc
--- /dev/null
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/platformxml/Site.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2010 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
+ *******************************************************************************/
+package org.eclipse.core.internal.runtime.bundlegroups.platformxml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * Represents a site in a platform.xml file.
+ */
+public class Site {
+
+ private String url;
+ private final List features = new ArrayList<>();
+
+ public void addFeature(Feature feature) {
+ this.features.add(feature);
+ }
+
+ public List getFeatures() {
+ return features;
+ }
+
+ /**
+ * Note the string that we are returning is an ENCODED URI string.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * Note that the string should be an ENCODED URI string.
+ */
+ public void setUrl(String url) {
+ this.url = url;
+ }
+}
diff --git a/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF b/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
index 9e92d1fd8ce..7217575c039 100644
--- a/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
+++ b/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
@@ -17,5 +17,4 @@ Import-Package: javax.xml.parsers,
org.xml.sax.helpers
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: org.eclipse.update.configurator
-Service-Component: OSGI-INF/org.eclipse.update.internal.configurator.BundleGroupComponent.xml,
- OSGI-INF/org.eclipse.update.internal.configurator.PlatformConfigurationFactory.xml
+Service-Component: OSGI-INF/org.eclipse.update.internal.configurator.PlatformConfigurationFactory.xml
diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/BundleGroupComponent.java b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/BundleGroupComponent.java
deleted file mode 100644
index 113d20ce3ad..00000000000
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/BundleGroupComponent.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2009, 2017 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
- *******************************************************************************/
-package org.eclipse.update.internal.configurator;
-
-import java.util.ArrayList;
-
-import org.eclipse.core.runtime.IBundleGroup;
-import org.eclipse.core.runtime.IBundleGroupProvider;
-import org.osgi.service.component.annotations.Activate;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.Reference;
-
-/**
- * Declarative services component that provides an implementation of
- * {@link IBundleGroupProvider}. This allows the bundle group provider to be
- * made available in the service registry before this bundle has started.
- */
-@Component(service = IBundleGroupProvider.class)
-@SuppressWarnings("removal")
-public class BundleGroupComponent implements IBundleGroupProvider {
-
-
- private final org.eclipse.update.configurator.IPlatformConfigurationFactory factory;
-
- @Activate
- public BundleGroupComponent(@Reference org.eclipse.update.configurator.IPlatformConfigurationFactory factory) {
- this.factory = factory;
- }
-
- @Override
- public IBundleGroup[] getBundleGroups() {
- org.eclipse.update.configurator.IPlatformConfiguration configuration = factory
- .getCurrentPlatformConfiguration();
- if (configuration == null) {
- return new IBundleGroup[0];
- }
- org.eclipse.update.configurator.IPlatformConfiguration.IFeatureEntry[] features = configuration
- .getConfiguredFeatureEntries();
- ArrayList bundleGroups = new ArrayList<>(features.length);
- for (org.eclipse.update.configurator.IPlatformConfiguration.IFeatureEntry feature : features) {
- if (feature instanceof FeatureEntry && ((FeatureEntry) feature).hasBranding()) {
- bundleGroups.add((IBundleGroup) feature);
- }
- }
- return bundleGroups.toArray(new IBundleGroup[bundleGroups.size()]);
- }
-
- @Override
- public String getName() {
- return Messages.BundleGroupProvider;
- }
-
-}