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 @@ -32,6 +32,7 @@
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.resource.ResourceCardinality;
import org.apache.nifi.components.resource.ResourceReference;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.expression.ExpressionLanguageScope;
Expand All @@ -58,6 +59,7 @@
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -98,20 +100,27 @@ public class TransformXml extends AbstractProcessor {
"XSLT Lookup key"
);

public static final PropertyDescriptor XSLT_FILE_NAME = new PropertyDescriptor.Builder()
.name("XSLT File Name")
.description("Provides the name (including full path) of the XSLT file to apply to the FlowFile XML content."
+ "One of the 'XSLT file name' and 'XSLT Lookup' properties must be defined.")
private static final List<String> OBSOLETE_XSLT_FILE_NAME = List.of(
"XSLT file name",
"XSLT File Name"
);

public static final PropertyDescriptor XSLT_DOCUMENT = new PropertyDescriptor.Builder()
.name("XSLT Document")
.description("""
Comment thread
dan-s1 marked this conversation as resolved.
Provides either the name (including full path) of the XSLT file or the actual XSLT to apply to the FlowFile XML content.
One of the 'XSLT Content' and 'XSLT Lookup' properties must be defined.""")
.required(false)
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE)
.identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE, ResourceType.TEXT)
.build();

public static final PropertyDescriptor XSLT_CONTROLLER = new PropertyDescriptor.Builder()
.name("XSLT Lookup")
.description("Controller lookup used to store XSLT definitions. One of the 'XSLT file name' and "
+ "'XSLT Lookup' properties must be defined. WARNING: note that the lookup controller service "
+ "should not be used to store large XSLT files.")
.description("""
Controller lookup used to store XSLT definitions. One of the 'XSLT Content' and
'XSLT Lookup' properties must be defined. WARNING: note that the lookup controller service
should not be used to store large XSLT files.""")
.required(false)
.identifiesControllerService(StringLookupService.class)
.build();
Expand All @@ -123,6 +132,7 @@ public class TransformXml extends AbstractProcessor {
.required(false)
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR)
.identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.TEXT)
.build();

