Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java plugin docu #524

Merged
merged 33 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2d9fff2
WIP
rjayasinghe Oct 17, 2023
ed2b776
Merge remote-tracking branch 'origin/main' into java_plugin_docu
rjayasinghe Oct 17, 2023
22d71a5
fix build error, continue text
rjayasinghe Oct 17, 2023
7cc046d
continued guide
rjayasinghe Oct 23, 2023
c57e658
continue document
rjayasinghe Nov 6, 2023
7fc3e80
Merge remote-tracking branch 'origin/main' into java_plugin_docu
rjayasinghe Nov 6, 2023
459f224
started protocol adapter section
rjayasinghe Nov 7, 2023
6510941
continued protocol adapter section
rjayasinghe Nov 9, 2023
0084f20
rename guide and smaller fixes
rjayasinghe Nov 15, 2023
02fa38d
Apply suggestions from code review
rjayasinghe Nov 21, 2023
59eae63
Apply suggestions from code review
rjayasinghe Nov 21, 2023
12accf5
Apply suggestions from code review
rjayasinghe Nov 21, 2023
c5b3576
fix typos
rjayasinghe Nov 21, 2023
5e83864
added missing interface name
rjayasinghe Nov 21, 2023
741d607
fixed further typo
rjayasinghe Nov 21, 2023
bb0438d
Merge branch 'main' into java_plugin_docu
rjayasinghe Nov 21, 2023
d73f1dc
add project word HCQL
rjayasinghe Nov 21, 2023
1d73206
add additional hints wrt. project structure
rjayasinghe Nov 23, 2023
e237c42
add menu entry
rjayasinghe Nov 24, 2023
5e780cb
Merge remote-tracking branch 'origin/main' into java_plugin_docu
rjayasinghe Nov 24, 2023
6028e35
fix section about project layout for model reuse
rjayasinghe Nov 24, 2023
777b0dd
reworked event handler section
rjayasinghe Nov 24, 2023
491c170
Update java/plugin_concept.md
rjayasinghe Nov 24, 2023
e956033
extended section about event handler plugin
rjayasinghe Nov 29, 2023
e1461d3
Merge remote-tracking branch 'origin/main' into java_plugin_docu
rjayasinghe Nov 29, 2023
9d98c09
fix typo
rjayasinghe Nov 29, 2023
2f5f96a
add general considerations section
rjayasinghe Nov 30, 2023
5a3d6ce
fix typo
rjayasinghe Nov 30, 2023
dd18256
correct further typos
rjayasinghe Nov 30, 2023
e516916
edit
renejeglinsky Dec 12, 2023
72735ec
spellcheck
renejeglinsky Dec 13, 2023
7784bd7
renamed file
renejeglinsky Dec 13, 2023
f051ca1
renamed file
renejeglinsky Dec 13, 2023
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
313 changes: 313 additions & 0 deletions java/plugin_concept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
---
synopsis: >
A collection of different mechanisms that can be used to build plugins for CAP Java.
status: released
---

# Building CAP Java Plugins

<style scoped>
h1:before {
content: "Java"; display: block; font-size: 60%; margin: 0 0 .2em;
}
</style>

{{ $frontmatter.synopsis }}


<!-- #### Content -->
<!--- % include _chapters toc="2,3" %} -->

Especially when working with larger projects that may consists of many individual CAP Java applications or when building platform services that need to be integrated with CAP applications there is the requirement to extend CAP Java with custom, yet reusable code.

In the following sections the different extension points and mechanisms will be explained.

## General Considerations

### Java version

When building CAP Java plugin modules you need to keep in mind that the generated Java byte code of the plugin has to be compatible with the Java byte code version of the potential consumers of the plugin. To be on the safe side we recommend to use *Java 17* as this is anyways the minimum Java version for CAP Java (for 2.x release) applications. In case you deviate from this you need to check and align with the potential consumers of the plugin.
renejeglinsky marked this conversation as resolved.
Show resolved Hide resolved

### Maven GroupId and Java packages

