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/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/AboutInfo.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java
similarity index 99%
rename from update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/AboutInfo.java
rename to runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java
index 5d3c412d7ef..0169c63b8ee 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/AboutInfo.java
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/AboutInfo.java
@@ -11,7 +11,7 @@
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
-package org.eclipse.update.internal.configurator.branding;
+package org.eclipse.core.internal.runtime.bundlegroups;
import java.net.URL;
import java.util.Hashtable;
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/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IBundleGroupConstants.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java
similarity index 86%
rename from update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IBundleGroupConstants.java
rename to runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java
index c8c78bacc36..bcedef3c09c 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IBundleGroupConstants.java
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IBundleGroupConstants.java
@@ -11,9 +11,8 @@
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
-package org.eclipse.update.internal.configurator.branding;
+package org.eclipse.core.internal.runtime.bundlegroups;
-import org.eclipse.update.configurator.IPlatformConfiguration;
/**
* These constants define the set of properties that the UI expects to
@@ -55,15 +54,13 @@ public interface IBundleGroupConstants {
/**
* The id of the bundle group's branding bundle.
*
- * @see IPlatformConfiguration.IFeatureEntry#getFeaturePluginIdentifier()
* @since 3.3
*/
public static final String BRANDING_BUNDLE_ID= "brandingBundleId"; //$NON-NLS-1$
/**
- * The version of the feature branding bundle.
+ * The version of the bundle group's branding bundle.
*
- * @see IPlatformConfiguration.IFeatureEntry#getFeaturePluginVersion()
* @since 3.3
*/
public static final String BRANDING_BUNDLE_VERSION= "brandingBundleVersion"; //$NON-NLS-1$
diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IProductConstants.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java
similarity index 98%
rename from update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IProductConstants.java
rename to runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java
index 2ce1a3f6665..7111b183518 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IProductConstants.java
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IProductConstants.java
@@ -11,7 +11,7 @@
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
-package org.eclipse.update.internal.configurator.branding;
+package org.eclipse.core.internal.runtime.bundlegroups;
/**
* These constants define the set of properties that the UI expects to
diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IniFileReader.java b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java
similarity index 98%
rename from update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IniFileReader.java
rename to runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java
index e42d4dac0bc..f4d48bef45d 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/branding/IniFileReader.java
+++ b/runtime/bundles/org.eclipse.core.runtime.bundlegroups/src/org/eclipse/core/internal/runtime/bundlegroups/IniFileReader.java
@@ -11,7 +11,7 @@
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
-package org.eclipse.update.internal.configurator.branding;
+package org.eclipse.core.internal.runtime.bundlegroups;
import java.io.IOException;
import java.io.InputStream;
@@ -31,7 +31,6 @@
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
-import org.eclipse.update.internal.configurator.Messages;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
@@ -43,7 +42,7 @@
* properties file - format being "n = some text", where n is a number.
*/
public class IniFileReader {
- private static final String PID = "org.eclipse.update.configurator"; //$NON-NLS-1$
+ 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$
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.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java
index 6c6e2cbaf54..4dbe8d282a3 100644
--- a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java
+++ b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java
@@ -13,7 +13,6 @@
*******************************************************************************/
package org.eclipse.update.configurator.tests;
-import org.eclipse.update.internal.configurator.tests.FeatureEntryTests;
import org.eclipse.update.internal.configurator.tests.SiteEntryTests;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@@ -25,7 +24,6 @@
*/
@Suite
@SelectClasses({ //
- FeatureEntryTests.class, //
SiteEntryTests.class, //
})
public class AutomatedConfiguratorSuite {
diff --git a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/FeatureEntryTests.java b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/FeatureEntryTests.java
deleted file mode 100644
index 8072cf4543a..00000000000
--- a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/FeatureEntryTests.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 Torbjörn Svensson 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:
- * Torbjörn Svensson - initial API and implementation
- *******************************************************************************/
-package org.eclipse.update.internal.configurator.tests;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import org.eclipse.update.internal.configurator.FeatureEntry;
-import org.eclipse.update.internal.configurator.SiteEntry;
-import org.junit.jupiter.api.Test;
-
-@SuppressWarnings("restriction")
-public class FeatureEntryTests {
-
- @Test
- public void testLicenseUrl() throws MalformedURLException {
- FeatureEntry featureEntry = new FeatureEntry("org.eclipse.platform", "4.13.0.v20190916-1323", null, null, false, null, null);
- featureEntry.setSite(new SiteEntry(new URL("platform:/base/")));
- featureEntry.setURL("features/org.eclipse.platform_4.13.0.v20190916-1323/");
-
- featureEntry.setLicenseURL("http://www.example.org");
- assertEquals("http://www.example.org", featureEntry.getLicenseURL());
-
- featureEntry.setLicenseURL("https://www.example.org");
- assertEquals("https://www.example.org", featureEntry.getLicenseURL());
-
- featureEntry.setLicenseURL("license.html");
- assertEquals("platform:/base/features/org.eclipse.platform_4.13.0.v20190916-1323/license.html", featureEntry.getLicenseURL());
- }
-}
\ No newline at end of file
diff --git a/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF b/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
index a4f3934ac99..d28d69b429b 100644
--- a/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
+++ b/update/org.eclipse.update.configurator/META-INF/MANIFEST.MF
@@ -7,8 +7,7 @@ Bundle-Activator: org.eclipse.update.internal.configurator.ConfigurationActivato
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Export-Package: org.eclipse.update.configurator,
- org.eclipse.update.internal.configurator;x-friends:="org.eclipse.update.core",
- org.eclipse.update.internal.configurator.branding;x-friends:="org.eclipse.update.core"
+ org.eclipse.update.internal.configurator;x-friends:="org.eclipse.update.core"
Require-Bundle: org.eclipse.core.runtime;bundle-version="3.29.0"
Bundle-RequiredExecutionEnvironment: JavaSE-17
Import-Package: javax.xml.parsers,
@@ -17,5 +16,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;
- }
-
-}
diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/FeatureEntry.java b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/FeatureEntry.java
index 80806881707..6a568fdbf50 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/FeatureEntry.java
+++ b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/FeatureEntry.java
@@ -13,20 +13,10 @@
*******************************************************************************/
package org.eclipse.update.internal.configurator;
-import java.net.MalformedURLException;
import java.net.URL;
-import java.net.URLClassLoader;
import java.util.ArrayList;
-import java.util.MissingResourceException;
-import java.util.ResourceBundle;
-import org.eclipse.core.runtime.IBundleGroup;
-import org.eclipse.core.runtime.Platform;
import org.eclipse.update.configurator.IPlatformConfiguration;
-import org.eclipse.update.internal.configurator.branding.AboutInfo;
-import org.eclipse.update.internal.configurator.branding.IBundleGroupConstants;
-import org.eclipse.update.internal.configurator.branding.IProductConstants;
-import org.osgi.framework.Bundle;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -35,13 +25,7 @@
*
* Feature information
*/
-public class FeatureEntry
- implements
- IPlatformConfiguration.IFeatureEntry,
- IConfigurationConstants,
- IBundleGroup,
- IBundleGroupConstants,
- IProductConstants {
+public class FeatureEntry implements IPlatformConfiguration.IFeatureEntry, IConfigurationConstants {
private final String id;
private final String version;
private final String pluginVersion;
@@ -50,12 +34,8 @@ public class FeatureEntry
private final boolean primary;
private final String pluginIdentifier;
private String url;
- private String description;
- private String licenseURL;
private ArrayList plugins;
- private AboutInfo branding;
private SiteEntry site;
- private ResourceBundle resourceBundle;
private boolean fullyParsed;
public FeatureEntry(String id, String version, String pluginIdentifier, String pluginVersion, boolean primary, String application, URL[] root) {
@@ -190,112 +170,6 @@ public Element toXML(Document doc) {
return featureElement;
}
- public void setDescription(String description) {
- this.description = description;
- }
-
- @Override
- public Bundle[] getBundles() {
- if (plugins == null) {
- fullParse();
- }
-
- ArrayList bundles = new ArrayList<>(plugins.size());
- for (PluginEntry plugin : plugins) {
- // get the highest version for the plugin
- Bundle bundle = Platform.getBundle(plugin.getPluginIdentifier());
- if (bundle != null) {
- bundles.add(bundle);
- }
- }
- return bundles.toArray(new Bundle[bundles.size()]);
- }
-
- @Override
- public String getDescription() {
- if (description == null) {
- fullParse();
- }
- return description;
- }
-
- @Override
- public String getIdentifier() {
- return id;
- }
-
- @Override
- public String getName() {
- if (branding == null) {
- branding = AboutInfo.readFeatureInfo(id, version, getFeaturePluginIdentifier());
- }
- return branding.getProductName();
- }
-
- @Override
- public String getProperty(String key) {
- if (key == null) {
- return null;
- }
-
- if (branding == null) {
- branding = AboutInfo.readFeatureInfo(id, version, getFeaturePluginIdentifier());
- }
-
- // IBundleGroupConstants
- if (key.equals(FEATURE_IMAGE)) {
- return branding.getFeatureImageURL() == null ? null : branding.getFeatureImageURL().toExternalForm();
- } else if (key.equals(TIPS_AND_TRICKS_HREF)) {
- return branding.getTipsAndTricksHref();
- } else if (key.equals(IBundleGroupConstants.WELCOME_PAGE)) { // same value is used by product and bundle group
- return branding.getWelcomePageURL() == null ? null : branding.getWelcomePageURL().toExternalForm();
- } else if (key.equals(WELCOME_PERSPECTIVE)) {
- return branding.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 branding.getAppName();
- } else if (key.equals(ABOUT_TEXT)) {
- return branding.getAboutText();
- } else if (key.equals(ABOUT_IMAGE)) {
- return branding.getAboutImageURL() == null ? null : branding.getAboutImageURL().toExternalForm();
- } else if (key.equals(WINDOW_IMAGE)) {
- return branding.getWindowImageURL()== null ? null : branding.getWindowImageURL().toExternalForm();
- } else if (key.equals(WINDOW_IMAGES)) {
- URL[] urls = branding.getWindowImagesURLs();
- if (urls == null) {
- return null;
- }
- StringBuilder windowImagesURLs = new StringBuilder();
- for (int i=0; i
- * For example, assume resource bundle plugin.properties contains
- * name = Project Name
- *
- *
- * resolveNLString(b,"Hello World") returns "Hello World"
- * resolveNLString(b,"%name") returns "Project Name"
- * resolveNLString(b,"%name Hello World") returns "Project Name"
- * resolveNLString(b,"%abcd Hello World") returns "Hello World"
- * resolveNLString(b,"%abcd") returns "%abcd"
- * resolveNLString(b,"%%name") returns "%name"
- *
- *
- * @param resourceBundle resource bundle.
- * @param string translatable string from model
- * @return string, or null
- * @since 2.0
- */
- public static String getResourceString(ResourceBundle resourceBundle, String string) {
-
- if (string == null) {
- return null;
- }
-
- String s = string.trim();
-
- if (s.isEmpty()) {
- return string;
- }
-
- if (!s.startsWith(KEY_PREFIX)) {
- return string;
- }
-
- 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 (resourceBundle == null) {
- return dflt;
- }
-
- try {
- return resourceBundle.getString(key.substring(1));
- } catch (MissingResourceException e) {
- return dflt;
- }
- }
public static boolean isAutomaticallyStartedBundle(String bundleURL) {
if (bundleURL.contains("org.eclipse.osgi")) { //$NON-NLS-1$
diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/messages.properties b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/messages.properties
index 0b2f4c3af9b..1d0bca48acc 100644
--- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/messages.properties
+++ b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/messages.properties
@@ -27,16 +27,10 @@ BundleManifest_noVersion = {0} attribute not found in manifest for bundle \"{1}\
FeatureParser_IdOrVersionInvalid= Error parsing feature stream. The unique identifier or the version is null or empty: unique identifier=\"{0}\" version=\"{1}\".
-BundleGroupProvider=Update Manager Configurator
ConfigurationParser_cannotLoadSharedInstall=Could not load from shared install
PluginEntry_versionError=Unable to create versioned identifier: {0} {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}
SiteEntry_computePluginStamp = Cannot compute plugins change stamp for site {0} because it does not support plugin detection.
SiteEntry_cannotFindFeatureInDir=Unable to find feature.xml in directory: {0}