Consolidate MetricType implementations using generics and abstract base classes
Nobody has claimed this yet.
- 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
- Phase-based migration - Don't convert all at once
- Coexistence period - Old and new patterns work together
- Prove value first - Start with highest-impact consolidation
- 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:
- Create GenericSQLMetric and SQLMetricProducer
- Migrate 5-10 metrics to prove pattern
- Verify: Compare results against old implementation
- Migrate remaining SQL metrics in batches
- Delete old metric classes (80+ files removed)
- 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:
- Create GenericFieldTypeCountMetric
- Create FieldTypeMetricProducer
- Migrate in batch (all 30 at once - simple pattern)
- Delete old files
Phase 3: Container Metrics (Review Phase)
Analysis Task:
- Document current hierarchy purpose
- Measure code duplication percentage
- Propose simplification if >40% reduction possible
- 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:
- Pattern Recognition: AI excels at extracting patterns
- Bulk Generation: Generate 80+ producer methods automatically
- Verification: Compare old vs new outputs systematically
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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