Skip to content

Allow MetaInject to run when all input streams are empty - #9078

Merged
mattcasters merged 4 commits into
apache:mainfrom
rmannibucau:feature/metainject-condition-stream-behavior
Oct 24, 2026
Merged

mattcasters merged 4 commits into
apache:mainfrom
rmannibucau:feature/metainject-condition-stream-behavior

Conversation

@rmannibucau

@rmannibucau rmannibucau commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9077

Problem

The MetaInject (ETL Metadata Injection) transform had a single allowEmptyStreamOnExecution boolean that only distinguished whether every stream must have data (false) or at least one must (true). There was no way to run the injected template pipeline when all input streams are empty, even though templates relying on constant mappings alone are valid.

Solution

Replace the boolean with a three-state ConditionStreamBehavior enum:

  • ALL_NON_EMPTY -- run only when every stream has data (legacy false/N)
  • AT_LEAST_ONE_NON_EMPTY -- run when at least one stream has data (legacy true/Y)
  • ALWAYS -- new: run even when every input stream is empty

Backward compatibility

The old allow_empty_stream_on_execution field is removed from new serialized metadata (new files carry only condition_stream_behavior), but old files still load and map deterministically: Y/true -> AT_LEAST_ONE_NON_EMPTY, N/false -> ALL_NON_EMPTY, missing (pre-legacy) -> ALL_NON_EMPTY. An explicit Y is never confused with a missing tag.

This is implemented through a new, general core mechanism: the @OnUnknownDeserializationProperty annotation, honored by both the XML and JSON serializers, lets any annotated metadata class translate a legacy/renamed key into a current field during deserialization. ReflectionUtil returns instance-bound Consumer<UnknownProperty> callbacks built from Class.getMethods(), so:

  • only public annotated methods are discovered (private/protected ignored),
  • a public override masks the overridden parent method,
  • multiple annotated methods are all invoked,
  • a method whose signature is not exactly (UnknownProperty) fails fast with IllegalArgumentException,

with the discovery result cached per class.

UI & docs

  • The Options-tab checkbox becomes a dropdown of the three modes, with en/fr/pt_BR labels.
  • User manual documents the new "When to run the resulting pipeline" option.
  • Dev manual adds a "Legacy and renamed keys" section documenting the callback contract.

Tests

  • Core: UnknownDeserializationPropertyTest (8) -- XML + JSON firing, no-fire for annotated keys, multiple callbacks, override-masking, invalid signature -> IllegalArgumentException, non-public exclusion.
  • MetaInjectMetaTest (7) + MetaInjectTest (13) -- new-code round-trip, legacy Y/N, missing -> default, "new writes only enum" check, and the shouldSkipExecution gate matrix.

Verification

  • mvn -T1C clean test on metainject: 20 tests, 0 failures
  • Core serialization suites green; RAT + Spotless clean
  • No javadoc/doclint issues in the new code (the single core javadoc error is a pre-existing, unrelated ConfigFile.java:54)

Introduce a ConditionStreamBehavior enum that replaces the legacy
allowEmptyStreamOnExecution boolean. It adds a third ALWAYS mode so the
injected template pipeline can run even when every input stream is empty
(e.g. templates relying on constant mappings alone), while ALL_NON_EMPTY
and AT_LEAST_ONE_NON_EMPTY preserve the previous false/true semantics.

Add a general @OnUnknownDeserializationProperty callback honored by both
the XML and JSON serializers, so legacy files still carrying the old
allow_empty_stream_on_execution tag keep mapping correctly (Y/true to
AT_LEAST_ONE_NON_EMPTY, N/false to ALL_NON_EMPTY), without writing it on
new metadata. The helper returns instance-bound Consumer callbacks built
from Class.getMethods(), so only public methods are discovered and a
public override masks its parent.

New files are serialized with only the condition_stream_behavior key.
The dialog checkbox becomes a dropdown of the three modes, with en/fr/pt_BR
labels, and the user/dev manuals document the option and the callback.
Backward-compatible handling of the legacy ALLOW_EMPTY_STREAM_ON_EXECUTION
injection key and allow_empty_stream_on_execution config tag after the move
to ConditionStreamBehavior: remap the old injection key and translate
Y/N/true/false to the enum code, apply the legacy tag only when the new enum
is absent, fix unknown-value parsing in the JSON serializer, add missing i18n,
and cover the legacy paths with parameterized tests.
@rmannibucau

Copy link
Copy Markdown
Contributor Author

@mattcasters tried to address all feedbacks and increase test coverage, do you mind having another look 🙏 ?

Comment thread docs/hop-dev-manual/modules/ROOT/pages/metadata-serialization.adoc Outdated
…very lookup site

Address the round-2 review feedback on PR apache#9078: the legacy
ALLOW_EMPTY_STREAM_ON_EXECUTION injection key is now normalized to
CONDITION_STREAM_BEHAVIOR everywhere the mapping is read, not only during
the injection itself.

- collectDataForOneMapping resolves the key up front so lookupGroupKey and
  HopMetadataInjector.isTopLevelInjectionKey see the normalized key; a
  legacy mapping stored with target_detail=Y is no longer logged as
  TargetKeyNotFound and is injected under CONDITION_STREAM_BEHAVIOR.
- getUnavailableTargetKeys checks the resolved key, so the dialog no
  longer offers to delete a legacy mapping that would actually run.
- MetaInjectDialog.findMappingForTargetKey normalizes both keys so the
  source of a legacy mapping stays visible in the Inject Metadata tree.
- resolveInjectionKey is widened to protected static so the dialog (same
  package) and subclasses can reuse it while staying out of the public API.

Docs: the @OnUnknownDeserializationProperty example in the dev manual now
shows the conditionStreamBehaviorSet guard so the legacy boolean cannot
override an explicit condition_stream_behavior (current enum wins when both
tags are present).

Tests: add coverage for getUnavailableTargetKeys keeping a legacy-key
mapping, and for the target_detail=Y scalar path injecting the enum code.
@rmannibucau

Copy link
Copy Markdown
Contributor Author

Updated the PR with review feedbacks

…avior change

- ConditionStreamBehavior.isLegacyBoolean now compares with equalsIgnoreCase
  instead of default-locale toLowerCase, so an uppercase legacy TRUE still
  maps to AT_LEAST_ONE_NON_EMPTY even under a Turkish (or similar) JVM locale;
  the legacy key constant-injection test gains a mixed-case TRUE case.
- MetaInjectDialog.enterMapping builds its MappingTarget with the normalized
  target key (new package-private resolveTargetAttributeKey, delegating to
  MetaInject.resolveInjectionKey) instead of the raw saved key, so a mapping
  still stored as ALLOW_EMPTY_STREAM_ON_EXECUTION is pre-selected in the
  mapping dialog and survives a confirm instead of being dropped.
- The condition-stream-behavior CCombo is now SWT.READ_ONLY so only the enum
  descriptions can be selected; a typo or cleared field can no longer silently
  fall back to ALL_NON_EMPTY.
- Test/dialog javadocs no longer narrate the PR review history; they state the
  behavior under test, and the resolveInjectionKey javadoc lists every lookup
  site (now including the enter-mapping target row).
@mattcasters

Copy link
Copy Markdown
Contributor

Ran all the mdi integration tests and everything is green.

@mattcasters
mattcasters merged commit 266d7b4 into apache:main Oct 24, 2026
5 checks passed
@rmannibucau
rmannibucau deleted the feature/metainject-condition-stream-behavior branch October 24, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow MetaInject to run the template pipeline when all input streams are empty

2 participants