dotCMS / dotCMS/core

Consolidate MetricType implementations using generics and abstract base classes

Open
#33,983 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : Metrics dotCMS : Technical Debt Priority : 3 Average stale Team : Platform
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Description

Reduce code duplication across 128+ MetricType implementations by introducing generic base classes and leveraging template method pattern.

System Design Principles

Incremental Change Strategy
  1. Phase-based migration - Don't convert all at once
  2. Coexistence period - Old and new patterns work together
  3. Prove value first - Start with highest-impact consolidation
  4. Minimal risk - Each phase independently testable
Conservative Approach
  • DON'T require configuration files (add complexity)
  • DON'T over-engineer abstractions (YAGNI principle)
  • DO eliminate obvious duplication (80+ identical SQL metrics)
  • DO use proven patterns (already have DBMetricType interface)

Current Duplication Analysis

Pattern 1: Simple SQL Metrics (80+ classes, ~2,400 lines)

Duplication Factor: 95% identical code

Current:

// TotalSitesDatabaseMetricType.java (35 lines)
public class TotalSitesDatabaseMetricType implements DBMetricType {
    @Override
    public String getName() { return "COUNT_OF_SITES"; }

    @Override
    public String getDescription() { return "Total count of sites"; }

    @Override
    public MetricCategory getCategory() { return MetricCategory.DIFFERENTIATING_FEATURES; }

    @Override
    public MetricFeature getFeature() { return MetricFeature.SITES; }

    @Override
    public String getSqlQuery() {
        return "SELECT COUNT(id) as value FROM identifier WHERE asset_subtype='Host' AND id <> 'SYSTEM_HOST'";
    }
}

// TotalActiveSitesDatabaseMetricType.java (40 lines)
// ... IDENTICAL STRUCTURE, different values
Pattern 2: Field Type Counters (30+ classes, ~600 lines)

Duplication Factor: 90% identical code

Current:

// CountOfBinaryFieldsMetricType.java
public class CountOfBinaryFieldsMetricType extends ContentTypeFieldsMetricType {
    boolean filterCondition(Map<String, Object> map) {
        return "com.dotcms.contenttype.model.field.BinaryField".equals(map.get("field_type"));
    }

    @Override
    public String getName() { return "COUNT_BINARY_FIELDS"; }

    @Override
    public String getDescription() { return "Count the number of binary fields"; }
}

// CountOfImageFieldsMetricType.java - IDENTICAL except field type class name
// ... 28 more identical classes
Pattern 3: Container Hierarchies (18 classes, complex inheritance)

Issue: 4-level inheritance chains, protected field access patterns

Proposed Consolidation (Phase-by-Phase)

Phase 1: SQL Metric Generic (Highest Impact - Eliminate 80+ classes)

Create Generic SQL Metric:

@ApplicationScoped
public class GenericSQLMetric implements DBMetricType {
    private final String name;
    private final String description;
    private final String sqlQuery;
    private final MetricCategory category;
    private final MetricFeature feature;

    // Package-private constructor for CDI producers
    GenericSQLMetric(String name, String description, String sqlQuery,
                     MetricCategory category, MetricFeature feature) {
        this.name = name;
        this.description = description;
        this.sqlQuery = sqlQuery;
        this.category = category;
        this.feature = feature;
    }

    @Override public String getName() { return name; }
    @Override public String getDescription() { return description; }
    @Override public String getSqlQuery() { return sqlQuery; }
    @Override public MetricCategory getCategory() { return category; }
    @Override public MetricFeature getFeature() { return feature; }
}

Create CDI Producer for Metrics:

@ApplicationScoped
public class SQLMetricProducer {

    @Produces
    @ApplicationScoped
    @Named("COUNT_OF_SITES")
    public MetricType totalSites() {
        return new GenericSQLMetric(
            "COUNT_OF_SITES",
            "Total count of sites",
            "SELECT COUNT(id) as value FROM identifier WHERE asset_subtype='Host' AND id <> 'SYSTEM_HOST'",
            MetricCategory.DIFFERENTIATING_FEATURES,
            MetricFeature.SITES
        );
    }

    @Produces
    @ApplicationScoped
    @Named("COUNT_OF_ACTIVE_SITES")
    public MetricType activeSites() {
        return new GenericSQLMetric(
            "COUNT_OF_ACTIVE_SITES",
            "Total count of active sites",
            "SELECT COUNT(id) as value FROM identifier i " +
            "JOIN contentlet_version_info cvi ON i.id = cvi.identifier " +
            "WHERE asset_subtype = 'Host' AND id <> 'SYSTEM_HOST' " +
            "AND cvi.live_inode is not null",
            MetricCategory.DIFFERENTIATING_FEATURES,
            MetricFeature.SITES
        );
    }

    // Continue for other metrics...
}

Migration Strategy:

  1. Create GenericSQLMetric and SQLMetricProducer
  2. Migrate 5-10 metrics to prove pattern
  3. Verify: Compare results against old implementation
  4. Migrate remaining SQL metrics in batches
  5. Delete old metric classes (80+ files removed)
  6. Result: 80 classes → 1 generic class + 1 producer class

