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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions components/camel-jackson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
<artifactId>camel-test-spring-junit6</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-main</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* 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.jackson;

import java.util.Map;

import org.apache.camel.CamelContext;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.main.Main;
import org.apache.camel.model.rest.RestBindingMode;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestConsumerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

/**
* The camel.dataformat.jackson.* options of Camel Main apply to the data formats of the REST DSL binding.
*/
public class JacksonRestBindingMainTest {

private Main main;

@AfterEach
void stopMain() {
if (main != null) {
main.stop();
}
}

@Test
void restBindingUsesTheJacksonOptions() throws Exception {
CamelContext context = startMain(Map.of(
"camel.dataformat.jackson.include", "NON_NULL",
"camel.dataformat.jackson.disable-features", "FAIL_ON_UNKNOWN_PROPERTIES"));

try (ProducerTemplate template = context.createProducerTemplate()) {
assertEquals("{\"name\":\"Camel\"}", template.requestBody("direct:get-pojo", null, String.class));
assertEquals("{\"name\":\"Camel\"}",
template.requestBody("direct:post-pojo", "{\"name\":\"Camel\",\"age\":20}", String.class));
}
}

@Test
void restDataFormatPropertiesComeFirst() throws Exception {
CamelContext context = startMain(Map.of(
"camel.dataformat.jackson.include", "NON_NULL",
"camel.rest.data-format-properties[json.out.include]", "ALWAYS"));

try (ProducerTemplate template = context.createProducerTemplate()) {
assertEquals("{\"name\":\"Camel\",\"country\":null}",
template.requestBody("direct:get-pojo", null, String.class));
}
}

@Test
void restBindingDoesNotTakeTheUnmarshalTypeHeader() throws Exception {
CamelContext context = startMain(Map.of("camel.dataformat.jackson.allow-unmarshall-type", "true"));

MockEndpoint posted = context.getEndpoint("mock:posted", MockEndpoint.class);
posted.expectedMessageCount(1);
try (ProducerTemplate template = context.createProducerTemplate()) {
template.requestBodyAndHeader("direct:post-pojo", "{\"name\":\"Camel\"}", JacksonConstants.UNMARSHAL_TYPE,
Map.class.getName());
}
posted.assertIsSatisfied();
assertInstanceOf(TestOtherPojo.class, posted.getReceivedExchanges().get(0).getMessage().getBody());
}

@Test
void restBindingDoesNotTakeTheBodyTypeOptions() throws Exception {
CamelContext context = startMain(Map.of(
"camel.dataformat.jackson.unmarshal-type", TestOtherPojo.class.getName(),
"camel.dataformat.jackson.use-list", "true",
"camel.dataformat.jackson.collection-type", "java.util.ArrayList"));

MockEndpoint put = context.getEndpoint("mock:put", MockEndpoint.class);
put.expectedMessageCount(1);
try (ProducerTemplate template = context.createProducerTemplate()) {
template.requestBody("direct:put-pojo", "{\"name\":\"Camel\"}");
}
put.assertIsSatisfied();
assertInstanceOf(Map.class, put.getReceivedExchanges().get(0).getMessage().getBody());
}

private CamelContext startMain(Map<String, String> properties) {
main = new Main();
main.bind("direct-rest", new DirectRestConsumerFactory());
properties.forEach(main::addInitialProperty);
main.configure().addRoutesBuilder(new RouteBuilder() {
@Override
public void configure() {
restConfiguration().component("direct-rest").bindingMode(RestBindingMode.json);

rest("/pojo")
.get().outType(TestOtherPojo.class).to("direct:pojo")
.post().type(TestOtherPojo.class).outType(TestOtherPojo.class).to("mock:posted")
.put().to("mock:put");

from("direct:pojo").process(e -> {
TestOtherPojo pojo = new TestOtherPojo();
pojo.setName("Camel");
e.getMessage().setBody(pojo);
});
}
});
main.start();
return main.getCamelContext();
}

/**
* Runs the REST services on direct endpoints, named after the verb and the path.
*/
private static final class DirectRestConsumerFactory implements RestConsumerFactory {

@Override
public Consumer createConsumer(
CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters)
throws Exception {
String uri = "direct:" + verb + basePath.replace('/', '-');
return camelContext.getEndpoint(uri).createConsumer(processor);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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.converter.jaxb;

import java.util.Map;

import org.apache.camel.CamelContext;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.example.Bar;
import org.apache.camel.example.PurchaseOrder;
import org.apache.camel.model.rest.RestBindingMode;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestConsumerFactory;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;

/**
* The partClass of the jaxb data format configured on the context (such as with camel.dataformat.jaxb.part-class in
* Camel Main) does not apply to the REST DSL binding, where the type of the verb decides the body type.
*/
public class JaxbRestBindingPartClassTest extends CamelTestSupport {

@Override
protected CamelContext createCamelContext() throws Exception {
CamelContext context = super.createCamelContext();
context.getRegistry().bind("direct-rest", new DirectRestConsumerFactory());
JaxbDataFormat jaxb = (JaxbDataFormat) context.resolveDataFormat("jaxb");
jaxb.setPartClass(Bar.class);
return context;
}

@Test
void restBindingDoesNotTakeThePartClass() throws Exception {
MockEndpoint posted = getMockEndpoint("mock:posted");
posted.expectedMessageCount(1);

template.requestBody("direct:post-order", "<purchaseOrder name=\"foo\" price=\"1.23\" amount=\"2.0\"/>");

posted.assertIsSatisfied();
assertInstanceOf(PurchaseOrder.class, posted.getReceivedExchanges().get(0).getMessage().getBody());
}

@Override
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
restConfiguration().component("direct-rest").bindingMode(RestBindingMode.xml);

rest("/order").post().type(PurchaseOrder.class).to("mock:posted");
}
};
}

/**
* Runs the REST services on direct endpoints, named after the verb and the path.
*/
private static final class DirectRestConsumerFactory implements RestConsumerFactory {

@Override
public Consumer createConsumer(
CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters)
throws Exception {
String uri = "direct:" + verb + basePath.replace('/', '-');
return camelContext.getEndpoint(uri).createConsumer(processor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@
package org.apache.camel.support.processor;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.apache.camel.CamelContext;
import org.apache.camel.spi.BeanIntrospection;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
import org.apache.camel.spi.PropertyConfigurer;
import org.apache.camel.spi.RestClientRequestValidator;
import org.apache.camel.spi.RestClientResponseValidator;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.EndpointHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.PropertyBindingSupport;
import org.apache.camel.support.PropertyConfigurerHelper;
import org.apache.camel.support.ResolverHelper;

/**
Expand All @@ -40,6 +45,12 @@ public class RestBindingAdviceFactory {
// matched by name: camel-support cannot depend on camel-jackson3, and JSON has no binding factory SPI
private static final String JACKSON3_PACKAGE = "org.apache.camel.component.jackson3.";
private static final String FAIL_ON_UNKNOWN_PROPERTIES = "FAIL_ON_UNKNOWN_PROPERTIES";
// options not taken from the data format configured on the context (lower case, as compared): a mapper may be the
// one it created when it started, and the request and response data formats keep mappers of their own; the body
// type comes from the type and outType of the verb, not from a header or the options of the data format
private static final Set<String> NOT_INHERITED_OPTIONS
= Set.of("objectmapper", "xmlmapper", "allowunmarshalltype", "allowjmstype", "unmarshaltype", "uselist",
"collectiontype", "partclass");

/**
* Builds the {@link RestBindingAdvice} from the given configuration
Expand Down Expand Up @@ -72,13 +83,13 @@ public static RestBindingAdvice build(CamelContext camelContext, RestBindingConf
// this will create a new instance as the name was not already pre-created
if (optional) {
try {
json = camelContext.createDataFormat(name);
json = createDataFormat(camelContext, name);
} catch (IllegalArgumentException e) {
// ignore
}
} else {
json = camelContext.createDataFormat(name);
outJson = camelContext.createDataFormat(name);
json = createDataFormat(camelContext, name);
outJson = createDataFormat(camelContext, name);
}

if (json != null) {
Expand All @@ -105,8 +116,8 @@ public static RestBindingAdvice build(CamelContext camelContext, RestBindingConf
name = "jaxb";
}
// this will create a new instance as the name was not already pre-created
xml = camelContext.createDataFormat(name);
outXml = camelContext.createDataFormat(name);
xml = createDataFormat(camelContext, name);
outXml = createDataFormat(camelContext, name);

// is xml binding required?
if (mode.contains("xml") && xml == null) {
Expand Down Expand Up @@ -149,6 +160,40 @@ public static RestBindingAdvice build(CamelContext camelContext, RestBindingConf
return advice;
}

/**
* Creates a new data format, with the options of the data format of the same name that is configured on the context
* (such as with camel.dataformat.jackson.* in Camel Main), as data formats in routes do. The rest configuration's
* data format properties are applied afterwards, so they take precedence.
*/
private static DataFormat createDataFormat(CamelContext camelContext, String name) {
DataFormat answer = camelContext.createDataFormat(name);
if (answer != null && camelContext.getDataFormatNames().contains(name)) {
DataFormat configured = camelContext.resolveDataFormat(name);
if (configured != null && configured.getClass() == answer.getClass()) {
copyOptions(camelContext, name, configured, answer);
}
}
return answer;
}

private static void copyOptions(CamelContext camelContext, String name, DataFormat source, DataFormat target) {
PropertyConfigurer configurer = PluginHelper.getConfigurerResolver(camelContext)
.resolvePropertyConfigurer(name + "-dataformat", camelContext);
if (configurer == null) {
configurer = PropertyConfigurerHelper.resolvePropertyConfigurer(camelContext, source);
}
if (configurer instanceof ExtendedPropertyConfigurerGetter getter) {
for (String option : getter.getAllOptions(source).keySet()) {
if (!NOT_INHERITED_OPTIONS.contains(option.toLowerCase(Locale.ROOT))) {
Object value = getter.getOptionValue(source, option, true);
if (value != null) {
configurer.configure(camelContext, target, option, value, true);
}
}
}
}
}

protected static void setupJson(
CamelContext camelContext,
RestConfiguration config, String type, Class<?> typeClass, String outType, Class<?> outTypeClass, DataFormat json,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,17 @@ as `camel run` already did. Before, the failure left `main()` and only the JVM p
The JVM no longer prints that same failure a second time. When the application logs nothing at ERROR (such as without
a logging implementation), the JVM prints it as before.

=== camel-main - the camel.dataformat options apply to the Rest DSL binding

With Camel Main and Camel JBang, the `camel.dataformat.<name>.*` options (such as `camel.dataformat.jackson.include`)
now also apply to the data formats that the Rest DSL binding creates for `json` and `xml` binding, as they already did
to the data formats of the routes. The request and response bodies of a REST service can therefore change, for
instance null fields are left out. The `camel.rest.dataFormatProperties` (`json.in.`, `json.out.`, `xml.in.` and
`xml.out.`) still take precedence. The `objectMapper`, `xmlMapper`, `allowUnmarshallType`, `allowJmsType`,
`unmarshalType`, `useList`, `collectionType` and `partClass` options are not taken over: the body type comes from the
`type` and `outType` of the verb, and the other options can be set with `camel.rest.dataFormatProperties` when the
binding needs them.

=== camel-support - API components use one thread pool per CamelContext

The producers of the API components (AS2, Box, Braintree, DHIS2, FHIR, Google Calendar, Google Drive, Google Mail,
Expand Down
6 changes: 6 additions & 0 deletions docs/user-manual/modules/ROOT/pages/rest-dsl-binding.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ The `value` in `dataFormatProperty` must be defined as a string value, so we use
----
====

The binding creates data formats of its own. They start from the options of the data format of the same name that is
configured on the CamelContext, such as with `camel.dataformat.jackson.include=NON_NULL` in Camel Main, except the
`objectMapper`, `xmlMapper`, `allowUnmarshallType`, `allowJmsType`, `unmarshalType`, `useList`, `collectionType` and
`partClass` options: the body type comes from the `type` and `outType` of the verb. The `dataFormatProperty` options are
applied after them, so they take precedence.

To use binding you must include the necessary data formats on the
classpath, such as `camel-jaxb` / `camel-jacksonxml` (or `camel-jackson3xml`) and/or `camel-jackson` (or `camel-jackson3`). And then enable
the binding mode. You can configure the binding mode globally on the
Expand Down
Loading