eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – All Access – NPI EA (cat= Spring)
announcement - icon

All Access is finally out, with all of my Spring courses. Learn JUnit is out as well, and Learn Maven is coming fast. And, of course, quite a bit more affordable. Finally.

>> GET THE COURSE
Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Overview

An LLM can answer questions about documents’ content when they are in the context window. However, many real-world documents are stored in formats such as PDF, which may be difficult for AI applications to process directly. Docling converts these documents into a structured format that can be efficiently parsed.

In this tutorial, we’ll build a document processing system using Apache Camel, Docling, and LangChain4j. Also, we’ll define a simple question-answering API using an embedded Undertow server.

2. Problem Statement

Many real-world documents contain structural information such as headings, tables, and images, which is difficult to preserve during text extraction. As a result, AI pipelines may lose valuable context, reducing the quality of retrieval and accuracy of generated responses.

A structured representation helps address this problem by preserving the document’s organization and relationships. Structured formats such as Markdown, HTML, and JSON retain much of the document’s semantic structure, making them easier to process and interpret.

IBM Docling bridges this gap by converting unstructured or semi-structured documents into structured representations that downstream AI applications can efficiently consume. We can store the generated Markdown on disk. Later, an application can load it and include it in the prompt for the LLM. This avoids converting the same document for every request the application processes.

3. Project Setup

Let’s bootstrap a Java project using Apache Camel and LangChain4j.

3.1. Maven Dependencies

First, let’s add the camel-core, camel-main, and langchain4j dependencies to our pom.xml:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-core</artifactId>
    <version>4.20.0</version>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-main</artifactId>
    <version>4.20.0</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.11.8</version>
</dependency>

The camel-core dependency provides classes such as RouteBuilder for defining a camel route using the Java DSL, camel-main provides a lightweight runtime for standalone Camel applications, and langchain4j provides the core APIs for interacting with LLMs.

Next, let’s add camel-docling, camel-langchain4j-chat, and langchain4j-open-ai dependencies to our pom.xml:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-docling</artifactId>
    <version>4.20.0</version>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-langchain4j-chat</artifactId>
    <version>4.20.0</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai</artifactId>
    <version>1.11.8</version>
</dependency>

Here, camel-docling integrates Apache Camel with a Docling server, allowing routes to submit documents for conversion into Docling’s structured document representation. Also, camel-langchain4j-chat enables Camel routes to send prompts to a LangChain4j chat model and process the generated response. The langchain4j-open-ai dependency provides the implementation needed to connect LangChain4j to OpenAI-compatible chat models.

All Apache Camel dependencies, including camel-docling, use the same version to ensure compatibility and avoid unexpected runtime errors.

Let’s also add the camel-undertow dependency to our pom.xml:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-undertow</artifactId>
    <version>4.20.0</version>
</dependency>

It enables Camel to expose HTTP endpoints using an embedded Undertow server.

Finally, let’s create a directory named document in the project’s root directory and place the policies.pdf file inside it.

3.2. Docker Compose File

The easiest way to run Docling locally is by using docling-serve, which exposes Docling’s document conversion capabilities through a REST API. For simplicity, we run docling-serve in a Docker container.

First, let’s create a new file docker-compose.yml in our project root directory and add the following configuration:

services:
  docling-serve:
    image: quay.io/docling-project/docling-serve:latest
    container_name: docling-serve
    ports:
      - "5001:5001"
    environment:
      - DOCLING_STORAGE_DIR=/data
      - DOCLING_PORT=5001
    volumes:
      - ./data/docling:/data
    restart: unless-stopped

The configuration starts a docling-serve container from the quay.io/docling-project/docling-serve image. If the image isn’t available locally, Docker automatically pulls it from the registry. It also maps port 5001 on the host to port 5001 inside the container, allowing our application to communicate with the Docling server.

Finally, let’s start the Docling server by running the following command:

$ docker compose up

3.3. Application Entry Point

Let’s create a CamelDoclingApplication class that serves as our application entry point:

public class CamelDoclingApplication {

    public static void main(String[] args) throws Exception {
        OpenAiChatModel model = OpenAiChatModel.builder()
          .baseUrl("http://langchain4j.dev/demo/openai/v1")
          .apiKey("demo")
          .modelName("gpt-4o-mini")
          .build();
        Main main = new Main();
        main.bind("chatModel", model);
        main.run(args);
    }
}

We create an OpenAiChatModel object configured to use the gpt-4o-mini model. Although OpenAI models normally require a valid API key, LangChain4j provides a public demo endpoint and API key for experimentation. We configure the client to use this endpoint by setting the baseUrl to the LangChain4j demo service. Alternatively, we could configure LangChain4j to communicate with a locally hosted model through Ollama or replace the demo endpoint with our own OpenAI API credentials.

The Main instance provides a lightweight runtime for standalone Camel applications. We bind the chat model to Camel’s registry under the name chatModel, that Camel components and routes can reference during message processing.

4. Converting a PDF Document to Markdown

Now that the Docling server is running, let’s create our first Camel route to convert policies.pdf into Markdown:

class ConversionRoute extends RouteBuilder {

    @Override
    public void configure() {

        from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&noop=true&idempotent=true")
          .routeId("document-analysis-workflow")
          .log("Processing document: ${header.CamelFileName}")
          .to("docling:CONVERT_TO_MARKDOWN?useDoclingServe=true&doclingServeUrl=http://localhost:5001&contentInBody=true")
          .setProperty("convertedMarkdown", body())
          .setHeader(Exchange.FILE_NAME, simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}.md"))
          .to("file:output") 
    }
}