Risk Mitigation:

  • Keep old classes during transition
  • CDI discovers both old and new (but named differently)
  • Gradual migration per metric type
  • Easy rollback (just delete new producer methods)
Phase 2: Field Type Generic (Eliminate 30+ classes)

Create Generic Field Counter:

@ApplicationScoped
public class GenericFieldTypeCountMetric extends ContentTypeFieldsMetricType {
    private final String fieldTypeClass;
    private final String name;
    private final String description;

    GenericFieldTypeCountMetric(String fieldTypeClass, String name, String description) {
        this.fieldTypeClass = fieldTypeClass;
        this.name = name;
        this.description = description;
    }

    @Override
    boolean filterCondition(Map<String, Object> map) {
        return fieldTypeClass.equals(map.get("field_type"));
    }

    @Override
    public String getName() { return name; }

    @Override
    public String getDescription() { return description; }
}

CDI Producer:

@ApplicationScoped
public class FieldTypeMetricProducer {

    @Produces
    @ApplicationScoped
    @Named("COUNT_BINARY_FIELDS")
    public MetricType binaryFields() {
        return new GenericFieldTypeCountMetric(
            "com.dotcms.contenttype.model.field.BinaryField",
            "COUNT_BINARY_FIELDS",
            "Count the number of binary fields"
        );
    }

    @Produces
    @ApplicationScoped
    @Named("COUNT_IMAGE_FIELDS")
    public MetricType imageFields() {
        return new GenericFieldTypeCountMetric(
            "com.dotcms.contenttype.model.field.ImageField",
            "COUNT_IMAGE_FIELDS",
            "Count the number of image fields"
        );
    }

    // ... 28 more producer methods (simple copy-paste pattern)
}

Result: 30 classes → 1 generic class + 1 producer class

Phase 3: Simplify Container Hierarchies (Review & Selective Refactoring)

Conservative Approach:

  • DON'T force consolidation if inheritance makes sense
  • DO flatten unnecessarily deep hierarchies
  • DO replace inheritance with composition where appropriate

Analysis First:

// Current 4-level hierarchy:
TotalContainersInTemplateDatabaseMetricType (abstract base)
  → TotalContainersInLiveTemplatesDatabaseMetricType (abstract)
    → TotalStandardContainersInLiveTemplatesDatabaseMetricType (concrete)
    → TotalFileContainersInLiveTemplatesDatabaseMetricType (concrete)

Possible Simplification (if worthwhile):

// Generic approach with strategy pattern
@ApplicationScoped
public class ContainerCountMetric implements MetricType {
    private final ContainerFilter filter;
    private final TemplateSelector templateSelector;
    private final String name;
    // ...

    enum ContainerFilter { STANDARD, FILE, ALL }
    enum TemplateSelector { LIVE, WORKING, LIVE_PAGES }
}

Decision Criteria:

  • Only refactor if reduces code by >40%
  • Must improve readability
  • If inheritance is clear and works, leave it alone

Implementation Steps (AI-Friendly)

Phase 1: SQL Metrics (3-4 hours)

Step 1: Create Infrastructure

# Create new files
dotCMS/src/main/java/com/dotcms/telemetry/collectors/generic/GenericSQLMetric.java
dotCMS/src/main/java/com/dotcms/telemetry/collectors/generic/SQLMetricProducer.java

Step 2: Migrate First Batch (Proof of Concept)

# Migrate site metrics (5 metrics)
# Compare: dotCMS/src/main/java/com/dotcms/telemetry/collectors/site/Total*
# Create producer methods in SQLMetricProducer
# Test: Verify metrics collected with same values

Step 3: Bulk Migration Script
AI can generate producer methods from existing classes:

# Pseudo-code for AI to implement
for metric_file in sql_metric_files:
    name = extract_name(metric_file)
    description = extract_description(metric_file)
    sql = extract_sql_query(metric_file)
    category = extract_category(metric_file)
    feature = extract_feature(metric_file)

    generate_producer_method(name, description, sql, category, feature)

Step 4: Cleanup

# After verification, delete old files
rm dotCMS/src/main/java/com/dotcms/telemetry/collectors/site/Total*.java
rm dotCMS/src/main/java/com/dotcms/telemetry/collectors/language/Total*.java
# ... etc
Phase 2: Field Type Metrics (2-3 hours)

Similar approach:

  1. Create GenericFieldTypeCountMetric
  2. Create FieldTypeMetricProducer
  3. Migrate in batch (all 30 at once - simple pattern)
  4. Delete old files
Phase 3: Container Metrics (Review Phase)

Analysis Task:

  1. Document current hierarchy purpose
  2. Measure code duplication percentage
  3. Propose simplification if >40% reduction possible
  4. Decision point: Refactor only if clear benefit

Risk Mitigation

Low Risk Phases
  • Phase 1 & 2: Clear duplication elimination
  • Producer pattern well-understood in dotCMS
  • Each metric independently verifiable