Of course it's up to your project / plugin how you call the corresponding Maven GroupId and Java packages. In order to avoid confusion and also to make responsiblities clear `com.sap.cds` for GroupId and Java package names are reserved for components maintained by the CAP Java team and must not be used for external plugins. This rule also includes sub-structures to `com.sap.cds` like `com.sap.cds.foo.plugin`.
rjayasinghe marked this conversation as resolved.
Show resolved Hide resolved


## Sharing Reusable CDS Models via Maven artifacts

Prior to the CAP Java 2.2 release CDS definitions had to be shared as node.js modules, also for Java projects.

Starting with the 2.2 release CDS models, CSV import data and i18n files can now be shared through Maven dependencies in addition to npm packages. This means you can now provide CDS models, CSV files, i18n files and Java code (for example, event handlers) in a single Maven dependency.

### Create the Reuse Model in a New Maven Artifact

Simply create a plain Maven Java project and place your CDS models in the `main/resources/cds` folder of the reuse package under a unique module directory (for example, leveraging group ID and artifact ID): `src/main/resources/cds/com.sap.capire/bookshop/`. With `com.sap.capire` being the group ID and `bookshop` being the artifact ID.

### Reference the Reuse Model in an Existing CAP Java Project

Projects wanting to import the content simply add a Maven dependency to the reuse package to their `pom.xml` in the `<dependencies>` section.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the parent pom in this case?

Suggested change
Projects wanting to import the content simply add a Maven dependency to the reuse package to their `pom.xml` in the `<dependencies>` section.
Projects wanting to import the content simply add a Maven dependency to the reuse package to their `./pom.xml` in the `<dependencies>` section.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It‘s the srv/pom.xml

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I consume that, until this point locally? If yes, I guess the location of the new artifact is in parallel to my CAP project, as it is also for Node.js, right?
Or do we need a "Publish to Maven" step in between this and the previous step?


```xml

<dependency>
<groupId>com.sap.capire</groupId>
<artifactId>bookshop</artifactId>
<version>1.0.0</version>
</dependency>
```

Additionally the new `resolve` goal from the CDS Maven Plugin needs to be added, to extract the models into the `target/cds/` folder of the Maven project, in order to make them available to the CDS Compiler.

```xml
<plugin>
<groupId>com.sap.cds</groupId>
<artifactId>cds-maven-plugin</artifactId>
<version>${cds.services.version}</version>
<executions>
...
<execution>
<id>cds.resolve</id>
<goals>
<goal>resolve</goal> // [!code focus]
</goals>
</execution>
...
</executions>
</plugin>
```
> Please be ware that the module that uses the reuse module needs to be a Maven module itself or a submodule to a Maven module that declares the dependency to the Maven module. Usually you would declare the dependency in the `srv` module of your CAP Java project and use the reuse model in the service's CDS files then. In case you want to use the reuse model in your `db` module you need to make sure that your `db` module is a Maven module and include it to the project's parent `pom.xml` file.
Copy link
Contributor

@renejeglinsky renejeglinsky Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I declare the dependency? In the srv/pom.xml?

Suggested change
> Please be ware that the module that uses the reuse module needs to be a Maven module itself or a submodule to a Maven module that declares the dependency to the Maven module. Usually you would declare the dependency in the `srv` module of your CAP Java project and use the reuse model in the service's CDS files then. In case you want to use the reuse model in your `db` module you need to make sure that your `db` module is a Maven module and include it to the project's parent `pom.xml` file.
> Please be aware that the module that uses the reuse module needs to be a Maven module itself or a submodule to a Maven module that declares the dependency to the Maven module. Usually you would declare the dependency in the _srv/pom.xml_ of your CAP Java project and use the reuse model in the service's CDS files then. In case you want to use the reuse model in your `db` module you need to make sure that there's a _db/pom.xml_ in your _db_ folder to declare as a Maven module and include this module then to the project's parent `pom.xml` file.


When your Maven build is set up correctly you can use the reuse models in your CDS files using the standard `using` directive:

```cds
using { CatalogService } from 'com.sap.capire/bookshop';
```

> Note that the location in the `using` directive differs from the [CDS model resolution rules](https://cap.cloud.sap/docs/cds/cdl#model-resolution). The *name* neither starts with a `/`, `./`, `../` nor with `@`. Instead, it follows to the groupId/artifactId scheme defined above. The name does not directly refer to an an actual file system location but is looked up in a `cds` folder in Maven's `target` folder. Also, the [CDS editor](../tools/#cds-editor) does not yet support this new location and hence shows an error marker for this line. This will be fixed soon.

[Learn more about providing and using reuse packages.](../guides/extensibility/composition){.learn-more}

This technique can be used independently or together with one or more of the techniques described on this page.

## Event Handlers for custom types and annotations

In CAP Java event handler are not tightly coupled to the request handling or any other runtime components. Thus it is easily possible to package event handlers in modules in order to provide common but custom functionality to CAP Java applications. You can achieve this by defining custom handlers that react on model characteristics (common types or annotations) or also on entity values e.g. validations.

In most of the cases a reuse module for a CAP Java application can be a plain Maven project without further dependencies or special project layout. Since you need to use or implement CAP Java extension points it's required to define the following dependencies:

```xml
<properties>
<cds.services.version>2.4.0</cds.services.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-bom</artifactId>
<version>${cds.services.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-api</artifactId>
</dependency>
</dependencies>
```

Inside your reuse module you can define a custom event handler and a registration hook as plain Java code. Once this module deployed to a Maven repository it can be added to any CAP Java application as a dependency. The contained event handler code will be active automatically once your CAP Java application is started along with the new reuse module.

The heart of the reuse module, the event handler basically looks like any other CAP Java event handler. Take this one as an example:

```java
@ServiceName(value = "*", type = ApplicationService.class)
public class SampleHandler implements EventHandler {

@After
public void handleSample(CdsReadEventContext context) {
// any custom Java code using the event context and CQL APIs
}
}
```

The shown handler code is registered for any entity type on any [ApplicationService](../guides/providing-services). Dependending on the use case the target scope could narrowed to specific entities and/or services. The handler registration applies to the same rules as custom handlers that are directly packaged with a CAP Java application.

[Learn more about event handling in our EventHandler documentation](provisioning-api){.learn-more}

Of course this handler code looks just the same as any other custom or builtin CAP Java handler. The only difference here is that you need to think a bit more about the provisioning of the handler. When you write a custom handler as part of (in the package of) a CAP Java application you can annotate the handler's class with `@Component` and Spring Boot's component scan will pick up the class during startup of the Application Context.

When you provide your custom handler as part of an reuse library external to you application things change a bit. At first you need to decide whether you want to use Spring Boot's component model and rely on dependency injection or if you want to use one of the CAP Java ServiceLoader based extention points.

The decision between the two is pretty straightforward: In case your handler depends on other Spring components e.g. relies on dependency injection you should use the the [Spring approach](#spring-autoconfiguration). This applies as soon as you need to access another CAP Service like [`CqnService`](https://cap.cloud.sap/docs/java/application-services), [`PersistenceService`](https://cap.cloud.sap/docs/java/persistence-services) or a service via it's [typed service interface](https://cap.cloud.sap/docs/releases/nov23#typed-service-interfaces).

If your custom handler is pretty much isolated and is for example only performing validation based on provided data or performing a calculation you can stick with the [CAP Java ServiceLoader approach](#service-loader) which is described in the follwing section.

### Load Plugin Code via ServiceLoaders {#service-loader}
At runtime, CAP Java uses the [`ServiceLoader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ServiceLoader.html) mechanism to load all implementations of the `CdsRuntimeConfiguration` interface from the application's ClassPath. In order to qualify as a contributor for a given ServiceLoader-enabled interface we need to place plain text file named like the fully qualified name of the interface in the directory `src/main/resources/META-INF/services` of our reuse model containing the name of the implementing class(es). For the above implemented `CdsRuntimeConfiguration` we need to create a file `src/main/resources/META-INF/services/CdsRuntimeConfiguration` with the following content:

```txt
com.sap.example.cds.SampleHandlerRuntimeConfiguration
```

With this code we instrument the CAP Java's ServiceLoader for `CdsRuntimeConfiguration` to load our new, generic EventHandler for all read events on all entities of all services. For realistic usecases the handler configuration can be more concise, of course.

So, in order to have a framework independent handler registration the `CdsRuntimeConfiguration` interface needs to be implemented like this:

```java
package com.sap.example.cds;

import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;

public class SampleHandlerRuntimeConfiguration implements CdsRuntimeConfiguration {

@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
configurer.eventHandler(new SampleHandler());
}

}
```

### Load Plugin Code with the Spring Component Model {#spring-autoconfiguration}

In case your reuse module depends on other components managed as part of the Spring ApplicationContext (having an @Autowired annotation in your class is a good hint for that) you need to register your plugin as a Spring component itself. The most straight forward (but not recommended) way is to annotate the plugin class itself with `@Component`.

This is, however, error-prone: [Spring Boot's component scan](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ComponentScan.html) is by default scanning downward from the package in which the main `Application` class is declared. Meaning that you need to place the plugin either in a sub-package or the same package as the `Application` class. This would hamper the reuse aspect of the plugin as it would only work applications in a specific package. You could customize the component scan of the application using your plugin but this is also error-prone as you explicitly have to remember to change the `@ComponentScan` annotation each time you include a plugin.

Due to those complications it's best practice to use the `AutoConfiguration` mechanism provided by Spring Boot in reuse modules that ship Spring components. For further details please refer to the [Spring Boot reference documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration).


A complete end-to-end example for reusable event handlers can be found in this [blog post](https://blogs.sap.com/2023/05/16/how-to-build-reusable-plugin-components-for-cap-java-applications/).

## Custom Protocol Adapters {#protocol-adapter}

In CAP Java, the protocol adapter is the mechanism to implement inbound communication (another service or the UI) to the CAP service in development. The task of a protocol adapter is to translate any incoming requests of a defined protocol to CQL statements that then can be executed on locally defined CDS services. CAP Java comes with 3 protocol adapters (OData V2 and V4, and HCQL) but can be extended with custom implementations. In this section we'll have a deeper look on how such a protocol adapter can be built and registered with the CAP Java runtime.
rjayasinghe marked this conversation as resolved.
Show resolved Hide resolved
rjayasinghe marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about shortening this intro and link to https://cap.cloud.sap/docs/java/architecture#protocol-adapters? This chapter about protocol adapters would need an update then, I guess?


Usually, a protocol adapter comes in 2 parts. The adapter itself (in most cases an extension of the HttpServlet abstract class) and a factory class that creates an instance of the adapter as well as providing information about the paths to which the protocol adapter (the servlet) needs to be registered. The factory interface is called `ServletAdapterFactory` and implementations of that factory will be loaded with the same [`ServiceLoader` approach as described above](#service-loader) in the event handler section.

This is an example implementation of the `ServletAdapterFactory`:

```java
public class SampleAdapterFactory implements ServletAdapterFactory, CdsRuntimeAware {

/*
* a short key identifying the protocol that's being served
* by the new protocol adapter e.g. odata-v4, hcql, ..
*/

static final String PROTOCOL_KEY = "protocol-key";

private CdsRuntime runtime;

@Override
public void setCdsRuntime(CdsRuntime runtime) {

/*
* In case the protocol adapter needs the CdsRuntime the
* factory can implement CdsRuntimeAware and will be provided
* with a CdsRuntime via this method. The create() method
* below can then use the provided runtime for the protocol adapter.
*/
this.runtime = runtime;
}

@Override
public Object create() {
// Create and return the protocol adapter
return new SampleAdater(runtime);
}

@Override
public boolean isEnabled() {
// Determines if the protocol adapter is enabled
}

@Override
public String getBasePath() {
// Return the base path
}

@Override
public String[] getMappings() {
/*
* Return all paths to which the protocol adapter is going to
* be mapped. Usually, this will be each CDS service
* with either it's canonical or annotated path prefixed with
* the base path of the protocol adapter (see above).
*/

}

@Override
public UrlResourcePath getServletPath() {
/*
* Use the UrlResourcePathBuilder to build and return a UrlResourcePath
* containing the basePath (see above) and all paths being registered
* for the protocol key of the new protocol adapter.
*/
}

}
```
rjayasinghe marked this conversation as resolved.
Show resolved Hide resolved

With the factory in place we can start to build the actual protocol adapter. As mentioned before, most adapters will implement HTTP connectivity and will therefore be an extension of the Jakarta `HttpServlet` class. Based on the incoming request path the protocol adapter needs to determine the corresponding CDS `ApplicationService`. Parts of the request path together with potential request parameters (this depends on the protocol to be implemented) then need to be mapped to a CQL statement which is then executed on the previously selected CDS `ApplicationService`.

```java
public class SampleAdapter extends HttpServlet {

private final CdsRuntime runtime;

public SampleAdapter(CdsRuntime runtime) {
this.runtime = runtime;
// see below for further details
}

@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
// see below for further details
}
}
```

As mentioned above, a protocol adapter maps incoming requests to CQL statements and executes them on the right [`ApplicationService`](https://cap.cloud.sap/docs/java/application-services) according to the `HttpServletRequest`'s request-path. In order to have all relevant `ApplicationServices` ready at runtime you can call `runtime.getServiceCatalog().getServices(ApplicationService.class)` in the adapter's constructor to load all `ApplicationServices` and then select the ones relevant for this protocol adapter and then have them ready (in e.g. a Map) for serving requests in `service()`.

When handling incoming requests at runtime, you need to extract the request path and parameters from the incoming HttpServletRequest. Then, you can use CQL API from the `cds4j-api` module to [create CQL](https://cap.cloud.sap/docs/java/query-api) corresponding to the extracted information. This statement then needs to be executed with [`ApplicationService.run()`](https://cap.cloud.sap/docs/java/query-execution). The returned result then needs to be mapped to the result format that is suitable for the protocol handled by the adapter. For REST it would be some canonical JSON serialization of the returned objects.

So, a REST request like

```http
GET /CatalogService/Books?id=100
```

would result in this CQL statement:

```java
CqnSelect select = Select.from("Books").byId(100);
```

The `CqnSelect` statement can then be executed with the right (previously selected) `ApplicationService` and then written to HttpServletResponse as a String serialization.

```java
String resposePayload = applicationService.run(select).toJson();
response.getWriter().write(responsePayload);
```

With that a first iteration of a working CAP Java protocol adapter would be complete. As a wrap-up, this would be the tasks that need to be implemented in the adapter:

1. Extract the request path and select the corresponding CDS `ApplicationService`.
2. Build a CQL statement based on the request path and parameters.
3. Execute the CQL statement on the selected service and write the result to the response.

On final comment on protocol adapters: even a very simple protocol adapter like sketched in this section enables full support of other CAP features like declarative security, i18n and of course custom as well as generic event handlers.

## Putting it all together

As we have learned in this guide there are various ways to extend the CAP Java framework. You can use one or more of the mentioned techniques and combine them in one or more Maven modules. This totally depends on your needs and requirements.

Most probably you to combine the *Event Handler with custom types and annotations* mechanism together with *Sharing reuasable CDS models via Maven artifacts* because the event handler mechanism might rely on shared CDS artifacts. The protocol adapters on the other hand are very generic and model-independent modules that should be packaged and distributed independently.
1 change: 1 addition & 0 deletions menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
- [CDS Properties](java/development/properties)
- [Observability](java/observability)
- [Migration](java/migration)
- [Building Plugins](java/plugin_concept)

### [Node.js](node.js/)

Expand Down
3 changes: 2 additions & 1 deletion project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ undiscloses
isdir
Undeploying
SWAPI
SusaaS
SusaaS
HCQL
Loading