oscerd commented on code in PR #27440:
URL: https://github.com/apache/camel/pull/27440#discussion_r4194835311


##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureConfiguration.java:
##########
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+
+@UriParams
+public class OpenFeatureConfiguration implements Cloneable {
+
+    @UriParam(label = "common",
+              description = "The feature flag key to evaluate. Can be 
overridden per message via the CamelOpenFeatureFlagKey header.")
+    private String flagKey;
+
+    @UriParam(label = "common", defaultValue = "false",
+              description = "Default value when flag evaluation fails. When 
evaluationType is not set,"
+                            + " also determines the evaluation type: \"true\" 
or \"false\" (case-insensitive)"
+                            + " selects boolean evaluation, any other value 
selects string evaluation.")
+    private String defaultValue = "false";
+
+    @UriParam(label = "common", enums = "boolean,variant",
+              description = "The evaluation type. When set to 'boolean', 
boolean evaluation is used (getBooleanValue)."
+                            + " When set to 'variant', string evaluation is 
used (getStringValue)."
+                            + " When not set, the type is inferred from 
defaultValue.")
+    private String evaluationType;
+
+    @UriParam(label = "producer",
+              description = "Store the evaluation result in this exchange 
property, preserving the original message body.")
+    private String resultProperty;
+
+    @UriParam(label = "common", description = "Remote flagd service host. When 
set, the flagd RPC resolver is used.")
+    private String host;
+
+    @UriParam(label = "common", defaultValue = "8013", description = "Remote 
flagd service port.")
+    private int port = 8013;
+
+    @UriParam(label = "common",
+              description = "A JSON object defining feature flags in flagd 
format (inline). Mutually exclusive with flagsResource.")
+    private String flags;
+
+    @UriParam(label = "common",
+              description = "Camel resource URI pointing to a feature flag 
definition file in flagd format."
+                            + " Mutually exclusive with flags and provider.")
+    private String flagsResource;
+
+    @UriParam(label = "common",
+              description = "Bean reference to a custom FeatureProvider (e.g. 
#myProvider)."
+                            + " Mutually exclusive with flags and 
flagsResource.")
+    private String provider;
+
+    public String getFlagKey() {
+        return flagKey;
+    }
+
+    /** The feature flag key to evaluate. */
+    public void setFlagKey(String flagKey) {
+        this.flagKey = flagKey;
+    }
+
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    /**
+     * Default value when flag evaluation fails. When evaluationType is not 
set, also determines the evaluation type:
+     * "true" or "false" (case-insensitive) selects boolean evaluation, any 
other value selects string evaluation.
+     */
+    public void setDefaultValue(String defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+
+    public String getEvaluationType() {
+        return evaluationType;
+    }
+
+    /**
+     * The evaluation type. When set to 'boolean', boolean evaluation is used. 
When set to 'variant', string evaluation
+     * is used. When not set, the type is inferred from defaultValue.
+     */
+    public void setEvaluationType(String evaluationType) {
+        this.evaluationType = evaluationType;
+    }
+
+    public String getResultProperty() {
+        return resultProperty;
+    }
+
+    /** Store the evaluation result in this exchange property, preserving the 
original message body. */
+    public void setResultProperty(String resultProperty) {
+        this.resultProperty = resultProperty;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    /** Remote flagd service host. When set, the flagd RPC resolver is used. */
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    /** Remote flagd service port. */
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public String getFlags() {
+        return flags;
+    }
+
+    /** A JSON object defining feature flags in flagd format (inline). 
Mutually exclusive with flagsResource. */
+    public void setFlags(String flags) {
+        this.flags = flags;
+    }
+
+    public String getFlagsResource() {
+        return flagsResource;
+    }
+
+    /**
+     * Camel resource URI pointing to a feature flag definition file in flagd 
format. Mutually exclusive with flags and
+     * provider.
+     */
+    public void setFlagsResource(String flagsResource) {
+        this.flagsResource = flagsResource;
+    }
+
+    public String getProvider() {
+        return provider;
+    }
+
+    /**
+     * Bean reference to a custom FeatureProvider (e.g. #myProvider). Mutually 
exclusive with flags and flagsResource.
+     */
+    public void setProvider(String provider) {
+        this.provider = provider;
+    }
+
+    public boolean isBooleanEvaluation(Exchange exchange, MutableContext ctx) {
+        String evalType = 
exchange.getIn().getHeader(OpenFeatureConstants.EVALUATION_TYPE, String.class);
+        if (evalType == null) {
+            evalType = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_TYPE, String.class);
+        }
+        if (evalType == null) {
+            evalType = evaluationType;
+        }
+
+        if ("boolean".equalsIgnoreCase(evalType)) {
+            return true;
+        }
+        if ("variant".equalsIgnoreCase(evalType)) {
+            return false;
+        }
+        if (!ctx.asMap().isEmpty()) {

Review Comment:
   **Boolean flags are evaluated as strings whenever an evaluation context is 
present**
   
   When `evaluationType` is not set (the documented default for boolean flags), 
this branch switches to string evaluation as soon as the context is non-empty. 
`MutableContext.setTargetingKey` stores the key in the same attribute map, so 
setting `CamelOpenFeatureTargetingKey`, passing 
`CamelOpenFeatureEvaluationContext`, or just having a `Map` body (e.g. after 
`unmarshal().json()`) is enough. A boolean flag evaluated as a string fails 
with TYPE_MISMATCH in flagd, the SDK client falls back to the default, and the 
producer returns the **String** `"false"` instead of the flag value. For 
example, a boolean rollout with `fractional` targeting evaluated with 
`.setHeader("CamelOpenFeatureTargetingKey", simple("${header.userId}"))` 
returns `"false"` for every user, and the docs example 
`exchangeProperty("enrichEnabled").isEqualTo(true)` stops matching once a 
targeting key is added. No test covers this branch (every context-based 
producer test sets `defaultValue=v1`). Deriving the type only fr
 om `evaluationType`/`defaultValue`, plus a producer test evaluating a boolean 
flag with a targeting key and with a Map body, would fix it.



##########
components/camel-openfeature/src/main/java/org/apache/camel/language/openfeature/OpenFeatureLanguage.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.openfeature;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.component.openfeature.OpenFeatureConstants;
+import org.apache.camel.component.openfeature.OpenFeatureEndpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Language;
+import org.apache.camel.support.ExpressionAdapter;
+import org.apache.camel.support.ExpressionToPredicateAdapter;
+import org.apache.camel.support.LanguageSupport;
+
+/** Evaluates a feature flag as a boolean predicate for use in EIP constructs 
such as filter, choice, and validate. */
+@Language(value = "openfeature", modelName = "language")
+@Metadata(title = "OpenFeature", description = "Evaluate a feature flag as a 
boolean predicate",
+          label = "language,cloud", firstVersion = "4.23.0")
+public class OpenFeatureLanguage extends LanguageSupport {
+
+    private String endpoint = "openfeature:flags";
+
+    @Override
+    public Predicate createPredicate(String expression) {
+        return createPredicate(expression, null);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Predicate createPredicate(String expression, Object[] properties) {
+        validateExpression(expression);
+        Evaluation answer = new Evaluation(
+                expression,
+                property(String.class, properties, 0, endpoint),
+                property(String.class, properties, 1, null),
+                property(Map.class, properties, 2, null),
+                property(String.class, properties, 3, "boolean"));
+        if (getCamelContext() != null) {
+            answer.init(getCamelContext());
+        }
+        return ExpressionToPredicateAdapter.toPredicate(answer);
+    }
+
+    @Override
+    public Expression createExpression(String expression) {
+        return createExpression(expression, new Object[] {});
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public ExpressionAdapter createExpression(String expression, Object[] 
properties) {
+        validateExpression(expression);
+        Evaluation answer = new Evaluation(
+                expression,
+                property(String.class, properties, 0, endpoint),
+                property(String.class, properties, 1, null),
+                property(Map.class, properties, 2, null),
+                property(String.class, properties, 3, "variant"));
+        if (getCamelContext() != null) {
+            answer.init(getCamelContext());
+        }
+        return answer;
+    }
+
+    public boolean validateExpression(String expression) {
+        if (expression == null || expression.isBlank()) {
+            throw new IllegalArgumentException("OpenFeature flag key must not 
be null or blank");
+        }
+        return true;
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    /** Managed endpoint URI. Defaults to openfeature:flags, inheriting 
camel.component.openfeature settings. */
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    private static final class Evaluation extends ExpressionAdapter {
+        private final String flagKey;
+        private final String endpointUri;
+        private final String targetingKey;
+        private final Map<String, Object> contextMap;
+        private OpenFeatureEndpoint endpoint;
+        private boolean useFlagKeyHeader;
+        private String evaluationType;
+
+        private Evaluation(String flagKey, String endpointUri, String 
targetingKey,
+                           Map<String, Object> contextMap, String 
evaluationType) {
+            this.flagKey = flagKey;
+            this.endpointUri = endpointUri;
+            this.targetingKey = targetingKey;
+            this.contextMap = contextMap;
+            this.evaluationType = evaluationType;
+        }
+
+        @Override
+        public void init(CamelContext context) {
+            super.init(context);
+            if (endpointUri == null || 
!endpointUri.startsWith("openfeature:")) {
+                throw new IllegalArgumentException("OpenFeature language 
endpoint must be an openfeature: URI");
+            }
+            String uri = endpointUri;
+            if (uri.contains("flagKey=")) {
+                useFlagKeyHeader = true;
+            } else {
+                uri += (uri.contains("?") ? "&" : "?") + "flagKey=" + flagKey;
+                useFlagKeyHeader = false;
+            }
+            endpoint = context.getEndpoint(uri, OpenFeatureEndpoint.class);
+        }
+
+        @Override
+        public <T> T evaluate(Exchange exchange, Class<T> type) {
+            if (Boolean.class.isAssignableFrom(type)) {
+                evaluationType = "boolean";
+            }
+            return super.evaluate(exchange, type);
+        }
+
+        @Override
+        public Object evaluate(Exchange exchange) {
+            try {
+                if (endpoint == null) {
+                    throw new IllegalStateException("OpenFeature expression 
must be initialized");
+                }
+                if (useFlagKeyHeader) {

Review Comment:
   **The language passes its settings through exchange properties: they leak 
into later calls, and headers override them**
   
   `Evaluation.evaluate` stores the flag key, targeting key, context and 
evaluation type as exchange properties and never removes them, while the 
endpoint resolves each setting as header → exchange property → endpoint 
configuration. So:
   - **They leak into later steps**: after `.filter().language("openfeature", 
"enrichment-enabled")`, a later 
`.to("openfeature:flags?flagKey=checkout-banner&evaluationType=variant&resultProperty=banner")`
 ignores its own `evaluationType=variant` because the leftover `boolean` 
property wins; the leaked targeting key/context/flag key feed unrelated 
evaluations the same way.
   - **Headers override the expression**: a `CamelOpenFeatureFlagKey` or 
`CamelOpenFeatureEvaluationType` header already on the message (e.g. from an 
earlier producer call) takes precedence, so `language("openfeature", "a")` can 
evaluate flag `b`.
   
   `TypeSafeAiLanguage`, which this follows, keeps per-expression settings in 
its `Evaluation` and calls the endpoint with explicit arguments. An 
`OpenFeatureEndpoint.evaluate(Exchange, flagKey, evaluationType, targetingKey, 
contextMap)` overload that leaves the exchange untouched would fix both, and 
would also avoid appending the unencoded flag key to the endpoint URI in `init` 
(which breaks the URI or injects options when a key contains `&`, `+`, `#` or 
`%`) and the one-endpoint-per-flag-key fan-out.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();

Review Comment:
   **Endpoints that share a domain silently use the first registered provider, 
yet each still builds its own**
   
   `resolveProvider()` always runs, but `registerProviderAndGetClient` only 
binds the provider of the first endpoint started for a domain. With 
`to("openfeature:flags?flagKey=a&flagsResource=classpath:a.json")` and 
`to("openfeature:flags?flagKey=b&flagsResource=classpath:b.json")`, the second 
endpoint evaluates against `a.json`, gets FLAG_NOT_FOUND and returns 
`defaultValue`, with no warning (the same with different `provider=#...` beans 
on one domain, and the docs use `openfeature:flags` almost everywhere). Every 
other endpoint still creates a provider that is never used: a temp-file copy, 
or with `host` a `FlagdProvider` whose constructor eagerly builds a gRPC 
channel that is not closed when the never-initialized provider is shut down. 
The language makes this the common case, since it creates one endpoint per flag 
key. Resolving the provider once per domain in the component, and failing fast 
(or at least a WARN) when an endpoint's provider settings differ from the bound 
one, would 
 fix it.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();
+        resolvedProvider = provider;
+        client = getComponent().registerProviderAndGetClient(domain, provider);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+
+        if (ownedProvider && resolvedProvider != null) {
+            resolvedProvider.shutdown();
+        }
+        resolvedProvider = null;
+
+        File tmp = tempFlagFile;
+        tempFlagFile = null;
+        if (tmp != null) {
+            Files.deleteIfExists(tmp.toPath());
+        }
+
+        super.doStop();
+    }
+
+    @SuppressWarnings("unchecked")
+    MutableContext buildContext(Exchange exchange) {
+        MutableContext ctx = new MutableContext();
+
+        // 1. CamelOpenFeatureEvaluationContext header
+        Object contextHeader = 
exchange.getMessage().getHeader(OpenFeatureConstants.EVALUATION_CONTEXT);
+        if (contextHeader == null) {
+            contextHeader = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_CONTEXT);
+        }
+        if (contextHeader instanceof Map) {
+            addMapToContext(ctx, (Map<String, Object>) contextHeader);
+        } else {
+            // Fallback to body map entries
+            Object body = exchange.getMessage().getBody();
+            if (body instanceof Map) {
+                addMapToContext(ctx, (Map<String, Object>) body);
+            }
+        }
+
+        // 2. CamelOpenFeatureTargetingKey header or exchange property
+        String targetingKey = 
exchange.getMessage().getHeader(OpenFeatureConstants.TARGETING_KEY, 
String.class);
+        if (targetingKey == null) {
+            targetingKey = 
exchange.getProperty(OpenFeatureConstants.TARGETING_KEY, String.class);
+        }
+        if (targetingKey != null) {
+            ctx.setTargetingKey(targetingKey);
+        }
+
+        return ctx;
+    }
+
+    private static void addMapToContext(MutableContext ctx, Map<String, 
Object> map) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue() != null ? 
String.valueOf(entry.getValue()) : "";

Review Comment:
   **Evaluation-context values are converted to strings, which breaks targeting 
on booleans, numbers and nulls**
   
   Every context entry goes through `String.valueOf(...)` and `null` becomes 
`""`, although `CamelOpenFeatureEvaluationContext` is documented as 
`Map<String, Object>` and the SDK supports typed values. flagd evaluates 
targeting with JsonLogic, where any non-empty string is truthy and strict 
equality depends on type: `{"if": [{"var": "beta"}, "on", "off"]}` with 
`beta=false` returns `on`, `{"===": [{"var": "age"}, 30]}` never matches 
`"30"`, and `{"==": [{"var": "x"}, null]}` no longer matches a null attribute. 
Building a `Map<String, Value>` with `Value.objectToValue(v)` (falling back to 
`String.valueOf` for unsupported types such as `BigDecimal`) and creating the 
context with `new MutableContext(targetingKey, attributes)` would keep the 
types.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureComponent.java:
##########
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("openfeature")
+public class OpenFeatureComponent extends DefaultComponent {
+
+    @Metadata
+    private OpenFeatureConfiguration configuration = new 
OpenFeatureConfiguration();
+
+    private volatile OpenFeatureAPI api;
+    private final Set<String> initializedDomains = 
ConcurrentHashMap.newKeySet();
+
+    @Override
+    protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) throws Exception {
+        OpenFeatureEndpoint endpoint = new OpenFeatureEndpoint(uri, this, 
remaining, configuration.copy());
+        setProperties(endpoint, parameters);
+        return endpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        api = OpenFeatureAPI.createIsolated();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        OpenFeatureAPI a = api;
+        api = null;
+        initializedDomains.clear();
+        if (a != null) {
+            a.shutdown();
+        }
+        super.doStop();
+    }
+
+    synchronized Client registerProviderAndGetClient(String domain, 
FeatureProvider provider) throws Exception {
+        if (initializedDomains.add(domain)) {

Review Comment:
   **Domain registration is never released, so after the registering endpoint 
stops the domain stays bound to a shut-down provider**
   
   `initializedDomains` only grows (cleared only in the component's `doStop`). 
When the endpoint that registered a domain's provider stops (`removeRoute`, or 
a route reload in `camel run --dev`), its `doStop` shuts the provider down and 
deletes the temp file, but the provider stays bound in the API and the domain 
stays in the set. Other endpoints on that domain (including all 
`language("openfeature", ...)` endpoints) keep using the shut-down provider, 
and a re-created endpoint's fresh provider is never registered because 
`initializedDomains.add(domain)` now returns false. The entry is also added 
before `setProviderAndWait`, so a failed registration (e.g. flagd not reachable 
within the init deadline) is never retried. Reference-counting endpoints per 
domain (register on first start, add the entry only after `setProviderAndWait` 
succeeds, rebind or shut down when the last user stops) would fix it.



##########
components/camel-openfeature/src/main/java/org/apache/camel/language/openfeature/OpenFeatureLanguage.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.openfeature;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.component.openfeature.OpenFeatureConstants;
+import org.apache.camel.component.openfeature.OpenFeatureEndpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Language;
+import org.apache.camel.support.ExpressionAdapter;
+import org.apache.camel.support.ExpressionToPredicateAdapter;
+import org.apache.camel.support.LanguageSupport;
+
+/** Evaluates a feature flag as a boolean predicate for use in EIP constructs 
such as filter, choice, and validate. */
+@Language(value = "openfeature", modelName = "language")
+@Metadata(title = "OpenFeature", description = "Evaluate a feature flag as a 
boolean predicate",
+          label = "language,cloud", firstVersion = "4.23.0")
+public class OpenFeatureLanguage extends LanguageSupport {
+
+    private String endpoint = "openfeature:flags";
+
+    @Override
+    public Predicate createPredicate(String expression) {
+        return createPredicate(expression, null);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Predicate createPredicate(String expression, Object[] properties) {
+        validateExpression(expression);
+        Evaluation answer = new Evaluation(
+                expression,
+                property(String.class, properties, 0, endpoint),
+                property(String.class, properties, 1, null),
+                property(Map.class, properties, 2, null),
+                property(String.class, properties, 3, "boolean"));
+        if (getCamelContext() != null) {
+            answer.init(getCamelContext());
+        }
+        return ExpressionToPredicateAdapter.toPredicate(answer);
+    }
+
+    @Override
+    public Expression createExpression(String expression) {
+        return createExpression(expression, new Object[] {});
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public ExpressionAdapter createExpression(String expression, Object[] 
properties) {
+        validateExpression(expression);
+        Evaluation answer = new Evaluation(
+                expression,
+                property(String.class, properties, 0, endpoint),
+                property(String.class, properties, 1, null),
+                property(Map.class, properties, 2, null),
+                property(String.class, properties, 3, "variant"));
+        if (getCamelContext() != null) {
+            answer.init(getCamelContext());
+        }
+        return answer;
+    }
+
+    public boolean validateExpression(String expression) {
+        if (expression == null || expression.isBlank()) {
+            throw new IllegalArgumentException("OpenFeature flag key must not 
be null or blank");
+        }
+        return true;
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    /** Managed endpoint URI. Defaults to openfeature:flags, inheriting 
camel.component.openfeature settings. */
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    private static final class Evaluation extends ExpressionAdapter {
+        private final String flagKey;
+        private final String endpointUri;
+        private final String targetingKey;
+        private final Map<String, Object> contextMap;
+        private OpenFeatureEndpoint endpoint;
+        private boolean useFlagKeyHeader;
+        private String evaluationType;
+
+        private Evaluation(String flagKey, String endpointUri, String 
targetingKey,
+                           Map<String, Object> contextMap, String 
evaluationType) {
+            this.flagKey = flagKey;
+            this.endpointUri = endpointUri;
+            this.targetingKey = targetingKey;
+            this.contextMap = contextMap;
+            this.evaluationType = evaluationType;
+        }
+
+        @Override
+        public void init(CamelContext context) {
+            super.init(context);
+            if (endpointUri == null || 
!endpointUri.startsWith("openfeature:")) {
+                throw new IllegalArgumentException("OpenFeature language 
endpoint must be an openfeature: URI");
+            }
+            String uri = endpointUri;
+            if (uri.contains("flagKey=")) {
+                useFlagKeyHeader = true;
+            } else {
+                uri += (uri.contains("?") ? "&" : "?") + "flagKey=" + flagKey;
+                useFlagKeyHeader = false;
+            }
+            endpoint = context.getEndpoint(uri, OpenFeatureEndpoint.class);
+        }
+
+        @Override
+        public <T> T evaluate(Exchange exchange, Class<T> type) {
+            if (Boolean.class.isAssignableFrom(type)) {
+                evaluationType = "boolean";

Review Comment:
   **evaluate(exchange, Boolean.class) permanently switches a shared expression 
to boolean evaluation**
   
   `evaluationType` is a mutable, non-volatile field of an `Evaluation` shared 
across exchanges and threads. One call with `Boolean.class` flips it to 
`boolean` for every later `evaluate(exchange)` / `evaluate(exchange, 
String.class)` on the same expression, so a variant flag then hits 
TYPE_MISMATCH and yields `"false"`, and concurrent callers race on the write. 
Deciding the type per call (passed down as a local value) would avoid it.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })

Review Comment:
   **@UriEndpoint is missing headersClass, and Category.CORE looks wrong**
   
   Without `headersClass = OpenFeatureConstants.class` the header `@Metadata` 
is never picked up: the catalog JSON has no `headers` section, the headers 
partial renders nothing (hence the hand-written table in the adoc), and the 
Endpoint DSL gets no header name builder, unlike siblings such as 
`OpenFgaEndpoint`/`OpaEndpoint` (needs a regen). Separately, `Category.CORE` is 
used only by camel-core-style components (bean, direct, log, seda, ...), and 
its label makes the doc header get `:core:`; something like 
`Category.MANAGEMENT` may fit better (your call; adjust the suggestion if you 
change it):
   
   ```suggestion
                producerOnly = true, category = { Category.CORE }, headersClass 
= OpenFeatureConstants.class)
   ```



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureComponent.java:
##########
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("openfeature")
+public class OpenFeatureComponent extends DefaultComponent {
+
+    @Metadata
+    private OpenFeatureConfiguration configuration = new 
OpenFeatureConfiguration();
+
+    private volatile OpenFeatureAPI api;
+    private final Set<String> initializedDomains = 
ConcurrentHashMap.newKeySet();
+
+    @Override
+    protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) throws Exception {
+        OpenFeatureEndpoint endpoint = new OpenFeatureEndpoint(uri, this, 
remaining, configuration.copy());
+        setProperties(endpoint, parameters);
+        return endpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        api = OpenFeatureAPI.createIsolated();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        OpenFeatureAPI a = api;
+        api = null;
+        initializedDomains.clear();
+        if (a != null) {
+            a.shutdown();

Review Comment:
   **api.shutdown() also shuts down provider beans the component does not own**
   
   `OpenFeatureAPI.shutdown()` shuts down every provider bound to the isolated 
API, including beans passed via `provider=#bean` or the `flags` registry bean 
that the endpoint deliberately marks `ownedProvider = false`. Also, SDK 1.22.1 
refuses to register a provider already set on `OpenFeatureAPI.getInstance()` 
with another API instance (the usual Spring Boot/Quarkus setup), so such an app 
cannot hand its provider to this component, and `createIsolated()` is 
documented as experimental. Accepting a user-supplied `OpenFeatureAPI` (not 
shut down by Camel), falling back to an isolated one only when none is given, 
and documenting provider ownership would cover both.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureConfiguration.java:
##########
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+
+@UriParams
+public class OpenFeatureConfiguration implements Cloneable {
+
+    @UriParam(label = "common",
+              description = "The feature flag key to evaluate. Can be 
overridden per message via the CamelOpenFeatureFlagKey header.")
+    private String flagKey;
+
+    @UriParam(label = "common", defaultValue = "false",
+              description = "Default value when flag evaluation fails. When 
evaluationType is not set,"
+                            + " also determines the evaluation type: \"true\" 
or \"false\" (case-insensitive)"
+                            + " selects boolean evaluation, any other value 
selects string evaluation.")
+    private String defaultValue = "false";
+
+    @UriParam(label = "common", enums = "boolean,variant",
+              description = "The evaluation type. When set to 'boolean', 
boolean evaluation is used (getBooleanValue)."
+                            + " When set to 'variant', string evaluation is 
used (getStringValue)."
+                            + " When not set, the type is inferred from 
defaultValue.")
+    private String evaluationType;
+
+    @UriParam(label = "producer",
+              description = "Store the evaluation result in this exchange 
property, preserving the original message body.")
+    private String resultProperty;
+
+    @UriParam(label = "common", description = "Remote flagd service host. When 
set, the flagd RPC resolver is used.")
+    private String host;
+
+    @UriParam(label = "common", defaultValue = "8013", description = "Remote 
flagd service port.")
+    private int port = 8013;
+
+    @UriParam(label = "common",
+              description = "A JSON object defining feature flags in flagd 
format (inline). Mutually exclusive with flagsResource.")
+    private String flags;
+
+    @UriParam(label = "common",
+              description = "Camel resource URI pointing to a feature flag 
definition file in flagd format."
+                            + " Mutually exclusive with flags and provider.")
+    private String flagsResource;
+
+    @UriParam(label = "common",
+              description = "Bean reference to a custom FeatureProvider (e.g. 
#myProvider)."
+                            + " Mutually exclusive with flags and 
flagsResource.")
+    private String provider;
+
+    public String getFlagKey() {
+        return flagKey;
+    }
+
+    /** The feature flag key to evaluate. */
+    public void setFlagKey(String flagKey) {
+        this.flagKey = flagKey;
+    }
+
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    /**
+     * Default value when flag evaluation fails. When evaluationType is not 
set, also determines the evaluation type:
+     * "true" or "false" (case-insensitive) selects boolean evaluation, any 
other value selects string evaluation.
+     */
+    public void setDefaultValue(String defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+
+    public String getEvaluationType() {
+        return evaluationType;
+    }
+
+    /**
+     * The evaluation type. When set to 'boolean', boolean evaluation is used. 
When set to 'variant', string evaluation
+     * is used. When not set, the type is inferred from defaultValue.
+     */
+    public void setEvaluationType(String evaluationType) {
+        this.evaluationType = evaluationType;
+    }
+
+    public String getResultProperty() {
+        return resultProperty;
+    }
+
+    /** Store the evaluation result in this exchange property, preserving the 
original message body. */
+    public void setResultProperty(String resultProperty) {
+        this.resultProperty = resultProperty;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    /** Remote flagd service host. When set, the flagd RPC resolver is used. */
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    /** Remote flagd service port. */
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public String getFlags() {
+        return flags;
+    }
+
+    /** A JSON object defining feature flags in flagd format (inline). 
Mutually exclusive with flagsResource. */
+    public void setFlags(String flags) {
+        this.flags = flags;
+    }
+
+    public String getFlagsResource() {
+        return flagsResource;
+    }
+
+    /**
+     * Camel resource URI pointing to a feature flag definition file in flagd 
format. Mutually exclusive with flags and
+     * provider.
+     */
+    public void setFlagsResource(String flagsResource) {
+        this.flagsResource = flagsResource;
+    }
+
+    public String getProvider() {
+        return provider;
+    }
+
+    /**
+     * Bean reference to a custom FeatureProvider (e.g. #myProvider). Mutually 
exclusive with flags and flagsResource.
+     */
+    public void setProvider(String provider) {
+        this.provider = provider;
+    }
+
+    public boolean isBooleanEvaluation(Exchange exchange, MutableContext ctx) {
+        String evalType = 
exchange.getIn().getHeader(OpenFeatureConstants.EVALUATION_TYPE, String.class);
+        if (evalType == null) {
+            evalType = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_TYPE, String.class);
+        }
+        if (evalType == null) {
+            evalType = evaluationType;
+        }
+
+        if ("boolean".equalsIgnoreCase(evalType)) {
+            return true;
+        }
+        if ("variant".equalsIgnoreCase(evalType)) {
+            return false;
+        }
+        if (!ctx.asMap().isEmpty()) {
+            return false;
+        }
+
+        return "true".equalsIgnoreCase(defaultValue) || 
"false".equalsIgnoreCase(defaultValue);
+    }
+
+    public OpenFeatureConfiguration copy() {
+        try {
+            return (OpenFeatureConfiguration) clone();
+        } catch (CloneNotSupportedException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    void validate() {
+        if (provider != null && (flags != null || flagsResource != null)) {

Review Comment:
   **Inherited component settings make an endpoint-level provider unusable**
   
   Endpoints start from `configuration.copy()`, so once 
`camel.component.openfeature.flags-resource` (or `flags`) is set, as the 
language docs recommend, any endpoint that adds `provider=#myProvider` fails 
validation ("mutually exclusive"), and it cannot clear the inherited value. 
Conversely, a registry bean named `flags` silently takes precedence over 
explicitly configured `flags`/`flagsResource`/`host`. Letting an endpoint-level 
`provider` override inherited flagd options, and a WARN when an implicit 
`flags` bean shadows configured options, would help.



##########
components/camel-openfeature/src/test/java/org/apache/camel/component/openfeature/OpenFeatureLanguageTest.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.language.openfeature.OpenFeatureLanguage;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.camel.test.junit6.TestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class OpenFeatureLanguageTest extends CamelTestSupport {
+
+    private static final String FLAGS_RESOURCE = 
"classpath:openfeature/flags.json";
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        OpenFeatureComponent component = new OpenFeatureComponent();
+        component.getConfiguration().setFlagsResource(FLAGS_RESOURCE);
+        camelContext.addComponent("openfeature", component);
+        return camelContext;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:filter-enabled")
+                        .filter().language("openfeature", "enrichment-enabled")
+                            .to("mock:filtered")
+                        .end()
+                        .to("mock:after-filter");
+
+                from("direct:filter-disabled")
+                        .filter().language("openfeature", "disabled-flag")
+                            .to("mock:filtered-disabled")
+                        .end()
+                        .to("mock:after-filter-disabled");
+
+                from("direct:choice")
+                        .choice()
+                            .when().language("openfeature", 
"enrichment-enabled")
+                                .to("mock:choice-enabled")
+                            .otherwise()
+                                .to("mock:choice-disabled")
+                        .end();
+
+                from("direct:with-context")
+                        .filter().language("openfeature", "enrichment-enabled")
+                            .to("mock:context-filtered")
+                        .end();
+            }
+        };
+    }
+
+    @Test
+    void testFilterPassesWhenFlagEnabled() throws Exception {
+        MockEndpoint filtered = getMockEndpoint("mock:filtered");
+        filtered.expectedMessageCount(1);
+        MockEndpoint afterFilter = getMockEndpoint("mock:after-filter");
+        afterFilter.expectedMessageCount(1);
+
+        template.sendBody("direct:filter-enabled", "test-message");
+
+        filtered.assertIsSatisfied();
+        afterFilter.assertIsSatisfied();
+    }
+
+    @Test
+    void testFilterBlocksWhenFlagDisabled() throws Exception {
+        MockEndpoint filtered = getMockEndpoint("mock:filtered-disabled");
+        filtered.expectedMessageCount(0);
+        MockEndpoint afterFilter = 
getMockEndpoint("mock:after-filter-disabled");
+        afterFilter.expectedMessageCount(1);
+
+        template.sendBody("direct:filter-disabled", "test-message");
+
+        filtered.assertIsSatisfied();
+        afterFilter.assertIsSatisfied();
+    }
+
+    @Test
+    void testChoiceRoutesToCorrectBranch() throws Exception {
+        MockEndpoint enabled = getMockEndpoint("mock:choice-enabled");
+        enabled.expectedMessageCount(1);
+        MockEndpoint disabled = getMockEndpoint("mock:choice-disabled");
+        disabled.expectedMessageCount(0);
+
+        template.sendBody("direct:choice", "test-message");
+
+        enabled.assertIsSatisfied();
+        disabled.assertIsSatisfied();
+    }
+
+    @Test
+    void testEvaluationContextViaHeaders() throws Exception {

Review Comment:
   **The language context tests cannot fail: the flag they evaluate has no 
targeting rule**
   
   This test and `testCreatePredicateWithContext` evaluate 
`enrichment-enabled`, which has no `targeting` in `flags.json`, so they pass 
even if the targeting key and context never reach the provider. A boolean flag 
with a targeting rule (e.g. `if`/`in` on `customer_tier`) asserting both 
outcomes, a producer test for a boolean flag with a targeting key / Map body, 
and a test mixing the language with a later `to("openfeature:...")` would have 
caught the issues above.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();
+        resolvedProvider = provider;
+        client = getComponent().registerProviderAndGetClient(domain, provider);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+
+        if (ownedProvider && resolvedProvider != null) {
+            resolvedProvider.shutdown();
+        }
+        resolvedProvider = null;
+
+        File tmp = tempFlagFile;
+        tempFlagFile = null;
+        if (tmp != null) {
+            Files.deleteIfExists(tmp.toPath());
+        }
+
+        super.doStop();
+    }
+
+    @SuppressWarnings("unchecked")
+    MutableContext buildContext(Exchange exchange) {
+        MutableContext ctx = new MutableContext();
+
+        // 1. CamelOpenFeatureEvaluationContext header
+        Object contextHeader = 
exchange.getMessage().getHeader(OpenFeatureConstants.EVALUATION_CONTEXT);
+        if (contextHeader == null) {
+            contextHeader = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_CONTEXT);
+        }
+        if (contextHeader instanceof Map) {
+            addMapToContext(ctx, (Map<String, Object>) contextHeader);
+        } else {
+            // Fallback to body map entries
+            Object body = exchange.getMessage().getBody();
+            if (body instanceof Map) {
+                addMapToContext(ctx, (Map<String, Object>) body);
+            }
+        }
+
+        // 2. CamelOpenFeatureTargetingKey header or exchange property
+        String targetingKey = 
exchange.getMessage().getHeader(OpenFeatureConstants.TARGETING_KEY, 
String.class);
+        if (targetingKey == null) {
+            targetingKey = 
exchange.getProperty(OpenFeatureConstants.TARGETING_KEY, String.class);
+        }
+        if (targetingKey != null) {
+            ctx.setTargetingKey(targetingKey);
+        }
+
+        return ctx;
+    }
+
+    private static void addMapToContext(MutableContext ctx, Map<String, 
Object> map) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue() != null ? 
String.valueOf(entry.getValue()) : "";
+            if ("targetingKey".equals(key)) {
+                ctx.setTargetingKey(value);
+            } else {
+                ctx.add(key, value);
+            }
+        }
+    }
+
+    String resolveFlagKey(Exchange exchange) {
+        String key = 
exchange.getMessage().getHeader(OpenFeatureConstants.FLAG_KEY, String.class);
+        if (key == null) {
+            key = exchange.getProperty(OpenFeatureConstants.FLAG_KEY, 
String.class);
+        }
+        if (key == null) {
+            key = configuration.getFlagKey();
+        }
+        if (key == null || key.isBlank()) {
+            throw new IllegalArgumentException(
+                    "No flag key specified. Set flagKey on the endpoint or 
provide it via the "
+                                               + OpenFeatureConstants.FLAG_KEY 
+ " header.");
+        }
+        return key;
+    }
+
+    public Object evaluate(Exchange exchange) {
+        Client c = client;
+        if (c == null) {
+            throw new IllegalStateException("OpenFeature endpoint is not 
started");
+        }
+        String flagKey = resolveFlagKey(exchange);
+        MutableContext ctx = buildContext(exchange);
+
+        if (configuration.isBooleanEvaluation(exchange, ctx)) {
+            boolean defaultVal = 
Boolean.parseBoolean(configuration.getDefaultValue());
+            return c.getBooleanValue(flagKey, defaultVal, ctx);

Review Comment:
   **Evaluation errors are silently swallowed, and the result details are 
discarded**
   
   `getBooleanValue`/`getStringValue` return `defaultValue` on every failure 
(FLAG_NOT_FOUND from a `flagKey` typo, TYPE_MISMATCH, PROVIDER_NOT_READY, 
PARSE_ERROR) and nothing is logged, so misconfiguration of a routing decision 
is invisible. Using `getBooleanDetails`/`getStringDetails`, exposing 
variant/reason/error code as headers, and logging when `errorCode != null` 
(possibly with an opt-in `failOnError`) would help. While the component is 
Preview: `evaluationType=variant` performs a string-value evaluation and 
returns the value, not the OpenFeature variant name, and integer/double/object 
flags cannot be evaluated at all; naming it `string` and adding the remaining 
types would avoid a misleading public option.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();
+        resolvedProvider = provider;
+        client = getComponent().registerProviderAndGetClient(domain, provider);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+
+        if (ownedProvider && resolvedProvider != null) {
+            resolvedProvider.shutdown();
+        }
+        resolvedProvider = null;
+
+        File tmp = tempFlagFile;
+        tempFlagFile = null;
+        if (tmp != null) {
+            Files.deleteIfExists(tmp.toPath());
+        }
+
+        super.doStop();
+    }
+
+    @SuppressWarnings("unchecked")
+    MutableContext buildContext(Exchange exchange) {
+        MutableContext ctx = new MutableContext();
+
+        // 1. CamelOpenFeatureEvaluationContext header
+        Object contextHeader = 
exchange.getMessage().getHeader(OpenFeatureConstants.EVALUATION_CONTEXT);
+        if (contextHeader == null) {
+            contextHeader = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_CONTEXT);
+        }
+        if (contextHeader instanceof Map) {
+            addMapToContext(ctx, (Map<String, Object>) contextHeader);
+        } else {
+            // Fallback to body map entries
+            Object body = exchange.getMessage().getBody();
+            if (body instanceof Map) {
+                addMapToContext(ctx, (Map<String, Object>) body);
+            }
+        }
+
+        // 2. CamelOpenFeatureTargetingKey header or exchange property
+        String targetingKey = 
exchange.getMessage().getHeader(OpenFeatureConstants.TARGETING_KEY, 
String.class);
+        if (targetingKey == null) {
+            targetingKey = 
exchange.getProperty(OpenFeatureConstants.TARGETING_KEY, String.class);
+        }
+        if (targetingKey != null) {
+            ctx.setTargetingKey(targetingKey);
+        }
+
+        return ctx;
+    }
+
+    private static void addMapToContext(MutableContext ctx, Map<String, 
Object> map) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue() != null ? 
String.valueOf(entry.getValue()) : "";
+            if ("targetingKey".equals(key)) {
+                ctx.setTargetingKey(value);
+            } else {
+                ctx.add(key, value);
+            }
+        }
+    }
+
+    String resolveFlagKey(Exchange exchange) {
+        String key = 
exchange.getMessage().getHeader(OpenFeatureConstants.FLAG_KEY, String.class);
+        if (key == null) {
+            key = exchange.getProperty(OpenFeatureConstants.FLAG_KEY, 
String.class);
+        }
+        if (key == null) {
+            key = configuration.getFlagKey();
+        }
+        if (key == null || key.isBlank()) {
+            throw new IllegalArgumentException(
+                    "No flag key specified. Set flagKey on the endpoint or 
provide it via the "
+                                               + OpenFeatureConstants.FLAG_KEY 
+ " header.");
+        }
+        return key;
+    }
+
+    public Object evaluate(Exchange exchange) {
+        Client c = client;
+        if (c == null) {
+            throw new IllegalStateException("OpenFeature endpoint is not 
started");
+        }
+        String flagKey = resolveFlagKey(exchange);
+        MutableContext ctx = buildContext(exchange);
+
+        if (configuration.isBooleanEvaluation(exchange, ctx)) {
+            boolean defaultVal = 
Boolean.parseBoolean(configuration.getDefaultValue());
+            return c.getBooleanValue(flagKey, defaultVal, ctx);
+        } else {
+            return c.getStringValue(flagKey, configuration.getDefaultValue(), 
ctx);
+        }
+    }
+
+    private FeatureProvider resolveProvider() throws IOException {
+        // 1. Explicit provider bean reference
+        String providerRef = configuration.getProvider();
+        if (providerRef != null) {
+            String beanName = providerRef.startsWith("#") ? 
providerRef.substring(1) : providerRef;
+            FeatureProvider provider = getCamelContext().getRegistry()
+                    .lookupByNameAndType(beanName, FeatureProvider.class);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        "No FeatureProvider bean found in the registry with 
name: " + beanName);
+            }
+            ownedProvider = false;
+            return provider;
+        }
+
+        // 2. Registry lookup for default bean
+        FeatureProvider defaultProvider = getCamelContext().getRegistry()
+                .lookupByNameAndType(DEFAULT_PROVIDER_BEAN, 
FeatureProvider.class);
+        if (defaultProvider != null) {
+            ownedProvider = false;
+            return defaultProvider;
+        }
+
+        // 3. Fallback to FlagdProvider
+        ownedProvider = true;
+        return createFlagdProvider();
+    }
+
+    private FeatureProvider createFlagdProvider() throws IOException {
+        if (configuration.getFlags() != null) {
+            return createFileProvider(configuration.getFlags());
+        }
+        if (configuration.getFlagsResource() != null) {
+            String content = loadResource(configuration.getFlagsResource());
+            return createFileProvider(content);
+        }
+        if (configuration.getHost() != null) {
+            FlagdOptions options = FlagdOptions.builder()

Review Comment:
   **Remote flagd mode: no TLS/deadline options and no test coverage**
   
   The RPC resolver is configured with only `host`/`port`; TLS (`tls`, 
`certPath`), `deadline` and `targetUri` can only be set through flagd's 
`FLAGD_*` environment variables, so by default the evaluation context 
(targeting key and attributes, often user identifiers) goes over plaintext gRPC 
with no endpoint option to change it. This path also has no tests (all use 
`InMemoryProvider` or the FILE resolver), although the Jira lists remote flagd 
as supported. Exposing at least `tls`/`certPath` (or `sslContextParameters`) 
and `deadline`, with a test against a flagd container or an in-process gRPC 
server, would close it.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();
+        resolvedProvider = provider;
+        client = getComponent().registerProviderAndGetClient(domain, provider);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+
+        if (ownedProvider && resolvedProvider != null) {
+            resolvedProvider.shutdown();
+        }
+        resolvedProvider = null;
+
+        File tmp = tempFlagFile;
+        tempFlagFile = null;
+        if (tmp != null) {
+            Files.deleteIfExists(tmp.toPath());
+        }
+
+        super.doStop();
+    }
+
+    @SuppressWarnings("unchecked")
+    MutableContext buildContext(Exchange exchange) {
+        MutableContext ctx = new MutableContext();
+
+        // 1. CamelOpenFeatureEvaluationContext header
+        Object contextHeader = 
exchange.getMessage().getHeader(OpenFeatureConstants.EVALUATION_CONTEXT);
+        if (contextHeader == null) {
+            contextHeader = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_CONTEXT);
+        }
+        if (contextHeader instanceof Map) {
+            addMapToContext(ctx, (Map<String, Object>) contextHeader);
+        } else {
+            // Fallback to body map entries
+            Object body = exchange.getMessage().getBody();
+            if (body instanceof Map) {
+                addMapToContext(ctx, (Map<String, Object>) body);
+            }
+        }
+
+        // 2. CamelOpenFeatureTargetingKey header or exchange property
+        String targetingKey = 
exchange.getMessage().getHeader(OpenFeatureConstants.TARGETING_KEY, 
String.class);
+        if (targetingKey == null) {
+            targetingKey = 
exchange.getProperty(OpenFeatureConstants.TARGETING_KEY, String.class);
+        }
+        if (targetingKey != null) {
+            ctx.setTargetingKey(targetingKey);
+        }
+
+        return ctx;
+    }
+
+    private static void addMapToContext(MutableContext ctx, Map<String, 
Object> map) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue() != null ? 
String.valueOf(entry.getValue()) : "";
+            if ("targetingKey".equals(key)) {
+                ctx.setTargetingKey(value);
+            } else {
+                ctx.add(key, value);
+            }
+        }
+    }
+
+    String resolveFlagKey(Exchange exchange) {
+        String key = 
exchange.getMessage().getHeader(OpenFeatureConstants.FLAG_KEY, String.class);
+        if (key == null) {
+            key = exchange.getProperty(OpenFeatureConstants.FLAG_KEY, 
String.class);
+        }
+        if (key == null) {
+            key = configuration.getFlagKey();
+        }
+        if (key == null || key.isBlank()) {
+            throw new IllegalArgumentException(
+                    "No flag key specified. Set flagKey on the endpoint or 
provide it via the "
+                                               + OpenFeatureConstants.FLAG_KEY 
+ " header.");
+        }
+        return key;
+    }
+
+    public Object evaluate(Exchange exchange) {
+        Client c = client;
+        if (c == null) {
+            throw new IllegalStateException("OpenFeature endpoint is not 
started");
+        }
+        String flagKey = resolveFlagKey(exchange);
+        MutableContext ctx = buildContext(exchange);
+
+        if (configuration.isBooleanEvaluation(exchange, ctx)) {
+            boolean defaultVal = 
Boolean.parseBoolean(configuration.getDefaultValue());
+            return c.getBooleanValue(flagKey, defaultVal, ctx);
+        } else {
+            return c.getStringValue(flagKey, configuration.getDefaultValue(), 
ctx);
+        }
+    }
+
+    private FeatureProvider resolveProvider() throws IOException {
+        // 1. Explicit provider bean reference
+        String providerRef = configuration.getProvider();
+        if (providerRef != null) {
+            String beanName = providerRef.startsWith("#") ? 
providerRef.substring(1) : providerRef;
+            FeatureProvider provider = getCamelContext().getRegistry()
+                    .lookupByNameAndType(beanName, FeatureProvider.class);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        "No FeatureProvider bean found in the registry with 
name: " + beanName);
+            }
+            ownedProvider = false;
+            return provider;
+        }
+
+        // 2. Registry lookup for default bean
+        FeatureProvider defaultProvider = getCamelContext().getRegistry()
+                .lookupByNameAndType(DEFAULT_PROVIDER_BEAN, 
FeatureProvider.class);
+        if (defaultProvider != null) {
+            ownedProvider = false;
+            return defaultProvider;
+        }
+
+        // 3. Fallback to FlagdProvider
+        ownedProvider = true;
+        return createFlagdProvider();
+    }
+
+    private FeatureProvider createFlagdProvider() throws IOException {
+        if (configuration.getFlags() != null) {
+            return createFileProvider(configuration.getFlags());
+        }
+        if (configuration.getFlagsResource() != null) {
+            String content = loadResource(configuration.getFlagsResource());
+            return createFileProvider(content);
+        }
+        if (configuration.getHost() != null) {
+            FlagdOptions options = FlagdOptions.builder()
+                    .host(configuration.getHost())
+                    .port(configuration.getPort())
+                    .build();
+            return new FlagdProvider(options);
+        }
+        throw new IllegalArgumentException(
+                "No provider found. Set a provider bean reference, register a 
'" + DEFAULT_PROVIDER_BEAN
+                                           + "' bean of type FeatureProvider, 
or configure flags, flagsResource, or host for the default flagd provider.");
+    }
+
+    private FlagdProvider createFileProvider(String flagContent) throws 
IOException {
+        File tmp = File.createTempFile("camel-openfeature-", ".json");

Review Comment:
   **Use Files.createTempFile so flag definitions are not world-readable**
   
   `File.createTempFile` creates the file with umask-default permissions 
(typically `rw-r--r--`) in the shared temp directory, and flag definitions 
often embed targeting data such as customer IDs or e-mail lists. 
`Files.createTempFile` is owner-only on POSIX (CWE-732), and `Files` is already 
imported:
   
   ```suggestion
           File tmp = Files.createTempFile("camel-openfeature-", 
".json").toFile();
   ```
   
   Related: because `flagsResource` is copied to a private temp file, flagd's 
FILE source only ever watches the copy, so editing the real file (e.g. 
`file:/etc/flags/flags.json` on a mounted ConfigMap) has no effect until a 
restart. Passing a local file's path straight to `offlineFlagSourcePath` 
(keeping the copy only for `classpath:`/remote/inline flags) would keep live 
reload working.



##########
components/camel-openfeature/src/main/java/org/apache/camel/component/openfeature/OpenFeatureEndpoint.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Map;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.MutableContext;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.ResourceHelper;
+
+/** Evaluate feature flags using the OpenFeature specification with flagd. */
+@UriEndpoint(firstVersion = "4.23.0", scheme = "openfeature", title = 
"OpenFeature", syntax = "openfeature:domain",
+             producerOnly = true, category = { Category.CORE })
+public class OpenFeatureEndpoint extends DefaultEndpoint {
+
+    private static final String DEFAULT_PROVIDER_BEAN = "flags";
+
+    @UriPath
+    @Metadata(required = true)
+    private String domain;
+
+    @UriParam
+    private OpenFeatureConfiguration configuration;
+
+    private volatile Client client;
+    private volatile FeatureProvider resolvedProvider;
+    private volatile File tempFlagFile;
+    private volatile boolean ownedProvider;
+
+    public OpenFeatureEndpoint(String uri, OpenFeatureComponent component, 
String domain,
+                               OpenFeatureConfiguration configuration) {
+        super(uri, component);
+        this.domain = domain;
+        this.configuration = configuration;
+    }
+
+    @Override
+    public OpenFeatureComponent getComponent() {
+        return (OpenFeatureComponent) super.getComponent();
+    }
+
+    @Override
+    public Producer createProducer() {
+        return new OpenFeatureProducer(this);
+    }
+
+    @Override
+    public Consumer createConsumer(Processor processor) {
+        throw new UnsupportedOperationException("OpenFeature is producer 
only");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        configuration.validate();
+
+        if (domain == null || domain.isBlank()) {
+            throw new IllegalArgumentException("domain must not be blank");
+        }
+
+        FeatureProvider provider = resolveProvider();
+        resolvedProvider = provider;
+        client = getComponent().registerProviderAndGetClient(domain, provider);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+
+        if (ownedProvider && resolvedProvider != null) {
+            resolvedProvider.shutdown();
+        }
+        resolvedProvider = null;
+
+        File tmp = tempFlagFile;
+        tempFlagFile = null;
+        if (tmp != null) {
+            Files.deleteIfExists(tmp.toPath());
+        }
+
+        super.doStop();
+    }
+
+    @SuppressWarnings("unchecked")
+    MutableContext buildContext(Exchange exchange) {
+        MutableContext ctx = new MutableContext();
+
+        // 1. CamelOpenFeatureEvaluationContext header
+        Object contextHeader = 
exchange.getMessage().getHeader(OpenFeatureConstants.EVALUATION_CONTEXT);
+        if (contextHeader == null) {
+            contextHeader = 
exchange.getProperty(OpenFeatureConstants.EVALUATION_CONTEXT);
+        }
+        if (contextHeader instanceof Map) {
+            addMapToContext(ctx, (Map<String, Object>) contextHeader);
+        } else {
+            // Fallback to body map entries
+            Object body = exchange.getMessage().getBody();
+            if (body instanceof Map) {

Review Comment:
   **Should a Map body implicitly become the evaluation context?**
   
   Any `Map` body, for example one unmarshalled from an inbound HTTP/JSON 
request, silently becomes targeting attributes, including a `targetingKey` 
entry, without the route author opting in; if a flag gates something by tier, 
the sender can choose their own attributes (and it triggers the 
boolean-to-string switch above). Would you consider making it opt-in (e.g. 
`contextFromBody=true`), or at least documenting that the body is treated as 
trusted targeting input?



##########
parent/pom.xml:
##########
@@ -418,7 +418,9 @@
         <ognl-version>3.4.14</ognl-version>
         <opa-version>2.1.1</opa-version>
         <opa-wasm-version>1.1.0</opa-wasm-version>
+        <openfeature-sdk-version>1.22.1</openfeature-sdk-version>
         <openfga-version>0.11.0</openfga-version>
+        <flagd-provider-version>0.14.2</flagd-provider-version>

Review Comment:
   **Pin flagd's open-range transitive dependencies**
   
   flagd 0.14.2 declares `flagd-core:[2.0.0,3.0.0)`, which declares 
`flagd-api:[1.0.0,2.0.0)`; these resolve to whatever is newest on Central at 
build time, so the module (and the BOM) can change without any Camel change. 
`parent/pom.xml` already pins such ranges in `dependencyManagement` (e.g. 
`com.aliyun:tea`); version properties and entries for `flagd-core` and 
`flagd-api` would do the same here. (SDK 1.23.0 is also out and within flagd's 
supported range.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to