Here, we monitor the documents directory for supported document types. The noop=true option leaves the original file in place after processing, while idempotent=true prevents the same file from being processed more than once. Note that the Camel file component already provides the information that the Docling component needs to process the document. Although we could explicitly replace the message body with the document’s path, doing so would be redundant because the conversion works correctly without it.

Next, the docling:CONVERT_TO_MARKDOWN endpoint sends the document to the locally running docling-serve instance, which converts it into Markdown and returns the generated content in the message body.

Finally, we store the converted Markdown as an exchange property, rename the output file by replacing its original extension with .md, and use the Camel file component to write it in the output directory.

If there are multiple documents in the documents directory, Camel processes each one independently and generates a corresponding Markdown file in the output directory.

Let’s run our application by registering ConversionRoute with the Camel runtime:

Main main = new Main();
main.bind("chatModel", model);
main.configure()
  .addRoutesBuilder(new ConversionRoute());
main.run(args);

Here, we register the ConversionRoute with Camel through the Main instance.

5. Passing the Markdown Document to LangChain4j

Now that we’ve converted the document to Markdown, let’s send it to our chat model and ask it to analyze the document:

class ConversionRoute extends RouteBuilder {

    @Override
    public void configure() {

        from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&noop=true&idempotent=true")
          .routeId("document-analysis-workflow")
          // ...
          .setBody(simple("""
              You are a helpful document analysis assistant. Please analyze
              the following document and provide:
              1. A brief summary (2-3 sentences)
              2. Key topics and main points
              3. Any important findings or conclusions

              Document content:
              ${exchangeProperty.convertedMarkdown}
              """))
          .to("langchain4j-chat:analysis?chatModel=#chatModel")
          .setHeader(
              Exchange.FILE_NAME,
              simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}-analysis.md")
          )
          .to("file:analysis");
    }
}

In this route, we construct a prompt that includes the Markdown generated by Docling and instruct the model to summarize and analyze the document. The ${exchangeProperty.convertedMarkdown} expression inserts the Markdown stored earlier in the route into the prompt before it’s sent to the model.

Next, the langchain4j-chat component sends the prompt to the chat model referenced by #chatModel. Camel resolves this reference from its registry, where we previously bound the OpenAiChatModel instance. The analysis in langchain4j-chat:analysis is the endpoint name. It’s primarily used to distinguish this endpoint from others.

Finally, we rename the output file by appending -analysis to the original filename and use the File component to write the model’s response to the analysis directory.

6. Interactive Q&A API

Now, it’s time to define a question-answering API that uses the converted Markdown document as its knowledge source.

Let’s create a Camel route that accepts a question through an HTTP endpoint:

public class QuestionAndAnswerRoute extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        from("undertow:http://0.0.0.0:8080/api/ask?httpMethodRestrict=POST").routeId("document-qa-api")
          .setProperty("question", bodyAs(String.class))
          .pollEnrich("file:output?fileName=policies.md&noop=true&idempotent=false")
          .setProperty("markdown", bodyAs(String.class))
          .setBody(simple("""
              You are a helpful document assistant.
              Answer ONLY using the document below.
              DOCUMENT
              =========
              ${exchangeProperty.markdown}
              QUESTION
              =========
              ${exchangeProperty.question}
              """))

          .to("langchain4j-chat:analysis?chatModel=#chatModel")
          .setHeader(Exchange.CONTENT_TYPE, constant("text/plain"));
    }
}

What happens here?

We expose the POST /api/ask endpoint through the embedded Undertow server. The request body contains the user’s question, which we store as an exchange property before replacing the message body later in the route.

The pollEnrich Enterprise Integration Pattern (EIP) reads the previously generated Markdown document from the output directory and places its contents into the current exchange. Rather than converting the PDF each time a request arrives, this route reads the Markdown file. This keeps the HTTP request lightweight by reusing the preprocessed document instead of invoking Docling for each question.

Also, we set idempotent=false so the File component can read the same Markdown file on every API request. By default, noop=true enables idempotent=true, which prevents the same file from being consumed more than once.

Then, we construct a prompt containing both the document and the user’s question before sending it to the langchain4j-chat component.

Finally, the model’s response is returned as the HTTP response body with a text/plain content type.

Let’s update our application entry point to register the QuestionAndAnswerRoute:

// ...
main.configure()
  .addRoutesBuilder(new QuestionAndAnswerRoute());
// ...

Once the application is up,  we can run the following curl command to send a question to the API:

curl -X POST \
  http://localhost:8080/api/ask \
  -H "Content-Type: text/plain" \
  -d "How many lifecycle stages does a task move through?"

Here’s an API response we can expect:

A task moves through four lifecycle stages: PENDING, IN_PROGRESS, REVIEW, and DONE.

The API successfully answers the question based on the document we supplied.

7. Conclusion

In this article, we learned how to integrate Apache Camel, Docling, and LangChain4j to build an intelligent document processing application. We used Docling to convert an unstructured document into a structured representation, orchestrated the conversion workflow with Apache Camel, and analyzed the converted content using an LLM through LangChain4j.

Additionally, we exposed a simple question-answer API that uses the converted document as context to generate grounded responses.

By converting documents into a structured representation once and reusing the generated Markdown for subsequent interactions, we simplify document processing workflows, avoid repeated conversions, and make it easier for LLMs to process the document content.

As always, the source code for the examples is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments