diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/ConverterConstants.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/ConverterConstants.java index 044dfe4c..02c0bb26 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/ConverterConstants.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/ConverterConstants.java @@ -89,6 +89,7 @@ public enum Level { public static final String DIALECTS = "dialects"; public static final String DIALECT = "dialect"; public static final String DIALECT_TABLEAU = "TABLEAU"; + public static final String DIALECT_ANSI_SQL = "ANSI_SQL"; // Relationship properties public static final String CRITERIA = "criteria"; diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java index 6add2b9b..00de289d 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java @@ -24,6 +24,7 @@ import org.apache.ossie.converter.ConverterConstants.Level; import org.apache.ossie.converter.pipeline.PipelineStep; +import org.apache.ossie.exception.ConversionException; import java.util.*; import org.apache.ossie.util.MappingUtils; @@ -78,9 +79,16 @@ private void mapOssieToSalesforce( // Filter mappings to get only metric-related entries Map metricMappings = MappingUtils.filterMappingsByPrefix(mappings, METRICS); + + Map mappedData = GenericMappingEngine.applyMappings(sourceData, metricMappings); metricMappings.keySet().forEach(mappings::remove); - logger.debug("Metrics are not mapped in Ossie to Salesforce direction"); + outputData.putAll(mappedData); + + List sfMetrics = getList(outputData, SEMANTIC_CALCULATED_MEASUREMENTS); + if (sfMetrics != null) { + unwrapExpressions(ossieMetrics, sfMetrics); + } } /** @@ -118,6 +126,67 @@ private void mapSalesforceToOssie( } + /** + * Unwraps expressions for Ossie→SF conversion, mirroring {@link #wrapExpressions}. + * + *

Picks an expression out of each Ossie metric's {@code expression.dialects[]} and + * flattens it into the Salesforce metric's {@code expression} string. {@code TABLEAU} is + * preferred (it is what Salesforce/Tableau CRM itself speaks); a model authored without one + * falls back to {@code ANSI_SQL} best-effort, since resolving/rewriting an expression into + * TABLEAU syntax is the scope of #222's expression-language work, not this fix. A metric with + * neither dialect fails the conversion rather than being silently omitted (#399). + */ + private void unwrapExpressions(List ossieMetrics, List sfMetrics) { + for (int i = 0; i < ossieMetrics.size() && i < sfMetrics.size(); i++) { + Map ossieMetric = asMap(ossieMetrics.get(i)); + Map sfMetric = asMap(sfMetrics.get(i)); + + String expressionValue = extractExpression(ossieMetric, DIALECT_TABLEAU); + if (expressionValue == null) { + expressionValue = extractExpression(ossieMetric, DIALECT_ANSI_SQL); + if (expressionValue != null) { + logger.warn( + "Metric '{}' has no TABLEAU-dialect expression; exporting its " + + "ANSI_SQL expression to Salesforce unresolved/untranslated", + getString(ossieMetric, NAME)); + } + } + if (expressionValue == null) { + throw new ConversionException( + "Metric '" + getString(ossieMetric, NAME) + "' has neither a TABLEAU nor " + + "an ANSI_SQL expression to export to Salesforce; add one to " + + "expression.dialects[] or remove the metric."); + } + sfMetric.put(EXPRESSION, expressionValue); + + String datatype = SalesforceDataTypeMapper.toSalesforce(getString(ossieMetric, OSSIE_DATATYPE)); + if (datatype != null) { + sfMetric.put(DATA_TYPE, datatype); + } + } + } + + /** + * Finds the given dialect's expression string in an Ossie metric's + * {@code expression.dialects[]}, or {@code null} when the metric has no expression or no + * entry for that dialect. + */ + private String extractExpression(Map ossieMetric, String dialect) { + Map expression = getMap(ossieMetric, EXPRESSION); + if (expression == null) { + return null; + } + List dialects = getList(expression, DIALECTS); + if (dialects == null) { + return null; + } + return streamMaps(dialects) + .filter(d -> dialect.equals(getString(d, DIALECT))) + .map(d -> getString(d, EXPRESSION)) + .findFirst() + .orElse(null); + } + /** * Wraps expressions for SF→Ossie conversion. */ diff --git a/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java b/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java index 6d5c1237..627637f0 100644 --- a/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java +++ b/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java @@ -287,12 +287,90 @@ void testCustomExtensionsRestoration() throws Exception { } @Test - void testMetricsNotConvertedInOssieToSalesforce() throws Exception { + void testMetricsConvertedToSemanticCalculatedMeasurements() throws Exception { List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> calcMeasurements = (List>) sfModel.get("semanticCalculatedMeasurements"); - assertNull(calcMeasurements, "Metrics from Ossie are not converted to semanticCalculatedMeasurements in Ossie->SF direction"); + assertNotNull(calcMeasurements, "Metrics from Ossie should convert to semanticCalculatedMeasurements"); + assertEquals(2, calcMeasurements.size()); + + Map totalRevenue = calcMeasurements.stream() + .filter(m -> "total_revenue".equals(m.get("apiName"))) + .findFirst() + .orElse(null); + assertNotNull(totalRevenue); + assertEquals("Sum of all order amounts", totalRevenue.get("description")); + assertEquals("Number", totalRevenue.get("dataType")); + // The fixture's metrics only carry an ANSI_SQL dialect (no TABLEAU) -- falls back to + // exporting it unresolved/untranslated rather than failing the whole conversion. + assertEquals("SUM([Orders].[amount])", totalRevenue.get("expression")); + + Map avgOrderValue = calcMeasurements.stream() + .filter(m -> "avg_order_value".equals(m.get("apiName"))) + .findFirst() + .orElse(null); + assertNotNull(avgOrderValue); + assertEquals("AVG([Orders].[amount])", avgOrderValue.get("expression")); + } + + @Test + void testMetricExpressionPrefersTableauDialectOverAnsiSql() throws Exception { + // Normalize line endings first: the fixture file may check out with CRLF depending on + // the platform's autocrlf setting, but the substitution below is written with LF. + String yamlWithTableauMetric = ossieYaml.replace("\r\n", "\n").replace( + " metrics:\n" + + " - description: Sum of all order amounts\n" + + " name: total_revenue\n" + + " datatype: Decimal\n" + + " expression:\n" + + " dialects:\n" + + " - dialect: ANSI_SQL\n" + + " expression: SUM([Orders].[amount])\n", + " metrics:\n" + + " - description: Sum of all order amounts\n" + + " name: total_revenue\n" + + " datatype: Decimal\n" + + " expression:\n" + + " dialects:\n" + + " - dialect: ANSI_SQL\n" + + " expression: SUM([Orders].[amount])\n" + + " - dialect: TABLEAU\n" + + " expression: SUM(Orders.amount)\n"); + assertTrue(yamlWithTableauMetric.contains("dialect: TABLEAU"), "fixture text substitution did not match"); + + List results = converter.convert(yamlWithTableauMetric); + Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); + List> calcMeasurements = (List>) sfModel.get("semanticCalculatedMeasurements"); + + Map totalRevenue = calcMeasurements.stream() + .filter(m -> "total_revenue".equals(m.get("apiName"))) + .findFirst() + .orElse(null); + assertNotNull(totalRevenue); + assertEquals("SUM(Orders.amount)", totalRevenue.get("expression"), + "TABLEAU dialect should be preferred over ANSI_SQL when both are present"); + } + + @Test + void testMetricWithNoConvertibleDialectFailsConversion() throws Exception { + // Normalize line endings first: the fixture file may check out with CRLF depending on + // the platform's autocrlf setting, but the substitution below is written with LF. + // The Ossie schema requires every metric to have an expression and restricts `dialect` + // to its own enum, so this uses BIGQUERY (a valid dialect, but neither TABLEAU nor + // ANSI_SQL) rather than omitting the expression or inventing an unrecognized dialect. + String yamlWithUnconvertibleDialect = ossieYaml.replace("\r\n", "\n").replace( + " - dialect: ANSI_SQL\n" + + " expression: SUM([Orders].[amount])\n", + " - dialect: BIGQUERY\n" + + " expression: SUM(Orders.amount)\n"); + assertTrue(yamlWithUnconvertibleDialect.contains("dialect: BIGQUERY"), + "fixture text substitution did not match"); + + Exception exception = + assertThrows(Exception.class, () -> converter.convert(yamlWithUnconvertibleDialect)); + String message = exception.getMessage() != null ? exception.getMessage() : exception.getCause().getMessage(); + assertTrue(message.contains("total_revenue"), "error should name the unconvertible metric: " + message); } @Test