Testing Strategy
@Test
public void testGenericSQLMetricMatchesOriginal() {
    // Old implementation
    TotalSitesDatabaseMetricType oldMetric = new TotalSitesDatabaseMetricType();
    Object oldValue = oldMetric.getValue().get();

    // New implementation
    MetricType newMetric = sqlProducer.totalSites();
    Object newValue = newMetric.getValue().get();

    assertEquals(oldValue, newValue);
}
Coexistence Period
  • Keep both implementations for 1-2 sprints
  • Run both in parallel, compare results
  • Switch after confidence built
  • Delete old code

Code Reduction Impact

Before
  • 128 metric classes
  • ~6,900 lines of code
  • High duplication (80+ nearly identical)
After
  • ~48 metric classes (unique logic only)
  • ~2,500 lines of code
  • 2 generic classes + 2 producer classes
  • Reduction: 80 classes, ~4,400 lines
Maintenance Benefit

Adding new metric:

Before:

# Create new file (35 lines)
# Implement 5 methods
# Add to MetricStatsCollector static block
# Total: 3 files touched, ~35 lines

After:

// Add one producer method (7 lines)
@Produces
@ApplicationScoped
@Named("NEW_METRIC")
public MetricType newMetric() {
    return new GenericSQLMetric(...);
}
// Total: 1 file touched, ~7 lines

Benefits

  • Eliminate ~4,400 lines of duplicated code
  • Reduce maintenance burden (fewer files)
  • Easier to add new metrics (producer method only)
  • Better testability (generic classes well-tested)
  • Complements issue #33980 - Generic metrics easier to cache

Files to Create

Phase 1
  • collectors/generic/GenericSQLMetric.java (~50 lines)
  • collectors/generic/SQLMetricProducer.java (~800 lines, but simple pattern)
Phase 2
  • collectors/generic/GenericFieldTypeCountMetric.java (~40 lines)
  • collectors/generic/FieldTypeMetricProducer.java (~300 lines)

Files to Delete (After Migration)

Phase 1 (80 files)
  • All simple SQL metric implementations in:
    • collectors/site/ (20+ files)
    • collectors/language/ (6 files)
    • collectors/workflow/ (6 files)
    • collectors/template/ (5 files)
    • collectors/theme/ (8 files)
    • collectors/experiment/ (15 files)
    • collectors/sitesearch/ (3 files)
    • collectors/urlmap/ (4 files)
    • collectors/user/ (4 files)
    • collectors/content/ (5 files)
Phase 2 (30 files)
  • All field type counter implementations in:
    • collectors/contenttype/CountOf*FieldsMetricType.java

Acceptance Criteria

Phase 1: SQL Metrics
  • GenericSQLMetric created and tested
  • SQLMetricProducer with all migrated metrics
  • All migrated metrics return identical values to original
  • Old SQL metric classes deleted
  • CDI discovers all producer methods
  • Integration tests pass
Phase 2: Field Type Metrics
  • GenericFieldTypeCountMetric created and tested
  • FieldTypeMetricProducer with all 30 field types
  • Results match original implementations
  • Old field counter classes deleted
  • Tests pass
Phase 3: Container Metrics (Optional)
  • Analysis document created
  • Decision made (refactor or keep as-is)
  • If refactored: Tests pass, code reduced by >40%

Success Metrics

  • Code reduction: ~4,400 lines eliminated
  • File reduction: ~110 files deleted
  • Maintainability: New metric requires 1 producer method vs 1 file
  • Zero regression: All metrics return same values
  • Foundation ready: For caching (#33980) and testing (#33982)

Additional Notes

Dependencies
  • REQUIRES: Issue #33979 (CDI refactoring) must be complete
  • ENABLES: Issue #33982 (easier to test generic classes)
  • COMPLEMENTS: Issue #33980 (generic metrics easier to cache)
AI Implementation Assistance

This issue is IDEAL for AI implementation:

  1. Pattern Recognition: AI excels at extracting patterns
  2. Bulk Generation: Generate 80+ producer methods automatically
  3. Verification: Compare old vs new outputs systematically
  4. Code Cleanup: Delete old files after verification

AI Can:

  • Extract SQL, name, description from existing files
  • Generate producer methods in correct format
  • Create comparison tests
  • Identify files for deletion
  • Run regression test suite

Human Should:

  • Review generated code for correctness
  • Make final decision on Phase 3
  • Approve deletion of old files
  • Verify business logic unchanged
Conservative Approach Rationale
  • NO configuration files - Adds complexity, dependency hell
  • NO YAML/JSON - Another thing to parse, validate, debug
  • YES to CDI producers - Already in codebase, well-understood
  • YES to Java code - Type-safe, IDE-friendly, refactorable

This keeps changes simple, safe, and incrementally adoptable.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the existing Total* metric classes under dotCMS/src/main/java/com/dotcms/telemetry/collectors/, the DBMetricType interface, and the MetricStatsCollector registration mentioned in the issue. Compare a small site-metric batch with the proposed GenericSQLMetric and SQLMetricProducer, then verify migrated metrics retain the old values before expanding the migration; done means the selected phases are independently verified and obsolete classes are removed.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.