Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,9 +79,16 @@ private void mapOssieToSalesforce(

// Filter mappings to get only metric-related entries
Map<String, String> metricMappings = MappingUtils.filterMappingsByPrefix(mappings, METRICS);

Map<String, Object> 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<Object> sfMetrics = getList(outputData, SEMANTIC_CALCULATED_MEASUREMENTS);
if (sfMetrics != null) {
unwrapExpressions(ossieMetrics, sfMetrics);
}
}

/**
Expand Down Expand Up @@ -118,6 +126,67 @@ private void mapSalesforceToOssie(
}


/**
* Unwraps expressions for Ossie→SF conversion, mirroring {@link #wrapExpressions}.
*
* <p>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<Object> ossieMetrics, List<Object> sfMetrics) {
for (int i = 0; i < ossieMetrics.size() && i < sfMetrics.size(); i++) {
Map<String, Object> ossieMetric = asMap(ossieMetrics.get(i));
Map<String, Object> 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<String, Object> ossieMetric, String dialect) {
Map<String, Object> expression = getMap(ossieMetric, EXPRESSION);
if (expression == null) {
return null;
}
List<Object> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,90 @@ void testCustomExtensionsRestoration() throws Exception {
}

@Test
void testMetricsNotConvertedInOssieToSalesforce() throws Exception {
void testMetricsConvertedToSemanticCalculatedMeasurements() throws Exception {
List<String> results = converter.convert(ossieYaml);
Map<String, Object> sfModel = jsonMapper.readValue(results.get(0), new TypeReference<Map<String, Object>>() {});

List<Map<String, Object>> calcMeasurements = (List<Map<String, Object>>) 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<String, Object> 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<String, Object> 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<String> results = converter.convert(yamlWithTableauMetric);
Map<String, Object> sfModel = jsonMapper.readValue(results.get(0), new TypeReference<Map<String, Object>>() {});
List<Map<String, Object>> calcMeasurements = (List<Map<String, Object>>) sfModel.get("semanticCalculatedMeasurements");

Map<String, Object> 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
Expand Down