This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 036d94dd Distinguish between literal and reference property values
036d94dd is described below

commit 036d94dd42260ee6cc719224098f0b497332683e
Author: Konrad Windszus <[email protected]>
AuthorDate: Sat May 9 11:51:05 2026 +0200

    Distinguish between literal and reference property values
---
 .../scripts/migrate_annotations.py                 | 218 +++++++++++----------
 .../scripts/tests/test_migrate_annotations.py      | 118 ++++++++++-
 2 files changed, 230 insertions(+), 106 deletions(-)

diff --git a/skills/osgi-scr-migrator/scripts/migrate_annotations.py 
b/skills/osgi-scr-migrator/scripts/migrate_annotations.py
index acccd3f4..31e9eb6c 100755
--- a/skills/osgi-scr-migrator/scripts/migrate_annotations.py
+++ b/skills/osgi-scr-migrator/scripts/migrate_annotations.py
@@ -41,6 +41,12 @@ class Property:
     description: Optional[str] = None
     is_configurable: bool = False
 
+    def _strip_quotes(self, val: str) -> str:
+        """Strip surrounding quotes from a value if present."""
+        if val.startswith('"') and val.endswith('"'):
+            return val[1:-1]
+        return val
+
     def is_property_type_annotation(self) -> bool:
         """Check if this property should use a Component Property Type 
annotation.
 
@@ -77,27 +83,64 @@ class Property:
 
         Precondition: is_property_type_annotation() returned True.
         This method should always succeed if the precondition is met.
+        Unquotes literal values before using them in annotations.
         """
+        val = self._strip_quotes(self.value)
+        
         if self.name == 'service.ranking':
-            return f'@ServiceRanking({self.value})'
+            return f'@ServiceRanking({val})'
         elif self.name == 'service.description':
-            return f'@ServiceDescription("{self.value}")'
+            return f'@ServiceDescription("{val}")'
         elif self.name == 'service.vendor':
-            return f'@ServiceVendor("{self.value}")'
+            return f'@ServiceVendor("{val}")'
 
         # Should never reach here if precondition is met
         raise ValueError(f"Unknown property type annotation: {self.name}")
 
     def to_component_property(self) -> List[str]:
-        """Convert to @Component property format."""
+        """Convert to @Component property format.
+        
+        Constant references are formatted as string concatenation: 
"name="+CONSTANT
+        Literal values (with quotes stripped) are kept unquoted within the 
property string: "name=value"
+        """
         properties = []
         type_suffix = f":{self.type.value}" if self.type != 
PropertyType.STRING else ""
 
+        def is_reference(val: str) -> bool:
+            """Return True when the value is not a valid literal for this 
property type."""
+            val = val.strip()
+
+            if self.type == PropertyType.STRING:
+                return not val.startswith('"')
+
+            if self.type == PropertyType.BOOLEAN:
+                return not bool(re.fullmatch(r'true|false', val))
+
+            if self.type in (PropertyType.INTEGER, PropertyType.LONG):
+                return not bool(re.fullmatch(r'-?\d+[lL]?', val))
+
+            if self.type in (PropertyType.FLOAT, PropertyType.DOUBLE):
+                return not 
bool(re.fullmatch(r'-?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[fFdD])?', val))
+
+            return True
+
         if self.values:
             for val in self.values:
-                properties.append(f'"{self.name}{type_suffix}={val}"')
+                if is_reference(val):
+                    # Reference - use string concatenation: "name="+CONSTANT
+                    properties.append(f'"{self.name}{type_suffix}="+{val}')
+                else:
+                    # Literal value - strip quotes and keep unquoted within 
property string
+                    stripped_val = self._strip_quotes(val)
+                    
properties.append(f'"{self.name}{type_suffix}={stripped_val}"')
         elif self.value is not None:
-            properties.append(f'"{self.name}{type_suffix}={self.value}"')
+            if is_reference(self.value):
+                # Reference - use string concatenation: "name="+CONSTANT
+                properties.append(f'"{self.name}{type_suffix}="+{self.value}')
+            else:
+                # Literal value - strip quotes and keep unquoted within 
property string
+                stripped_val = self._strip_quotes(self.value)
+                properties.append(f'"{self.name}{type_suffix}={stripped_val}"')
 
         return properties
 
@@ -359,7 +402,10 @@ class AnnotationMigrator:
 
         # Parse attributes
         name_match = re.search(r'name\s*=\s*"([^"]*)"', annotation)
-        value_match = 
re.search(r'(?<!bool)(?<!int)(?<!long)(?<!float)(?<!double)value\s*=\s*"([^"]*)"',
 annotation)
+        value_match = re.search(
+            
r'(?<!bool)(?<!int)(?<!long)(?<!float)(?<!double)value\s*=\s*("[^"]*"|[A-Za-z_][A-Za-z0-9_.]*)',
+            annotation
+        )
         values_match = re.search(r'value\s*=\s*\{([^}]+)\}', annotation)
         bool_value_match = re.search(r'boolValue\s*=\s*(true|false)', 
annotation)
         int_value_match = re.search(r'intValue\s*=\s*(-?\d+)', annotation)
@@ -406,11 +452,13 @@ class AnnotationMigrator:
             prop.type = PropertyType.DOUBLE
             prop.value = double_value_match.group(1)
         elif values_match:
-            # Array of values
+            # Array of values - preserve quotes on string literals
             values_str = values_match.group(1)
-            prop.values = [v.strip().strip('"') for v in values_str.split(',')]
+            prop.values = [v.strip() for v in values_str.split(',')]
         elif value_match:
-            prop.value = value_match.group(1)
+            raw_value = value_match.group(1)
+            # Keep string literals quoted; keep constant references unquoted.
+            prop.value = raw_value
 
         if label_match:
             prop.label = label_match.group(1)
@@ -447,6 +495,25 @@ class AnnotationMigrator:
 
     def _migrate_sling_servlet(self):
         """Migrate @SlingServlet to @Component with Sling Servlets 
Annotations."""
+        def parse_annotation_values(annotation_text: str, attr_name: str) -> 
Optional[List[str]]:
+            """Parse a SlingServlet attribute as a list of raw Java values.
+
+            Values are returned exactly as Java expressions (quoted string 
literal or constant
+            reference), so callers can preserve constants without adding 
quotes.
+            """
+            array_match = re.search(rf'{attr_name}\s*=\s*\{{([^}}]+)\}}', 
annotation_text)
+            if array_match:
+                return [v.strip() for v in array_match.group(1).split(',') if 
v.strip()]
+
+            single_match = re.search(
+                rf'{attr_name}\s*=\s*("[^"]+"|[A-Za-z_][A-Za-z0-9_.]*)',
+                annotation_text
+            )
+            if single_match:
+                return [single_match.group(1).strip()]
+
+            return None
+
         i = 0
         while i < len(self.lines):
             line = self.lines[i]
@@ -469,12 +536,8 @@ class AnnotationMigrator:
                 indent_str = ' ' * indent
 
                 # Determine if path-based or resource-type-based
-                paths_match = re.search(r'paths\s*=\s*\{([^}]+)\}', annotation)
-                path_match = re.search(r'paths\s*=\s*"([^"]+)"', annotation) 
if not paths_match else None
-                rt_match = re.search(r'resourceTypes\s*=\s*\{([^}]+)\}', 
annotation)
-                rt_single_match = re.search(r'resourceTypes\s*=\s*"([^"]+)"', 
annotation) if not rt_match else None
-                # Also match constant references like 
ActivityManagerImpl.RESOURCE_TYPE
-                rt_constant_match = 
re.search(r'resourceTypes\s*=\s*([A-Za-z_][A-Za-z0-9_.]*)', annotation) if not 
(rt_match or rt_single_match) else None
+                path_values = parse_annotation_values(annotation, 'paths')
+                rt_values = parse_annotation_values(annotation, 
'resourceTypes')
 
                 new_annotations = []
 
