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


##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.rest.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * camel-openapi-java has a copy of this class for the specification it serves 
as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;

Review Comment:
   Fixed in 47ac40a05768: each nested reference is now resolved against the 
document that contains it, `ExternalRefProcessor.join(document, ref)` for 
OpenAPI 3.0 and `ReferenceUtils.toBaseURI(resolve(ref, base))` for 3.1, and 
read the way the parser reads it: a `file:` or `classpath:` URI by its path 
(`Visitor.readURI`), anything else with the 5-arg `RefUtils.readExternalRef(…, 
RELATIVE, …, null)` (working directory, then classpath). A document that cannot 
be read now fails the check, with the reference that led to it.
   
   Tests: `localResolvesTheReferencesOfADocumentInASubdirectory` 
(`schemas/pet.yaml` → `schemas/owner.yaml`, positive), 
`localChecksTheReferencesOfADocumentInASubdirectory`, 
`localChecksADocumentReferencedByAbsolutePath` (zero requests) and 
`localFailsWhenAReferencedDocumentCannotBeRead`; in camel-openapi-java 
`checksTheReferencesOfADocumentInASubdirectory`.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.rest.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * camel-openapi-java has a copy of this class for the specification it serves 
as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;
+        }
+    }
+
+    private static boolean isHttp(String file) {
+        String lower = file.toLowerCase(Locale.ROOT);
+        return lower.startsWith("http:") || lower.startsWith("https:");
+    }
+
+    /**
+     * The check of one document.
+     */
+    private static final class DocumentCheck {
+
+        private final String policy;
+        private final String specificationUri;
+        private final String document;
+        private final boolean remote;
+        private final List<String> localDocuments = new ArrayList<>();
+
+        DocumentCheck(String policy, String specificationUri, String document, 
boolean remote) {
+            this.policy = policy;
+            this.specificationUri = specificationUri;
+            this.document = document;
+            this.remote = remote;
+        }
+
+        void scan(JsonNode node, String path) {
+            if (node.isObject()) {
+                for (Map.Entry<String, JsonNode> field : node.properties()) {
+                    String fieldPath = path.isEmpty() ? field.getKey() : path 
+ "." + field.getKey();
+                    if ("$ref".equals(field.getKey()) && 
field.getValue().isTextual()) {

Review Comment:
   Fixed in 47ac40a05768: for an OpenAPI 3.1 document the scan carries the 
`$id` chain the way `OpenAPI31Traverser.traverseSchema` builds it 
(`toBaseURI(resolve(toBaseURI(id), base))`, root base `local`) and classifies 
`toBaseURI(resolve(ref, base))`, as `ReferenceVisitor.resolveSchemaRef` does. 
So `$id: https://host/schemas/` + `$ref: owner.yaml` is rejected under `local`, 
and the message says what the reference resolves to. A `#…` reference below an 
`$id` stays allowed: the parser registers the `$id` base in the reference set 
before it visits the children, so the fragment is evaluated in the document; 
`noneAllowsAReferenceInsideTheDocumentBelowAnHttpId` asserts zero requests for 
that.
   
   Tests: `localRejectsAnOpenApi31ReferenceResolvedAgainstAnHttpId` (zero 
requests) and the control `allResolvesAnOpenApi31ReferenceAgainstItsId` (one 
request to `/schemas/owner.yaml`); in camel-openapi-java 
`checksAnOpenApi31ReferenceAgainstTheIdOfItsSchema`.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.rest.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * camel-openapi-java has a copy of this class for the specification it serves 
as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);

Review Comment:
   Fixed in 47ac40a05768: the checker now scans 
`DeserializationUtils.deserializeIntoTree(content, location)`, the tree the 
parser resolves, so the aliases are expanded (and the parser's alias limits 
apply) before the check.
   
   Tests: `noneRejectsAReferenceGivenAsAYamlAlias` (`&#remote`) and 
`localRejectsAReferenceGivenAsAYamlAlias`, both with zero requests, and the 
control `allResolvesAReferenceGivenAsAYamlAlias` (one request); in 
camel-openapi-java `checksAReferenceGivenAsAYamlAlias`.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.rest.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * camel-openapi-java has a copy of this class for the specification it serves 
as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;

Review Comment:
   Fixed in 47ac40a05768, see the reply on line 125 above for the resolution 
against the containing document and the fail-closed read. `file:` references 
are read by their path as `Visitor.readURI` does, and their references checked: 
`localChecksAnOpenApi31FileReference` (an http reference inside a `file:` 
document, zero requests) and `localAllowsAnOpenApi31FileReference`. The 
resolved location is what `checked` and the error message use.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.rest.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * camel-openapi-java has a copy of this class for the specification it serves 
as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;
+        }
+    }
+
+    private static boolean isHttp(String file) {
+        String lower = file.toLowerCase(Locale.ROOT);
+        return lower.startsWith("http:") || lower.startsWith("https:");
+    }
+
+    /**
+     * The check of one document.
+     */
+    private static final class DocumentCheck {
+
+        private final String policy;
+        private final String specificationUri;
+        private final String document;
+        private final boolean remote;
+        private final List<String> localDocuments = new ArrayList<>();
+
+        DocumentCheck(String policy, String specificationUri, String document, 
boolean remote) {
+            this.policy = policy;
+            this.specificationUri = specificationUri;
+            this.document = document;
+            this.remote = remote;
+        }
+
+        void scan(JsonNode node, String path) {
+            if (node.isObject()) {
+                for (Map.Entry<String, JsonNode> field : node.properties()) {
+                    String fieldPath = path.isEmpty() ? field.getKey() : path 
+ "." + field.getKey();
+                    if ("$ref".equals(field.getKey()) && 
field.getValue().isTextual()) {
+                        reference(field.getValue().asText(), fieldPath);
+                    } else {
+                        scan(field.getValue(), fieldPath);
+                    }
+                }
+            } else if (node.isArray()) {
+                int i = 0;
+                for (JsonNode item : node) {
+                    scan(item, path + "[" + i++ + "]");
+                }
+            }
+        }
+
+        private void reference(String ref, String path) {
+            if (ref.isEmpty() || ref.startsWith("#")) {
+                // inside the document
+                return;
+            }
+            int fragment = ref.indexOf('#');
+            String file = fragment < 0 ? ref : ref.substring(0, fragment);
+            String reason = null;
+            if (NONE.equals(policy)) {
+                reason = "externalReferences=none allows only references 
inside the document";
+            } else if (remote) {
+                reason = "externalReferences=local allows no external 
reference in a specification loaded from a URL,"
+                         + " since the parser resolves it against that URL";
+            } else if (isHttp(file)) {
+                reason = "externalReferences=local allows no http(s) 
reference";

Review Comment:
   Fixed in 47ac40a05768, see the reply on line 156 above: the policy is 
applied to the resolved reference.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * This is a copy of the class of the same name in camel-rest-openapi, which 
neither module can depend on the other for;
+ * keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {

Review Comment:
   Fixed in 47ac40a05768: a document that cannot be read fails the check, 
naming the reference that led to it (both copies).
   
   _Claude Code on behalf of oscerd_



##########
components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.openapi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against 
the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it 
meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification 
itself comes from a URL.
+ * <p>
+ * This is a copy of the class of the same name in camel-rest-openapi, which 
neither module can depend on the other for;
+ * keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+    static final String ALL = "all";
+    static final String LOCAL = "local";
+    static final String NONE = "none";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    private OpenApiExternalReferences() {
+    }
+
+    /**
+     * The value of the option to apply: {@link #ALL} when none is set.
+     *
+     * @throws IllegalArgumentException for a value other than all, local or 
none
+     */
+    static String policy(String externalReferences) {
+        if (externalReferences == null || externalReferences.isBlank()) {
+            return ALL;
+        }
+        String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+        if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) 
{
+            return policy;
+        }
+        throw new IllegalArgumentException(
+                "Unknown externalReferences value: " + externalReferences + ", 
expected all, local or none");
+    }
+
+    /**
+     * Checks every {@code $ref} of the specification, and of the local 
documents it references, against the option.
+     *
+     * @param  externalReferences       the option: all, local or none
+     * @param  specification            the specification, as JSON or YAML text
+     * @param  specificationUri         where the specification comes from, 
for the error message
+     * @param  remote                   whether the specification comes from a 
URL: the parser then resolves its
+     *                                  external references against that URL
+     * @throws IllegalArgumentException for the first reference the option 
does not allow
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());

Review Comment:
   Fixed in 47ac40a05768: resolved against the containing document and read the 
way the parser reads it (both copies); see the thread on line 125 of the 
camel-rest-openapi copy.
   
   _Claude Code on behalf of oscerd_



-- 
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