public static final PropertyDescriptor INDENT_OUTPUT = new PropertyDescriptor.Builder()
Expand Down Expand Up @@ -160,7 +170,7 @@ public class TransformXml extends AbstractProcessor {
.build();

private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
XSLT_FILE_NAME,
XSLT_DOCUMENT,
XSLT_CONTROLLER,
XSLT_CONTROLLER_KEY,
INDENT_OUTPUT,
Expand All @@ -186,7 +196,7 @@ public class TransformXml extends AbstractProcessor {

private static final XMLStreamReaderProvider STREAM_READER_PROVIDER = new StandardXMLStreamReaderProvider();

private LoadingCache<String, Templates> cache;
private LoadingCache<ResourceReference, Templates> cache;

private volatile boolean secureProcessingEnabled;

Expand All @@ -204,16 +214,16 @@ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
protected Collection<ValidationResult> customValidate(ValidationContext validationContext) {
final List<ValidationResult> results = new ArrayList<>(super.customValidate(validationContext));

PropertyValue filename = validationContext.getProperty(XSLT_FILE_NAME);
PropertyValue document = validationContext.getProperty(XSLT_DOCUMENT);
PropertyValue controller = validationContext.getProperty(XSLT_CONTROLLER);
PropertyValue key = validationContext.getProperty(XSLT_CONTROLLER_KEY);

if ((filename.isSet() && controller.isSet())
|| (!filename.isSet() && !controller.isSet())) {
if ((document.isSet() && controller.isSet())
|| (!document.isSet() && !controller.isSet())) {
results.add(new ValidationResult.Builder()
.valid(false)
.subject(this.getClass().getSimpleName())
.explanation("Exactly one of the \"XSLT file name\" and \"XSLT controller\" properties must be defined.")
.explanation("Exactly one of the \"XSLT Content\" and \"XSLT Lookup\" properties must be defined.")
.build());
}

Expand Down Expand Up @@ -266,7 +276,7 @@ public void onScheduled(final ProcessContext context) {
cacheBuilder.expireAfterAccess(cacheTTL, TimeUnit.SECONDS);
}

cache = cacheBuilder.build(path -> newTemplates(context, path));
cache = cacheBuilder.build(resourceReference -> newTemplates(context, resourceReference));
} else {
cache = null;
logger.info("Stylesheet cache disabled because cache size is set to 0");
Expand All @@ -281,18 +291,18 @@ public void onTrigger(final ProcessContext context, final ProcessSession session
}

final StopWatch stopWatch = new StopWatch(true);
final String path = context.getProperty(XSLT_FILE_NAME).isSet()
? context.getProperty(XSLT_FILE_NAME).evaluateAttributeExpressions(original).getValue()
: context.getProperty(XSLT_CONTROLLER_KEY).evaluateAttributeExpressions(original).getValue();
final ResourceReference resourceReference = context.getProperty(XSLT_DOCUMENT).isSet()
? context.getProperty(XSLT_DOCUMENT).evaluateAttributeExpressions(original).asResource()
: context.getProperty(XSLT_CONTROLLER_KEY).evaluateAttributeExpressions(original).asResource();

try {
final FlowFile transformed = session.write(original, (inputStream, outputStream) -> {
try (final InputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
final Templates templates;
if (cache == null) {
templates = newTemplates(context, path);
templates = newTemplates(context, resourceReference);
} else {
templates = cache.get(path);
templates = cache.get(resourceReference);
}

final Transformer transformer = templates.newTransformer();
Expand All @@ -312,7 +322,14 @@ public void onTrigger(final ProcessContext context, final ProcessSession session
final Result result = new StreamResult(outputStream);
transformer.transform(source, result);
} catch (final Exception e) {
throw new IOException(String.format("XSLT Source Path [%s] Transform Failed", path), e);
final String message;
if (resourceReference.getResourceType() == ResourceType.TEXT) {
message = context.getProperty(XSLT_DOCUMENT).isSet()
? "XSLT Transform Failed" : String.format("XSLT Source Path [%s] Transform Failed", getResourceReferenceText(resourceReference));
} else {
message = String.format("XSLT Source Path [%s] Transform Failed", resourceReference.getLocation());
}
throw new IOException(message, e);
}
});
session.transfer(transformed, REL_SUCCESS);
Expand All @@ -332,19 +349,19 @@ public void migrateProperties(PropertyConfiguration config) {
config.renameProperty("secure-processing", SECURE_PROCESSING.getName());
config.renameProperty("cache-size", CACHE_SIZE.getName());
config.renameProperty("cache-ttl-after-last-access", CACHE_TTL_AFTER_LAST_ACCESS.getName());
config.renameProperty("XSLT file name", XSLT_FILE_NAME.getName());
OBSOLETE_XSLT_FILE_NAME.forEach(obsoletePropertyName -> config.renameProperty(obsoletePropertyName, XSLT_DOCUMENT.getName()));
}

private ErrorListenerLogger getErrorListenerLogger() {
return new ErrorListenerLogger(getLogger());
}

@SuppressWarnings("unchecked")
private Templates newTemplates(final ProcessContext context, final String path) throws TransformerConfigurationException, LookupFailureException {
private Templates newTemplates(final ProcessContext context, final ResourceReference resourceReference) throws TransformerConfigurationException, LookupFailureException, IOException {
final TransformerFactory transformerFactory = getTransformerFactory();
final LookupService<String> lookupService = context.getProperty(XSLT_CONTROLLER).asControllerService(LookupService.class);
final boolean filePath = context.getProperty(XSLT_FILE_NAME).isSet();
final StreamSource templateSource = getTemplateSource(lookupService, path, filePath);
final boolean xsltDocument = context.getProperty(XSLT_DOCUMENT).isSet();
final StreamSource templateSource = getTemplateSource(lookupService, resourceReference, xsltDocument);
final Source configuredTemplateSource = secureProcessingEnabled ? getSecureSource(templateSource) : templateSource;
return transformerFactory.newTemplates(configuredTemplateSource);
}
Expand All @@ -361,12 +378,14 @@ private TransformerFactory getTransformerFactory() throws TransformerConfigurati
return factory;
}

private StreamSource getTemplateSource(final LookupService<String> lookupService, final String path, final boolean filePath) throws LookupFailureException {
private StreamSource getTemplateSource(final LookupService<String> lookupService, final ResourceReference resourceReference,
final boolean xsltDocument) throws LookupFailureException, IOException {
final StreamSource streamSource;
if (filePath) {
streamSource = new StreamSource(path);
if (xsltDocument) {
streamSource = new StreamSource(resourceReference.read());
} else {
final String coordinateKey = lookupService.getRequiredKeys().iterator().next();
final String path = getResourceReferenceText(resourceReference);
final Map<String, Object> coordinates = Collections.singletonMap(coordinateKey, path);
final Optional<String> foundSource = lookupService.lookup(coordinates);
if (foundSource.isPresent() && StringUtils.isNotBlank(foundSource.get())) {
Expand Down Expand Up @@ -403,6 +422,14 @@ private Source getSecureSource(final StreamSource streamSource) throws Transform
}
}

private String getResourceReferenceText(ResourceReference resourceReference) {
try (InputStream inputStream = resourceReference.read()) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException ignored) {
return "";
}
}

private static class ErrorListenerLogger implements ErrorListener {
private final ComponentLog logger;

Expand Down
Loading
Loading