@@ -482,86 +545,29 @@ class AnnotationMigrator:
                 new_annotations.append(f'{indent_str}@Component(service = 
Servlet.class)')
 
                 # Path-based servlet
-                if paths_match or path_match:
-                    if paths_match:
-                        paths = [p.strip().strip('"') for p in 
paths_match.group(1).split(',')]
-                        paths_str = ', '.join(f'"{p}"' for p in paths)
+                if path_values:
+                    if len(path_values) == 1:
+                        
new_annotations.append(f'{indent_str}@SlingServletPaths(value = 
{path_values[0]})')
+                    else:
+                        paths_str = ', '.join(path_values)
                         
new_annotations.append(f'{indent_str}@SlingServletPaths(value = 
{{{paths_str}}})')
-                    elif path_match:
-                        
new_annotations.append(f'{indent_str}@SlingServletPaths(value = 
"{path_match.group(1)}")')
 
                 # Resource-type-based servlet
-                elif rt_match or rt_single_match or rt_constant_match:
+                elif rt_values:
                     attrs = []
 
-                    if rt_match:
-                        # Parse array elements, preserving whether they're 
string literals or constants
-                        raw_rts = [rt.strip() for rt in 
rt_match.group(1).split(',')]
-                        formatted_rts = []
-                        for rt in raw_rts:
-                            if rt.startswith('"') and rt.endswith('"'):
-                                # String literal - keep as is
-                                formatted_rts.append(rt)
-                            else:
-                                # Constant reference - keep without quotes
-                                formatted_rts.append(rt)
-
-                        if len(formatted_rts) == 1:
-                            # Single element - check if it needs quotes
-                            if formatted_rts[0].startswith('"'):
-                                attrs.append(f'resourceTypes = 
{formatted_rts[0]}')
-                            else:
-                                attrs.append(f'resourceTypes = 
{formatted_rts[0]}')
+                    def append_attr(attr_name: str, values: 
Optional[List[str]]):
+                        if not values:
+                            return
+                        if len(values) == 1:
+                            attrs.append(f'{attr_name} = {values[0]}')
                         else:
-                            rts_str = ', '.join(formatted_rts)
-                            attrs.append(f'resourceTypes = {{{rts_str}}}')
-                    elif rt_single_match:
-                        attrs.append(f'resourceTypes = 
"{rt_single_match.group(1)}"')
-                    elif rt_constant_match:
-                        # Constant reference - use as-is without quotes
-                        attrs.append(f'resourceTypes = 
{rt_constant_match.group(1)}')
-
-                    # Selectors
-                    selectors_match = 
re.search(r'selectors\s*=\s*\{([^}]+)\}', annotation)
-                    selector_match = re.search(r'selectors\s*=\s*"([^"]+)"', 
annotation) if not selectors_match else None
-
-                    if selectors_match:
-                        sels = [s.strip().strip('"') for s in 
selectors_match.group(1).split(',')]
-                        if len(sels) == 1:
-                            attrs.append(f'selectors = "{sels[0]}"')
-                        else:
-                            sels_str = ', '.join(f'"{s}"' for s in sels)
-                            attrs.append(f'selectors = {{{sels_str}}}')
-                    elif selector_match:
-                        attrs.append(f'selectors = 
"{selector_match.group(1)}"')
-
-                    # Extensions
-                    ext_match = re.search(r'extensions\s*=\s*\{([^}]+)\}', 
annotation)
-                    ext_single_match = 
re.search(r'extensions\s*=\s*"([^"]+)"', annotation) if not ext_match else None
-
-                    if ext_match:
-                        exts = [e.strip().strip('"') for e in 
ext_match.group(1).split(',')]
-                        if len(exts) == 1:
-                            attrs.append(f'extensions = "{exts[0]}"')
-                        else:
-                            exts_str = ', '.join(f'"{e}"' for e in exts)
-                            attrs.append(f'extensions = {{{exts_str}}}')
-                    elif ext_single_match:
-                        attrs.append(f'extensions = 
"{ext_single_match.group(1)}"')
-
-                    # Methods
-                    methods_match = re.search(r'methods\s*=\s*\{([^}]+)\}', 
annotation)
-                    method_match = re.search(r'methods\s*=\s*"([^"]+)"', 
annotation) if not methods_match else None
-
-                    if methods_match:
-                        methods = [m.strip().strip('"') for m in 
methods_match.group(1).split(',')]
-                        if len(methods) == 1:
-                            attrs.append(f'methods = "{methods[0]}"')
-                        else:
-                            methods_str = ', '.join(f'"{m}"' for m in methods)
-                            attrs.append(f'methods = {{{methods_str}}}')
-                    elif method_match:
-                        attrs.append(f'methods = "{method_match.group(1)}"')
+                            attrs.append(f'{attr_name} = {{{", 
".join(values)}}}')
+
+                    append_attr('resourceTypes', rt_values)
+                    append_attr('selectors', 
parse_annotation_values(annotation, 'selectors'))
+                    append_attr('extensions', 
parse_annotation_values(annotation, 'extensions'))
+                    append_attr('methods', parse_annotation_values(annotation, 
'methods'))
 
                     # Build @SlingServletResourceTypes annotation
                     if len(attrs) == 1:
@@ -585,7 +591,7 @@ class AnnotationMigrator:
                 # Add imports
                 
self.imports_to_add.add('org.osgi.service.component.annotations.Component')
                 self.imports_to_add.add('javax.servlet.Servlet')
-                if paths_match or path_match:
+                if path_values:
                     
self.imports_to_add.add('org.apache.sling.servlets.annotations.SlingServletPaths')
                 else:
                     
self.imports_to_add.add('org.apache.sling.servlets.annotations.SlingServletResourceTypes')
@@ -599,6 +605,14 @@ class AnnotationMigrator:
 
     def _migrate_sling_filter(self):
         """Migrate @SlingFilter to @Component with @SlingServletFilter 
annotation."""
+        def parse_single_annotation_value(annotation_text: str, attr_name: 
str) -> Optional[str]:
+            """Parse a SlingFilter single-value attribute as raw Java 
expression."""
+            match = re.search(
+                rf'{attr_name}\s*=\s*("[^"]+"|-?\d+|[A-Za-z_][A-Za-z0-9_.]*)',
+                annotation_text
+            )
+            return match.group(1).strip() if match else None
+
         i = 0
         while i < len(self.lines):
             line = self.lines[i]
@@ -623,19 +637,21 @@ class AnnotationMigrator:
                 attrs = []
 
                 # Scope
-                scope_match = 
re.search(r'scope\s*=\s*SlingFilterScope\.(\w+)', annotation)
-                if scope_match:
-                    attrs.append(f'scope = 
SlingServletFilterScope.{scope_match.group(1)}')
+                scope_value = parse_single_annotation_value(annotation, 
'scope')
+                if scope_value:
+                    # Map old enum constants to the new enum class, preserve 
custom constants as-is.
+                    scope_value = scope_value.replace('SlingFilterScope.', 
'SlingServletFilterScope.')
+                    attrs.append(f'scope = {scope_value}')
 
                 # Order (maps to order parameter in @SlingServletFilter)
-                order_match = re.search(r'order\s*=\s*(-?\d+)', annotation)
-                if order_match:
-                    attrs.append(f'order = {order_match.group(1)}')
+                order_value = parse_single_annotation_value(annotation, 
'order')
+                if order_value:
+                    attrs.append(f'order = {order_value}')
 
                 # Pattern
-                pattern_match = re.search(r'pattern\s*=\s*"([^"]+)"', 
annotation)
-                if pattern_match:
-                    attrs.append(f'pattern = "{pattern_match.group(1)}"')
+                pattern_value = parse_single_annotation_value(annotation, 
'pattern')
+                if pattern_value:
+                    attrs.append(f'pattern = {pattern_value}')
 
                 # Generate annotations
                 new_annotations = []
@@ -666,7 +682,7 @@ class AnnotationMigrator:
                 
self.imports_to_add.add('org.osgi.service.component.annotations.Component')
                 self.imports_to_add.add('javax.servlet.Filter')
                 
self.imports_to_add.add('org.apache.sling.servlets.annotations.SlingServletFilter')
-                if scope_match:
+                if scope_value and ('SlingServletFilterScope.' in scope_value):
                     
self.imports_to_add.add('org.apache.sling.servlets.annotations.SlingServletFilterScope')
 
                 self.changes_made = True
diff --git a/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py 
b/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
index 23f39628..598d8458 100644
--- a/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
+++ b/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
@@ -23,7 +23,7 @@ from migrate_annotations import (
 class TestPropertyParsing(unittest.TestCase):
     """Test @Property annotation parsing."""
 
-    def test_simple_string_property(self):
+    def test_simple_string_literal_property(self):
         """Test parsing simple string property."""
         content = '''
 @Property(name = "service.vendor", value = "Apache Software Foundation")
@@ -35,7 +35,22 @@ class TestPropertyParsing(unittest.TestCase):
         self.assertEqual(len(migrator.component_properties), 1)
         prop = migrator.component_properties[0]
         self.assertEqual(prop.name, "service.vendor")
-        self.assertEqual(prop.value, "Apache Software Foundation")
+        self.assertEqual(prop.value, "\"Apache Software Foundation\"")
+        self.assertEqual(prop.type, PropertyType.STRING)
+
+    def test_simple_string_reference_property(self):
+        """Test parsing simple string property."""
+        content = '''
+@Property(name = "service.vendor", value = Constants.SERVICE_VENDOR)
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        migrator._collect_properties()
+
+        self.assertEqual(len(migrator.component_properties), 1)
+        prop = migrator.component_properties[0]
+        self.assertEqual(prop.name, "service.vendor")
+        self.assertEqual(prop.value, "Constants.SERVICE_VENDOR")
         self.assertEqual(prop.type, PropertyType.STRING)
 
     def test_boolean_property(self):
@@ -81,7 +96,7 @@ class TestPropertyParsing(unittest.TestCase):
         prop = migrator.component_properties[0]
         self.assertEqual(prop.name, "paths")
         self.assertEqual(len(prop.values), 3)
-        self.assertEqual(prop.values, ["/path1", "/path2", "/path3"])
+        self.assertEqual(prop.values, ["\"/path1\"", "\"/path2\"", 
"\"/path3\""])
 
     def test_property_with_label(self):
         """Test parsing property with label and description."""
@@ -105,7 +120,7 @@ class TestPropertyConversion(unittest.TestCase):
 
     def test_simple_property_conversion(self):
         """Test converting simple property to component format."""
-        prop = Property(name="service.vendor", value="Apache Software 
Foundation")
+        prop = Property(name="service.vendor", value="\"Apache Software 
Foundation\"")
         result = prop.to_component_property()
 
         self.assertEqual(len(result), 1)
@@ -129,7 +144,7 @@ class TestPropertyConversion(unittest.TestCase):
 
     def test_array_property_conversion(self):
         """Test converting array property."""
-        prop = Property(name="paths", values=["/path1", "/path2"])
+        prop = Property(name="paths", values=["\"/path1\"", "\"/path2\""])
         result = prop.to_component_property()
 
         self.assertEqual(len(result), 2)
@@ -219,6 +234,50 @@ public class MyServlet extends SlingAllMethodsServlet {
         
self.assertNotIn('org.apache.felix.scr.annotations.sling.SlingServlet', 
new_content)
         
self.assertIn('org.apache.sling.servlets.annotations.SlingServletResourceTypes',
 new_content)
 
+    def test_servlet_with_constant_values_for_all_resource_type_elements(self):
+        """Test constants are preserved for 
resourceTypes/selectors/extensions/methods."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.sling.SlingServlet;
+
+@SlingServlet(
+    resourceTypes = MyServlet.RESOURCE_TYPE,
+    selectors = {MyServlet.SELECTOR_A, "print"},
+    extensions = {MyServlet.EXT_JSON, "html"},
+    methods = {HttpConstants.METHOD_GET, "POST"}
+)
+public class MyServlet extends SlingAllMethodsServlet {
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        new_content, changed = migrator.migrate()
+
+        self.assertTrue(changed)
+        self.assertIn('@SlingServletResourceTypes', new_content)
+        self.assertIn('resourceTypes = MyServlet.RESOURCE_TYPE', new_content)
+        self.assertIn('selectors = {MyServlet.SELECTOR_A, "print"}', 
new_content)
+        self.assertIn('extensions = {MyServlet.EXT_JSON, "html"}', new_content)
+        self.assertIn('methods = {HttpConstants.METHOD_GET, "POST"}', 
new_content)
+
+    def test_path_based_servlet_with_constant_paths(self):
+        """Test constants are preserved for paths attribute."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.sling.SlingServlet;
+
+@SlingServlet(paths = {MyServlet.PATH_A, "/bin/fallback"})
+public class MyServlet extends SlingAllMethodsServlet {
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        new_content, changed = migrator.migrate()
+
+        self.assertTrue(changed)
+        self.assertIn('@SlingServletPaths(value = {MyServlet.PATH_A, 
"/bin/fallback"})', new_content)
+        
self.assertIn('org.apache.sling.servlets.annotations.SlingServletPaths', 
new_content)
+
     def test_sling_filter_migration(self):
         """Test migrating Sling filter to Sling Servlets Annotations."""
         content = '''package com.example;
@@ -248,6 +307,55 @@ public class MyFilter implements Filter {
         
self.assertIn('org.apache.sling.servlets.annotations.SlingServletFilter', 
new_content)
         
self.assertIn('org.apache.sling.servlets.annotations.SlingServletFilterScope', 
new_content)
 
+    def test_sling_filter_with_constant_values(self):
+        """Test constants are preserved for SlingFilter attributes."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.sling.SlingFilter;
+
+@SlingFilter(
+    scope = MyFilter.SCOPE,
+    order = MyFilter.ORDER,
+    pattern = MyFilter.PATTERN
+)
+public class MyFilter implements Filter {
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        new_content, changed = migrator.migrate()
+
+        self.assertTrue(changed)
+        self.assertIn('@SlingServletFilter', new_content)
+        self.assertIn('scope = MyFilter.SCOPE', new_content)
+        self.assertIn('order = MyFilter.ORDER', new_content)
+        self.assertIn('pattern = MyFilter.PATTERN', new_content)
+
+    def test_sling_filter_scope_enum_mapping_with_constants(self):
+        """Test old SlingFilterScope enum constants are mapped to 
SlingServletFilterScope."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.sling.SlingFilter;
+import org.apache.felix.scr.annotations.sling.SlingFilterScope;
+
+@SlingFilter(
+    scope = SlingFilterScope.INCLUDE,
+    order = FilterOrder.ORDER,
+    pattern = FilterPatterns.REQUEST
+)
+public class MyFilter implements Filter {
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        new_content, changed = migrator.migrate()
+
+        self.assertTrue(changed)
+        self.assertIn('scope = SlingServletFilterScope.INCLUDE', new_content)
+        self.assertIn('order = FilterOrder.ORDER', new_content)
+        self.assertIn('pattern = FilterPatterns.REQUEST', new_content)
+        
self.assertIn('org.apache.sling.servlets.annotations.SlingServletFilterScope', 
new_content)
+
 
 class TestComponentMigration(unittest.TestCase):
     """Test @Component migration."""

Reply via email to