diff --git a/.gitignore b/.gitignore
index 22625d2eb..d927c9022 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,9 +11,6 @@ tmp/
local.properties
.loadpath
-# Eclipse Core
-.project
-
# External tool builders
.externalToolBuilders/
@@ -66,4 +63,5 @@ hs_err_pid*
# --- Derby stuff
**/jpa-processor/test/**
-/.vscode/
+/.vscode/
+/.idea/
diff --git a/.project b/.project
new file mode 100644
index 000000000..35c726d43
--- /dev/null
+++ b/.project
@@ -0,0 +1,11 @@
+
+
+ olingo-jpa-processor-v4
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 275796726..5f0d36b6b 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ There is no further development for this major version.
### 2.x.x
-The current version is based on [Jakarta 10](https://projects.eclipse.org/releases/jakarta-10), so [JPA 3.1.0](https://projects.eclipse.org/projects/ee4j.jpa/releases/3.1) or [Jakarta Persistence Specification](https://github.com/jakartaee/persistence), receptively and [Jakarta Servlet 6.0](https://projects.eclipse.org/projects/ee4j.servlet/releases/6.0). Test are performed using [Eclipselink 4.0.2](https://projects.eclipse.org/projects/ee4j.eclipselink/releases/4.0.2), but there is no real dependency to a JPA implementation. This version requires Java [17](https://sap.github.io/SapMachine/#download).
+The current version, [2.0.2](https://github.com/SAP/olingo-jpa-processor-v4/releases/tag/2.0.2), is based on [Jakarta 10](https://projects.eclipse.org/releases/jakarta-10), so [JPA 3.1.0](https://projects.eclipse.org/projects/ee4j.jpa/releases/3.1) or [Jakarta Persistence Specification](https://github.com/jakartaee/persistence), receptively and [Jakarta Servlet 6.0](https://projects.eclipse.org/projects/ee4j.servlet/releases/6.0). Test are performed using [Eclipselink 4.0.2](https://projects.eclipse.org/projects/ee4j.eclipselink/releases/4.0.2), but there is no real dependency to a JPA implementation. This version requires Java [17](https://sap.github.io/SapMachine/#download).
The current version comes with [Olingo 4.10.0](https://github.com/apache/olingo-odata4), which does not support Jakarta. Till Olingo supports Jakarta, requests get mapped by the JPA Processor.
diff --git a/additionalWords.directory b/additionalWords.directory
index 338fd9304..6244fae27 100644
--- a/additionalWords.directory
+++ b/additionalWords.directory
@@ -56,4 +56,9 @@ Servlet
Jakarta
CUD
JPAO
-subquery
\ No newline at end of file
+subquery
+skiptoken
+Redis
+icao
+iata
+MULTI
diff --git a/jpa-archetype/.project b/jpa-archetype/.project
new file mode 100644
index 000000000..eccaa1071
--- /dev/null
+++ b/jpa-archetype/.project
@@ -0,0 +1,28 @@
+
+
+ jpa-archetype
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
+
+ 1634102235489
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/jpa-archetype/odata-jpa-archetype-spring/.project b/jpa-archetype/odata-jpa-archetype-spring/.project
new file mode 100644
index 000000000..98302e60f
--- /dev/null
+++ b/jpa-archetype/odata-jpa-archetype-spring/.project
@@ -0,0 +1,23 @@
+
+
+ jpa-archetype-spring
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/jpa-archetype/odata-jpa-archetype-spring/src/main/resources/archetype-resources/pom.xml b/jpa-archetype/odata-jpa-archetype-spring/src/main/resources/archetype-resources/pom.xml
index 91053d4d7..fedea3134 100644
--- a/jpa-archetype/odata-jpa-archetype-spring/src/main/resources/archetype-resources/pom.xml
+++ b/jpa-archetype/odata-jpa-archetype-spring/src/main/resources/archetype-resources/pom.xml
@@ -12,7 +12,7 @@
org.springframework.bootspring-boot-starter-parent
- 3.2.0
+ 3.2.3
diff --git a/jpa-archetype/pom.xml b/jpa-archetype/pom.xml
index f47af6a1b..88fb5de8f 100644
--- a/jpa-archetype/pom.xml
+++ b/jpa-archetype/pom.xml
@@ -4,17 +4,17 @@
4.0.0com.sap.olingoodata-jpa-archetype
- 2.1.0-SNAPSHOT
+ 2.1.0-SNAPSHOTpomhttps://github.com/SAP/olingo-jpa-processor-v4UTF-817
- 2.1.0-SNAPSHOT
+ 2.1.0
- odata-jpa-archetype-spring
+ odata-jpa-archetype-spring
\ No newline at end of file
diff --git a/jpa-tutorial/.asciidoctorconfig.adoc b/jpa-tutorial/.asciidoctorconfig.adoc
new file mode 100644
index 000000000..d3cddb773
--- /dev/null
+++ b/jpa-tutorial/.asciidoctorconfig.adoc
@@ -0,0 +1,12 @@
+// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+// + Initial AsciiDoc editor configuration file - V1.0 +
+// ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+//
+// Did not find any configuration files, so creating this at project root level.
+// If you do not like those files to be generated - you can turn it off inside Asciidoctor Editor preferences.
+//
+// You can define editor specific parts here.
+// For example: with next line you could set imagesdir attribute to subfolder "images" relative to the folder where this config file is located.
+// :imagesdir: {asciidoctorconfigdir}/images
+//
+// For more information please take a look at https://github.com/de-jcup/eclipse-asciidoctor-editor/wiki/Asciidoctor-configfiles
diff --git a/jpa-tutorial/.project b/jpa-tutorial/.project
new file mode 100644
index 000000000..6b8fa3861
--- /dev/null
+++ b/jpa-tutorial/.project
@@ -0,0 +1,11 @@
+
+
+ jpa-tutorial
+
+
+
+
+
+
+
+
diff --git a/jpa-tutorial/Questions/HowToBuildServerDrivenPaging.adoc b/jpa-tutorial/Questions/HowToBuildServerDrivenPaging.adoc
new file mode 100644
index 000000000..90bac97df
--- /dev/null
+++ b/jpa-tutorial/Questions/HowToBuildServerDrivenPaging.adoc
@@ -0,0 +1,694 @@
+= How to build server driven paging?
+
+== Introduction
+
+OData describes that a server can restrict the number of returned records e.g., to prevent DoS attacks or
+ to prevent that the server dies with an OutOfMemory exception. Implementing this so called
+http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part1-protocol/odata-v4.0-errata02-os-part1-protocol-complete.html#_Toc406398310[Server-Driven Paging]
+requires the knowledge about a couple of details such as:
+
+- Heap Size of the web service
+- Width respectively memory consumption of an instance of an entity
+- Expected number of results of a $expand
+- Expected number of parallel processed requests on a service instance
+- ...
+
+This makes a general implementation impossible. Instead the JPA Processor provides a hook to calculate the pages of a request.
+This hook must implement interface https://github.com/SAP/olingo-jpa-processor-v4/blob/main/jpa/odata-jpa-processor/src/main/java/com/sap/olingo/jpa/processor/core/api/JPAODataPagingProvider.java[`JPAODataPagingProvider`],
+which contains two methods `getFirstPage` and `getNextPage`. `getFirstPage` is called in case a query does not contain a `$skiptoken`.
+It either returns an instance of https://github.com/SAP/olingo-jpa-processor-v4/blob/main/jpa/odata-jpa-processor/src/main/java/com/sap/olingo/jpa/processor/core/api/JPAODataPage.java[`JPAODataPage`],
+which describes the subset of the requested entities that shall be returned or null, so all entities are returned.
+If a request has a `$skiptoken` method `getNextPage` is called. In case this method does not return a `JPAODataPage` instance, which describes the next page to be retrieved, a _http 410, "Gone"_, exception is raised.
+
+As a paging provider connects to multiple requests, it is put to the session context:
+
+[source,java]
+----
+ @Bean
+ public JPAODataSessionContextAccess sessionContext(@Autowired final EntityManagerFactory emf)
+ throws ODataException {
+
+ return JPAODataServiceContext.with()
+ ...
+ .setPagingProvider(new PagingProvider(buildPagingProvider()))//<1>
+ ...
+ }
+
+ private PagingProvider buildPagingProvider() { //<2>
+ final Map pageSizes = new HashMap<>();
+ pageSizes.put("People", 10);
+
+ return new PagingProvider(pageSizes);
+ }
+----
+
+<1> An instance of the paging provider set in the session context.
+<2> Creation of a Map containing a page size for each relevant entity set.
+
+Depending on the usage of the service, three cases can be distinguished, which have an increasing complexity.
+They are discussed below. Even so the first scenario (one service instance using one thread) is not very likely,
+it is worth to read the chapter, as it contains some general hints.
+
+== Single service instance single thread
+
+Implementing server driven paging, we must answer some questions. The first, general question, is how the skip-token should look like.
+There are two obvious options:
+
+. The skip-token is a string that contains all the information needed to build the next page and to determine the last page.
+. The skip-token is a random key.
+
+Both have drawbacks. If we provide a string, the client can manipulate it, so the server must check e.g., that the client does not request to many entities.
+If a random skip-token is used, the server must store information about the query.
+
+For this tutorial we use the second option, as it seams to be easier to implement. So, lets have a look at the next questions that need to be answered:
+
+. How to store the necessary information to build a query from the skip-token?
+. How to prevent to many open skip-token and create a memory leak?
+. Can skip-token be used multiple times?
+. What page sizes should be used?
+
+For this case, single service instance and just one thread, we can cache the necessary information in a `Map` with the skip-token as key.
+To limit the memory consumption, we add a `Queue` that gives the skip-token an order. This enable us to remove the oldest entry, if the
+cache limit is reached.
+
+[NOTE]
+====
+The example is guided by https://github.com/SAP/olingo-jpa-processor-v4/blob/main/jpa/odata-jpa-processor/src/main/java/com/sap/olingo/jpa/processor/core/api/example/JPAExamplePagingProvider.java[JPAExamplePagingProvider],
+which is part of `odata-jpa-processor`
+====
+
+The frame of out paging provider will look as follows:
+
+[source,java]
+----
+public class PagingProvider implements JPAODataPagingProvider {
+
+ private static final int BUFFER_SIZE = 500; //<1>
+ private final Map pageCache; //<2>
+ private final Queue index; //<3>
+ private final Map maxPageSizes; //<4>
+
+ public PagingProvider(final Map pageSizes) { //<4>
+ maxPageSizes = Collections.unmodifiableMap(pageSizes);
+ pageCache = new HashMap<>(BUFFER_SIZE);
+ index = new LinkedList<>();
+ }
+
+ ...
+}
+----
+<1> Definition of the size of the cache. So, we wont have more then 500 skip-token in the cache.
+<2> Map to cache the query information.
+<3> Queue to control the cache limit.
+<4> Map to store the page sizes per entity type, which is provided when an instance of the paging provider is created.
+
+If we look carefully at the first part of the implementation, we see that we need a class that takes the information
+needed to create the next page:
+
+[source,java]
+----
+ private static record CacheEntry(Long last, //<1>
+ JPAODataPage page) {} //<2>
+----
+<1> The last index to be returned for the request.
+<2> The page provided.
+
+[NOTE]
+====
+If the cache stores to last top value, it could happen that entries are missed in case they are created while a
+client retrieves page by page. Nevertheless, as determine the last top include a count query, so a round trip to the database,
+this information is not calculated again.
+====
+
+Having done this preparation, we can start to implement `getFirstPage` and `getNextPage`.
+
+[WARNING]
+====
+With 2.1.0 `JPAODataPagingProvider` got a new set of methods. Do not implement the old, deprecated once.
+
+====
+
+First things first. Let's implement `getFirstPage`:
+
+[source,java]
+----
+ @Override
+ public Optional getFirstPage(
+ final JPARequestParameterMap requestParameter,
+ final JPAODataPathInformation pathInformation,
+ final UriInfo uriInfo, //<1>
+ @Nullable final Integer preferredPageSize,
+ final JPACountQuery countQuery,
+ final EntityManager em) throws ODataApplicationException {
+
+ final UriResource root = uriInfo.getUriResourceParts().get(0); //<1>
+ // Paging will only be done for Entity Sets. It may also be needed for functions
+ if (root instanceof final UriResourceEntitySet entitySet) {
+ // Check if Entity Set shall be packaged
+ final Integer maxSize = maxPageSizes.get(entitySet.getEntitySet().getName());
+ if (maxSize != null) {
+ // Read $top and $skip
+ final Integer skipValue = uriInfo.getSkipOption() != null ? uriInfo.getSkipOption().getValue() : 0;
+ final Integer topValue = uriInfo.getTopOption() != null ? uriInfo.getTopOption().getValue() : null;
+ // Determine page size
+ final Integer pageSize = preferredPageSize != null && preferredPageSize < maxSize ? preferredPageSize : maxSize; //<2>
+ if (topValue != null && topValue <= pageSize) //<3>
+ return Optional.of(new JPAODataPage(uriInfo, skipValue, topValue, null));
+ // Determine end of list
+ final Long maxResults = countQuery.countResults(); //<4>
+ final Long count = topValue != null && (topValue + skipValue) < maxResults
+ ? topValue.longValue() : maxResults - skipValue; //<5>
+ final Long last = topValue != null && (topValue + skipValue) < maxResults
+ ? (topValue + skipValue) : maxResults; //<6>
+ // Create a unique skip token if needed
+ String skipToken = null;
+ if (pageSize < count)
+ skipToken = UUID.randomUUID().toString(); //<7>
+ // Create page information
+ final JPAODataPage page = new JPAODataPage(uriInfo, skipValue, pageSize, skipToken);
+ // Cache page to be able to fulfill next link based request
+ if (skipToken != null)
+ addToCache(page, last); //<8>
+ return Optional.of(page);
+ }
+ }
+ return Optional.empty();
+ }
+----
+
+<1> UriInfo is a class provided by Olingo. It contains the parsed request information. The implementation looks at
+the root of the request to decide if paging shall be considered. This may not always be the right thing, as
+for chains of navigations the last part is retrieved from the database and will get the page limitation, based on the root.
+<2> A client can ask for certain page size by using `odata.maxpagesize` preference header. The paging provider shall respect this as
+long as the value is lower the maximum supported.
+<3> Skip further processing if no paging is required.
+<4> Determine maximum number of results that can be expected.
+<5> Determine requested number of results. Needed to decide if paging is needed.
+<6> Determine the last result requested. Needed to be able to stop the paging.
+<7> If paging is required, create a random skip-token.
+<8> Add the page to the cache.
+
+Now we must implement method `addToCache`, which is responsible to organize it:
+
+[source, java]
+----
+ private void addToCache(final JPAODataPage page, final Long count) {
+ if (pageCache.size() == BUFFER_SIZE) //<1>
+ pageCache.remove(index.poll());
+
+ pageCache.put((String) page.skipToken(), new CacheEntry(count, page));
+ index.add((String) page.skipToken());
+ }
+----
+
+<1> If the cache is full, the oldest is removed.
+
+With the implementation we already have, plus an empty one for `getNextPage`, we can test the paging and see
+if the skip-token is provided in the response of the request.
+
+Last step is to implement `getNextPage`:
+
+[source, java]
+----
+ @Override
+ public Optional getNextPage(
+ @Nonnull final String skipToken,
+ final OData odata,
+ final ServiceMetadata serviceMetadata,
+ final JPARequestParameterMap requestParameter,
+ final EntityManager em) {
+ final CacheEntry previousPage = pageCache.get(skipToken.replace("'", "")); //<1>
+ if (previousPage != null) {
+ // Calculate next page
+ final Integer skip = previousPage.page().skip() + previousPage.page().top();
+ // Create a new skip token, if next page is not the last one
+ String nextToken = null;
+ if (skip + previousPage.page().top() < previousPage.last()) //<2>
+ nextToken = UUID.randomUUID().toString();
+ final int top = (int) ((skip + previousPage.page().top()) < previousPage.last()
+ ? previousPage.page().top() : previousPage.last() - skip); //<3>
+ final JPAODataPage page = new JPAODataPage(previousPage.page().uriInfo(), skip, top, nextToken);
+ if (nextToken != null)
+ addToCache(page, previousPage.last());
+ return Optional.of(page);
+ }
+ // skip token not found => let JPA Processor handle this by return http.gone
+ return Optional.empty();
+ }
+----
+
+<1> Look for query information in the cache.
+<2> Check if this is the last page.
+<3> Calculate the value of $top, which may be different for the last page.
+
+We are done and can test our complete server driven paging.
+
+== Single service instance multiple threads
+
+The main difference, when we go from a single thread to multiple threads, is that we get a race condition in the cache handling.
+This becomes harder as we have two collections, which must be kept in sync. We can solve this by synchronizing the cache accesses:
+
+[source,java]
+----
+public class JPAExamplePagingProvider implements JPAODataPagingProvider {
+
+ private static final Object lock = new Object(); //<1>
+
+ ...
+
+
+ private void addToCache(final JPAODataPage page, final Long count) {
+
+ synchronized (lock) { //<2>
+ if (pageCache.size() == cacheSize)
+ pageCache.remove(index.poll());
+
+ pageCache.put((String) page.skipToken(), new CacheEntry(count, page));
+ index.add((String) page.skipToken());
+ }
+ }
+
+ ...
+}
+
+----
+<1> Introduction of a lock object needed for the synchronization.
+<2> Synchronization of the cache access.
+
+== Multiple service instances
+In case we have multiple instances of our service, the standard situation for microservices, we usually do not know which instance
+will handle a request. It may or may not be the same that has handled the request before. This holds also true for server driven paging.
+Therefore, we need to make the query information available for all instances, which requires a central backing service
+that can be reached from each instance of our service. Two options will be described below.
+
+One remark needs to be given up front. The processing of an OData request requires an instance of interface _UriInfo_.
+Unfortunately, _UriInfoImpl_ is not serializable. Instead of that we will store the URL
+and make use of Olingo's URL parser to get the _UriInfo_ back.
+
+
+=== Use the database
+We have already a backing service in place, the database. To store the pages, we must create a corresponding table:
+
+[source,sql]
+----
+CREATE TABLE "Trippin"."Pages" (
+ "token" varchar(255) NOT NULL,
+ "skip" int4 NOT NULL,
+ "top" int4 NOT NULL,
+ "count" int4 NOT NULL,
+ "baseUri" varchar(1000) NULL,
+ "oDataPath" varchar(1000) NULL,
+ "queryPath" varchar(1000) NULL,
+ "fragments" varchar(1000) NULL,
+ CONSTRAINT "Pages_pkey" PRIMARY KEY (token)
+);
+----
+
+To access the table, we create the corresponding entity:
+
+[source,java]
+----
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import com.sap.olingo.jpa.metadata.core.edm.annotation.EdmIgnore;
+
+@EdmIgnore
+@Entity
+@Table(schema = "\"Trippin\"", name = "\"Pages\"")
+public class Pages {
+
+ @Id
+ @Column(name = "\"token\"")
+ private String token;
+
+ @Column(name = "\"skip\"")
+ private Integer skip;
+
+ @Column(name = "\"top\"")
+ private Integer top;
+
+ @Column(name = "\"last\"")
+ private Integer last;
+
+ @Column(name = "\"baseUri\"")
+ private String baseUri;
+
+ @Column(name = "\"oDataPath\"")
+ private String oDataPath;
+
+ @Column(name = "\"queryPath\"")
+ private String queryPath;
+
+ @Column(name = "\"fragments\"")
+ private String fragments;
+
+ public Pages() {
+ // Needed for JPA
+ }
+
+ public Pages(final String token, final Integer skip, final Integer top, final Integer last, final String baseUri,
+ final String oDataPath, final String queryPath, final String fragments) {
+ super();
+ this.token = token;
+ this.skip = skip;
+ this.top = top;
+ this.last = last;
+ this.baseUri = baseUri;
+ this.oDataPath = oDataPath;
+ this.queryPath = queryPath;
+ this.fragments = fragments;
+ }
+
+ public Pages(final Pages previousPage, final int skip, final String token) {
+ super();
+ this.token = token;
+ this.skip = skip;
+ this.top = previousPage.top;
+ this.last = previousPage.last;
+ this.baseUri = previousPage.baseUri;
+ this.oDataPath = previousPage.oDataPath;
+ this.queryPath = previousPage.queryPath;
+ this.fragments = previousPage.fragments;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public Integer getSkip() {
+ return skip;
+ }
+
+ public Integer getTop() {
+ return top;
+ }
+
+ public String getBaseUri() {
+ return baseUri;
+ }
+
+ public String getODataPath() {
+ return oDataPath;
+ }
+
+ public String getQueryPath() {
+ return queryPath;
+ }
+
+ public String getFragments() {
+ return fragments;
+ }
+
+ public Integer getLast() {
+ return last;
+ }
+}
+----
+
+To store the page information on the database we need to replace the call of `addToCache`
+from above by calling a method to insert a new row:
+
+[source,java]
+----
+ @Override
+ public Optional getFirstPage(
+ final JPARequestParameterMap requestParameter,
+ final JPAODataPathInformation pathInformation,
+ final UriInfo uriInfo,
+ @Nullable final Integer preferredPageSize,
+ final JPACountQuery countQuery,
+ final EntityManager em) throws ODataApplicationException {
+
+ ...
+ if(skipToken != null)
+ savePage(pathInformation, em, last, page); //<1>
+ ...
+ }
+
+----
+<1> Calling method to save a page on the database.
+
+The `savePage` looks as follows:
+
+[source,java]
+----
+ private void savePage(final JPAODataPathInformation pathInformation, final EntityManager em, final Long last,
+ final JPAODataPage page) {
+
+ if (page.skipToken() != null) {
+ final Pages pagesItem = new Pages((String) page.skipToken(), page.skip(), page.top(), last > Integer.MAX_VALUE
+ ? Integer.MAX_VALUE : last.intValue(),
+ pathInformation.baseUri(), pathInformation.oDataPath(), pathInformation.queryPath(),
+ pathInformation.fragments());
+ em.getTransaction().begin();
+ em.persist(pagesItem);
+ em.getTransaction().commit();
+ }
+ }
+----
+
+Having done that, we have to go ahead and handle the retrieval of the next page:
+
+[source,java]
+----
+ @Override
+ public Optional getNextPage(@Nonnull final String skipToken, final OData odata,
+ final ServiceMetadata serviceMetadata, final JPARequestParameterMap requestParameter, final EntityManager em) {
+ final Pages previousPage = em.find(Pages.class, skipToken.replace("'", "")); //<1>
+ if (previousPage != null) {
+ try {
+ final UriInfo uriInfo = new Parser(serviceMetadata.getEdm(), odata)
+ .parseUri(previousPage.getODataPath(), previousPage.getQueryPath(), previousPage.getFragments(),
+ previousPage.getBaseUri()); //<2>
+ final Integer skipValue = previousPage.getSkip() + previousPage.getTop();
+ final Integer topValue = skipValue + previousPage.getTop() > previousPage.getLast()
+ ? previousPage.getLast() - skipValue : previousPage.getTop();
+ final String newToken = skipValue + topValue < previousPage.getLast() ? UUID.randomUUID().toString() : null;
+ final JPAODataPage nextPage = new JPAODataPage(uriInfo, skipValue, topValue, newToken);
+ replacePage(previousPage, nextPage, em); //<3>
+ return Optional.of(nextPage);
+ } catch (final ODataException e) {
+ return Optional.empty();
+ }
+ }
+ return Optional.empty();
+ }
+----
+<1> Reading the previous page.
+<2> Calling Olingo's URI parser to get a UriInfo.
+<3> Save the next page on the database.
+
+For this variant we want to remove the already processed page on the database be the new page. This is the reason why we cannot use `savePage` here:
+
+[source,java]
+----
+ private void replacePage(final Pages previousPage, final JPAODataPage newPage, final EntityManager em) {
+
+ em.getTransaction().begin();
+ em.remove(previousPage);
+ if (newPage.skipToken() != null) {
+ final Pages pagesItem = new Pages(previousPage, newPage.skip(), (String) newPage.skipToken());
+ em.persist(pagesItem);
+ }
+ em.getTransaction().commit();
+ }
+----
+
+[WARNING]
+====
+We cannot force the client to read all pages. That is, we must take into account that over the time the Page table
+get bigger and bigger, filled with garbage. To get rid of it, we have to have a clean-up job, removing old entries.
+
+====
+
+
+=== Use an external cache
+As an alternative we can use an external cache that offers a lifetime for its entries. There might be other option, but
+for this tutorial, we use Redis. It will not be described how to set it up. There are a lot of tutorial out there that handle this topic.
+For the tutorial we assume Redis it is available. +
+Even so Spring offers an encapsulation to access Redis, we use Jedis as Java API. We get it by adding the following dependency to our POM:
+
+[source,XML]
+----
+
+ redis.clients
+ jedis
+
+----
+
+To be able to use Jedis within our paging provider we first must create a JedisPool. We
+extend class ProcessorConfiguration for this:
+
+[source, java]
+----
+public class ProcessorConfiguration {
+ public static final String REQUEST_ID = "RequestId";
+ public static final String REDIS = "Redis"; //<1>
+
+ @Bean
+ JedisPool jedisPool() {
+ final JedisPoolConfig poolConfig = new JedisPoolConfig();
+ poolConfig.setJmxEnabled(false);
+ return new JedisPool(poolConfig, "localhost", 6379); //<2>
+ }
+----
+
+<1> Constant used as identifier for the JedisPool in the request context.
+<2> Creation of the JedisPool with host and port.
+
+Next, we need to make it available:
+
+[source,java]
+----
+ JPAODataRequestContext requestContext(@Autowired final JedisPool jedisPool) {
+ return JPAODataRequestContext.with()
+ ...
+ .setParameter(REDIS, jedisPool) //<1>
+ ...
+ .build();
+ }
+
+----
+<1> Add JedisPool instance as a parameter to the request context
+
+We store the page information as key - value pairs. We start with a set of constants containing the keys. We also have to adopt
+the interface of `savePage`
+
+[source,java]
+----
+ private static final int EXPIRES_AFTER = 300; // <1>
+ private static final int MAX_SIZE = 50; // Page size
+ private static final String FRAGMENTS = "fragments";
+ private static final String QUERY_PATH = "queryPath";
+ private static final String O_DATA_PATH = "oDataPath";
+ private static final String BASE_URI = "baseUri";
+ private static final String LAST = "last";
+ private static final String TOP = "top";
+ private static final String SKIP = "skip";
+
+ ...
+
+ @Override
+ public Optional getFirstPage(
+ final JPARequestParameterMap requestParameter,
+ final JPAODataPathInformation pathInformation,
+ final UriInfo uriInfo,
+ @Nullable final Integer preferredPageSize,
+ final JPACountQuery countQuery,
+ final EntityManager em) throws ODataApplicationException {
+
+ ...
+ if(skipToken != null)
+ savePage(pathInformation, last, page, requestParameter.get(ProcessorConfiguration.REDIS));//<2>
+ ...
+ }
+
+----
+<1> Lifetime in seconds.
+<2> Using the new interface of `savePage`.
+
+[source,java]
+----
+ private void savePage(final JPAODataPathInformation pathInformation, final Long last,
+ final JPAODataPage page, final Object pool) {
+
+ if (page.skipToken() != null
+ && pool instanceof final JedisPool jedisPool) {
+ try (var jedis = jedisPool.getResource()) {
+ final Map values = new HashMap<>();
+ putIfNotNull(values, SKIP, page.skip());
+ putIfNotNull(values, TOP, page.top());
+ putIfNotNull(values, LAST, last > Integer.MAX_VALUE ? Integer.MAX_VALUE : last.intValue());
+ putIfNotNull(values, BASE_URI, pathInformation.baseUri());
+ putIfNotNull(values, O_DATA_PATH, pathInformation.oDataPath());
+ putIfNotNull(values, QUERY_PATH, pathInformation.queryPath());
+ putIfNotNull(values, FRAGMENTS, pathInformation.fragments());
+
+ final Pipeline pipeline = jedis.pipelined();
+ pipeline.hset((String) page.skipToken(), values);
+ pipeline.expire((String) page.skipToken(), EXPIRES_AFTER);
+ pipeline.sync();
+ } catch (final JedisConnectionException e) {
+ log.error("Redis exception", e);
+ throw e;
+ }
+ }
+ }
+
+ private void putIfNotNull(@Nonnull final Map values, @Nonnull final String name,
+ @Nullable final Integer value) {
+ if (value != null)
+ values.put(name, Integer.toString(value));
+
+ }
+
+ private void putIfNotNull(@Nonnull final Map values, @Nonnull final String name,
+ @Nullable final String value) {
+ if (value != null)
+ values.put(name, value);
+ }
+----
+
+Also `getNextPage` has to be adopted:
+
+[source, java]
+----
+ @Override
+ public Optional getNextPage(@Nonnull final String skipToken, final OData odata,
+ final ServiceMetadata serviceMetadata, final JPARequestParameterMap requestParameter, final EntityManager em) {
+ final Map previousPage = getPreviousPage(skipToken, requestParameter.get(
+ ProcessorConfiguration.REDIS)); //<1>
+ if (previousPage.size() > 0) {
+ try {
+ final UriInfo uriInfo = new Parser(serviceMetadata.getEdm(), odata)
+ .parseUri(getString(previousPage, O_DATA_PATH), getString(previousPage, QUERY_PATH), getString(previousPage,
+ FRAGMENTS), getString(previousPage, BASE_URI));
+ final Integer skipValue = getInteger(previousPage, SKIP) + getInteger(previousPage, TOP);
+ final Integer topValue = skipValue + getInteger(previousPage, TOP) > getInteger(previousPage, LAST)
+ ? getInteger(previousPage, LAST) - skipValue : getInteger(previousPage, TOP);
+ final String newToken = skipValue + topValue < getInteger(previousPage, LAST) ? UUID.randomUUID().toString()
+ : null;
+ final JPAODataPage nextPage = new JPAODataPage(uriInfo, skipValue, topValue, newToken);
+ replacePage(previousPage, nextPage, requestParameter.get(ProcessorConfiguration.REDIS)); //<2>
+ return Optional.of(nextPage);
+ } catch (final ODataException e) {
+ Optional.empty();
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Map getPreviousPage(final String skipToken, final Object pool) {
+ if (skipToken != null
+ && pool instanceof final JedisPool jedisPool) {
+ try (var jedis = jedisPool.getResource()) {
+ final Map values = jedis.hgetAll(skipToken.replace("'", ""));
+ if (values != null)
+ return values;
+ }
+ }
+ return Collections.emptyMap();
+ }
+
+ @CheckForNull
+ private String getString(@Nonnull final Map values, @Nonnull final String name) {
+ return values.get(name);
+ }
+
+ @Nonnull
+ private Integer getInteger(@Nonnull final Map values, @Nonnull final String name) {
+ return Integer.valueOf(Objects.requireNonNull(values.get(name), "Missing value for " + name));
+ }
+----
+
+<1> Retrieval of previous page.
+<2> Writing the next page.
+
+[WARNING]
+====
+Using Redis helps us to keep our cache clean, but, as usual, we do not get this for free. We have to operate another component.
+
+====
diff --git a/jpa-tutorial/Questions/WhatIsTheProblemWithInAndExist.adoc b/jpa-tutorial/Questions/WhatIsTheProblemWithInAndExist.adoc
index a209ad0f8..665a9cc2f 100644
--- a/jpa-tutorial/Questions/WhatIsTheProblemWithInAndExist.adoc
+++ b/jpa-tutorial/Questions/WhatIsTheProblemWithInAndExist.adoc
@@ -20,7 +20,7 @@ INSERT INTO "Product" VALUES (4, 'Trousers', 'white', 2);
INSERT INTO "Product" VALUES (5, 'Shirt', 'red', 1);
----
-So, there is a table "Product" with some columns, like the color of the product. Let's assume we like to find all the products that are either blue or white. The following SQL query would do the job:
+So, there is a table "Product" with some columns, like the color of the product. Let's assume we like to find all the products that are either _blue_ or _white_. The following SQL query would do the job:
[source,sql]
----
@@ -187,9 +187,9 @@ SELECT COUNT(*)
FROM "AdministrativeDivision" t0
WHERE (t0."CodePublisher", t0."CodeID", t0."DivisionCode") IN (
SELECT t1."CodePublisher",t1."ParentCodeID", t1."ParentDivisionCode"
- FROM "OLINGO"."AdministrativeDivision" t1
- GROUP BY t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
- HAVING (COUNT(t1."DivisionCode") >= 2))
+ FROM "OLINGO"."AdministrativeDivision" t1
+ GROUP BY t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
+ HAVING (COUNT(t1."DivisionCode") >= 2))
----
@@ -200,12 +200,12 @@ SELECT COUNT(*)
FROM "AdministrativeDivision" t0
WHERE EXISTS (
SELECT t1."CodePublisher"
- FROM "AdministrativeDivision" t1
- WHERE t0."CodePublisher" = "CodePublisher"
- AND t0."CodeID" = t1."ParentCodeID"
- AND t0."DivisionCode" = t1."ParentDivisionCode"
- GROUP BY t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
- HAVING (COUNT(t1."DivisionCode") >= 2))
+ FROM "AdministrativeDivision" t1
+ WHERE t0."CodePublisher" = "CodePublisher"
+ AND t0."CodeID" = t1."ParentCodeID"
+ AND t0."DivisionCode" = t1."ParentDivisionCode"
+ GROUP BY t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
+ HAVING (COUNT(t1."DivisionCode") >= 2))
----
The query should count the number of administrative divisions that have at least two subdivisions.
@@ -223,3 +223,21 @@ The following execution times could be measured:
This is not a scientific measurement, but gives a good impression about the difference or the performance
increase an IN can give. So, in case such a query is required and the relation comprises multiple columns, we
cannot use the Criteria Builder, we have to generate a parameterized query.
+
+One last remark: In case we look for result that do not match in the results of the sub-query, we have to use NOT IN or
+NOT EXISTS, respectively. The variant behave a bit different, if the result set of the sub-query contains Null values.
+NOT EXISTS behaves as expected, but NOT IN will not return a result or create an error. So, for NOT IN we have to add
+Null check e.g., to find all the administrative divisions that have no subdivisions:
+[source,sql]
+----
+SELECT count(*)
+ FROM "AdministrativeDivision" t0
+ WHERE (t0."CodePublisher", t0."CodeID", t0."DivisionCode") NOT IN (
+ SELECT t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
+ FROM "AdministrativeDivision" t1
+ WHERE t1."CodePublisher" IS NOT NULL
+ AND t1."ParentCodeID" IS NOT NULL
+ AND t1."ParentDivisionCode" IS NOT NULL
+ GROUP BY t1."CodePublisher", t1."ParentCodeID", t1."ParentDivisionCode"
+ HAVING (COUNT(t1."CodePublisher") <> 0))
+----
\ No newline at end of file
diff --git a/jpa-tutorial/QuickStart/QuickStart.adoc b/jpa-tutorial/QuickStart/QuickStart.adoc
index a8ae653aa..4123d5303 100644
--- a/jpa-tutorial/QuickStart/QuickStart.adoc
+++ b/jpa-tutorial/QuickStart/QuickStart.adoc
@@ -46,7 +46,7 @@ This should contain the information about archetype:
com.sap.olingoodata-jpa-archetype-spring
- 2.0.0
+ 2.1.0
diff --git a/jpa/odata-jpa-annotation/.project b/jpa/odata-jpa-annotation/.project
index 800458209..3e42f49f5 100644
--- a/jpa/odata-jpa-annotation/.project
+++ b/jpa/odata-jpa-annotation/.project
@@ -32,12 +32,9 @@
- org.eclipse.jem.workbench.JavaEMFNature
- org.eclipse.wst.common.modulecore.ModuleCoreNatureorg.eclipse.jdt.core.javanatureorg.eclipse.m2e.core.maven2Natureorg.eclipse.wst.common.project.facet.core.nature
- org.whitesource.eclipse.plugin.WSNature
diff --git a/jpa/odata-jpa-coverage/.project b/jpa/odata-jpa-coverage/.project
new file mode 100644
index 000000000..86e0d38bf
--- /dev/null
+++ b/jpa/odata-jpa-coverage/.project
@@ -0,0 +1,17 @@
+
+
+ jpa-coverage
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/jpa/odata-jpa-coverage/pom.xml b/jpa/odata-jpa-coverage/pom.xml
index cf230382c..87ff98e04 100644
--- a/jpa/odata-jpa-coverage/pom.xml
+++ b/jpa/odata-jpa-coverage/pom.xml
@@ -4,7 +4,7 @@
com.sap.olingoodata-jpa
- 2.1.0-SNAPSHOT
+ 2.1.0-SNAPSHOTodata-jpa-coverage
@@ -35,6 +35,7 @@
com.sap.olingoodata-jpa-test
+ compilecom.sap.olingo
@@ -87,4 +88,4 @@
pom
-
\ No newline at end of file
+
diff --git a/jpa/odata-jpa-metadata/.project b/jpa/odata-jpa-metadata/.project
index 3de8bd583..29cac8c24 100644
--- a/jpa/odata-jpa-metadata/.project
+++ b/jpa/odata-jpa-metadata/.project
@@ -15,16 +15,6 @@
-
- org.eclipse.wst.validation.validationbuilder
-
-
-
-
- org.whitesource.eclipse.plugin.WSbuilder
-
-
- org.eclipse.m2e.core.maven2Builder
@@ -32,7 +22,6 @@
- org.eclipse.jem.workbench.JavaEMFNatureorg.eclipse.wst.common.modulecore.ModuleCoreNatureorg.eclipse.jdt.core.javanatureorg.eclipse.m2e.core.maven2Nature
diff --git a/jpa/odata-jpa-metadata/pom.xml b/jpa/odata-jpa-metadata/pom.xml
index c798a5b50..da1ecf585 100644
--- a/jpa/odata-jpa-metadata/pom.xml
+++ b/jpa/odata-jpa-metadata/pom.xml
@@ -5,9 +5,9 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4.0.0
- com.sap.olingo
- odata-jpa
- 2.1.0-SNAPSHOT
+ com.sap.olingo
+ odata-jpa
+ 2.1.0-SNAPSHOTodata-jpa-metadata
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAnnotatable.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAnnotatable.java
index e5083ad90..93530dde0 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAnnotatable.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAnnotatable.java
@@ -5,9 +5,13 @@
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotation;
+import com.sap.olingo.jpa.metadata.core.edm.extension.vocabularies.AliasAccess;
+import com.sap.olingo.jpa.metadata.core.edm.extension.vocabularies.PropertyAccess;
+import com.sap.olingo.jpa.metadata.core.edm.extension.vocabularies.TermAccess;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;
/**
+ * Gives access to OData annotations
*
* @author Oliver Grande
* @since 1.1.1
@@ -25,5 +29,77 @@ public interface JPAAnnotatable {
@CheckForNull
CsdlAnnotation getAnnotation(@Nonnull String alias, @Nonnull String term) throws ODataJPAModelException;
+ /**
+ * Returns the value of a given property of an annotation. E.g.,
+ * getAnnotationValue("Capabilities", "FilterRestrictions", "filterable")
+ *
+ * The value is returned as instance of corresponding type, with the following features
+ *
+ *
Enumerations are returned as strings
+ *
Path are returned as {@link JPAPath}
+ *
Navigation path are returned as {@link JPAAssociationPath}
+ *
+ * @param alias of the vocabulary.
+ * @param term of the annotation in question.
+ * @param property the value is requested for.
+ * @return The value of the property
+ * @throws ODataJPAModelException
+ * @since 2.1.0
+ */
+ @CheckForNull
+ Object getAnnotationValue(@Nonnull String alias, @Nonnull String term, @Nonnull String property)
+ throws ODataJPAModelException;
+
+ /**
+ * Returns the value of a given property of an annotation. E.g.,
+ * getAnnotationValue("Capabilities", "FilterRestrictions", "filterable", Boolean.class)
+ *
+ * The value is returned as instance of corresponding type, with the following features
+ *
+ *
Enumerations are returned as strings
+ *
Path are returned as {@link JPAPath}
+ *
Navigation path are returned as {@link JPAAssociationPath}
+ *
+ * @param Java type of annotation.
+ * @param alias of the vocabulary.
+ * @param term of the annotation in question.
+ * @param propertyName the value is requested for.
+ * @param type java type of property e.g., Boolean.class.
+ * @return
+ * @throws ODataJPAModelException
+ * @since 2.1.0
+ */
+ @SuppressWarnings("unchecked")
+ @CheckForNull
+ default T getAnnotationValue(@Nonnull final String alias, @Nonnull final String term,
+ @Nonnull final String property, @Nonnull final Class> type) throws ODataJPAModelException {
+ return (T) getAnnotationValue(alias, term, property);
+ }
+
+ /**
+ * Returns the value of a given property of an annotation. E.g.,
+ * getAnnotationValue(Aliases.CAPABILITIES, Terms.FILTER_RESTRICTIONS, FilterRestrictionsProperties.FILTERABLE, Boolean.class)
+ *
+ * The value is returned as instance of corresponding type, with the following features
+ *
+ *
Enumerations are returned as strings
+ *
Path are returned as {@link JPAPath}
+ *
Navigation path are returned as {@link JPAAssociationPath}
+ *
+ * @param Java type of annotation.
+ * @param alias of the vocabulary.
+ * @param term of the annotation in question.
+ * @param property the value is requested for.
+ * @param type java type of property e.g., Boolean.class.
+ * @return
+ * @throws ODataJPAModelException
+ * @since 2.1.0
+ */
+ @CheckForNull
+ default T getAnnotationValue(@Nonnull final AliasAccess alias, @Nonnull final TermAccess term,
+ @Nonnull final PropertyAccess property, @Nonnull final Class type) throws ODataJPAModelException {
+ return getAnnotationValue(alias.alias(), term.term(), property.property(), type);
+ }
+
String getExternalName();
}
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAttribute.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAttribute.java
index d76a593cd..95f0b48ad 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAttribute.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/api/JPAAttribute.java
@@ -86,19 +86,29 @@ public interface JPAAttribute extends JPAElement, JPAAnnotatable {
*/
public boolean isCollection();
- public boolean isComplex();
+ public default boolean isComplex() {
+ return false;
+ }
/**
* True if the property has an enum as type
* @return
*/
- public boolean isEnum();
+ public default boolean isEnum() {
+ return false;
+ }
- public boolean isEtag();
+ public default boolean isEtag() {
+ return false;
+ }
- public boolean isKey();
+ public default boolean isKey() {
+ return false;
+ }
- public boolean isSearchable();
+ public default boolean isSearchable() {
+ return false;
+ }
public boolean hasProtection();
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelException.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelException.java
index b33641e6e..62e27d70c 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelException.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelException.java
@@ -88,7 +88,8 @@ public enum MessageKeys implements ODataJPAMessageKey {
PATH_ELEMENT_NOT_EMBEDDABLE,
DB_TYPE_NOT_DETERMINED,
FILE_NOT_FOUND,
- MISSING_ONE_TO_ONE_ANNOTATION;
+ MISSING_ONE_TO_ONE_ANNOTATION,
+ ENTITY_TYPE_NOT_FOUND;
@Override
public String getKey() {
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelInternalException.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelInternalException.java
new file mode 100644
index 000000000..87c8bbb1c
--- /dev/null
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/exception/ODataJPAModelInternalException.java
@@ -0,0 +1,12 @@
+package com.sap.olingo.jpa.metadata.core.edm.mapper.exception;
+
+public class ODataJPAModelInternalException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+ public final ODataJPAModelException rootCause;
+
+ public ODataJPAModelInternalException(final ODataJPAModelException rootCause) {
+ super();
+ this.rootCause = rootCause;
+ }
+}
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntitySet.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntitySet.java
index c51608ddc..d4ec2eb07 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntitySet.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntitySet.java
@@ -1,7 +1,11 @@
package com.sap.olingo.jpa.metadata.core.edm.mapper.impl;
+import static com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException.MessageKeys.ENTITY_TYPE_NOT_FOUND;
+
import java.util.List;
+import javax.annotation.CheckForNull;
+
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotation;
import org.apache.olingo.commons.api.edm.provider.CsdlEntitySet;
import org.apache.olingo.commons.api.edm.provider.CsdlEntityType;
@@ -37,6 +41,7 @@ final class IntermediateEntitySet extends IntermediateTopLevelEntity implements
* @return
*/
@Override
+ @CheckForNull
public JPAEntityType getODataEntityType() {
if (entityType.asTopLevelOnly())
return (JPAEntityType) entityType.getBaseType();
@@ -56,7 +61,7 @@ protected synchronized void lazyBuildEdmItem() throws ODataJPAModelException {
postProcessor.processEntitySet(this);
edmEntitySet = new CsdlEntitySet();
- final CsdlEntityType edmEt = ((IntermediateEntityType>) getODataEntityType()).getEdmItem();
+ final var edmEt = determineEdmType();
edmEntitySet.setName(getExternalName());
edmEntitySet.setType(buildFQN(edmEt.getName()));
@@ -85,4 +90,11 @@ public CsdlAnnotation getAnnotation(final String alias, final String term) throw
return filterAnnotation(alias, term);
}
+ private CsdlEntityType determineEdmType() throws ODataJPAModelException {
+ final IntermediateEntityType> type = (IntermediateEntityType>) getODataEntityType();
+ if (type != null)
+ return type.getEdmItem();
+ throw new ODataJPAModelException(ENTITY_TYPE_NOT_FOUND, getInternalName());
+ }
+
}
\ No newline at end of file
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntityType.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntityType.java
index 0ef16e410..979d3fd37 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntityType.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateEntityType.java
@@ -30,6 +30,7 @@
import org.apache.olingo.commons.api.edm.provider.CsdlEntityType;
import org.apache.olingo.commons.api.edm.provider.CsdlProperty;
import org.apache.olingo.commons.api.edm.provider.CsdlPropertyRef;
+import org.apache.olingo.commons.api.edm.provider.annotation.CsdlDynamicExpression;
import org.apache.olingo.server.api.uri.UriResourceProperty;
import com.sap.olingo.jpa.metadata.core.edm.annotation.EdmEntityType;
@@ -42,8 +43,8 @@
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAEntityType;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAPath;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAQueryExtension;
-import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAStructuredType;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;
+import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelInternalException;
import com.sap.olingo.jpa.metadata.core.edm.mapper.extension.IntermediateEntityTypeAccess;
/**
@@ -87,10 +88,48 @@ public CsdlAnnotation getAnnotation(final String alias, final String term) throw
return filterAnnotation(alias, term);
}
+ @Override
+ public Object getAnnotationValue(final String alias, final String term, final String property)
+ throws ODataJPAModelException {
+
+ try {
+ return Optional.ofNullable(getAnnotation(alias, term))
+ .map(CsdlAnnotation::getExpression)
+ .map(expression -> getAnnotationValue(property, expression))
+ .orElse(null);
+ } catch (final ODataJPAModelInternalException e) {
+ throw e.rootCause;
+ }
+ }
+
+ @Override
+ protected Object getAnnotationDynamicValue(final String property, final CsdlDynamicExpression expression)
+ throws ODataJPAModelInternalException {
+ try {
+ if (expression.isRecord()) {
+ // This may create a problem if the property in question is a record itself. Currently non is supported in
+ // standard
+ final var propertyValue = findAnnotationPropertyValue(property, expression);
+ if (propertyValue.isPresent()) {
+ return getAnnotationValue(property, propertyValue.get());
+ }
+ } else if (expression.isCollection()) {
+ return getAnnotationCollectionValue(expression);
+ } else if (expression.isPropertyPath()) {
+ return getPath(expression.asPropertyPath().getValue());
+ } else if (expression.isNavigationPropertyPath()) {
+ return getAssociationPath(expression.asNavigationPropertyPath().getValue());
+ }
+ return null;
+ } catch (final ODataJPAModelException e) {
+ throw new ODataJPAModelInternalException(e);
+ }
+ }
+
@Override
public Optional getAttribute(final String internalName) throws ODataJPAModelException {
buildEdmTypeIfEmpty();
- final Optional a = super.getAttribute(internalName);
+ final var a = super.getAttribute(internalName);
if (a.isPresent())
return a;
return getKey(internalName);
@@ -99,7 +138,7 @@ public Optional getAttribute(final String internalName) throws ODa
@Override
public Optional getAttribute(final UriResourceProperty uriResourceItem) throws ODataJPAModelException {
buildEdmTypeIfEmpty();
- final Optional a = super.getAttribute(uriResourceItem);
+ final var a = super.getAttribute(uriResourceItem);
if (a.isPresent())
return a;
return getKey(uriResourceItem);
@@ -107,7 +146,7 @@ public Optional getAttribute(final UriResourceProperty uriResource
@Override
public JPACollectionAttribute getCollectionAttribute(final String externalName) throws ODataJPAModelException {
- final JPAPath path = getPath(externalName);
+ final var path = getPath(externalName);
if (path != null && path.getLeaf() instanceof final JPACollectionAttribute collectionAttribute)
return collectionAttribute;
return null;
@@ -115,13 +154,13 @@ public JPACollectionAttribute getCollectionAttribute(final String externalName)
@Override
public String getContentType() throws ODataJPAModelException {
- final IntermediateSimpleProperty stream = getStreamProperty();
+ final var stream = getStreamProperty();
return stream.getContentType();
}
@Override
public JPAPath getContentTypeAttributePath() throws ODataJPAModelException {
- final String propertyInternalName = getStreamProperty().getContentTypeProperty();
+ final var propertyInternalName = getStreamProperty().getContentTypeProperty();
if (propertyInternalName == null || propertyInternalName.isEmpty()) {
return null;
}
@@ -131,7 +170,7 @@ public JPAPath getContentTypeAttributePath() throws ODataJPAModelException {
@Override
public Optional getDeclaredAttribute(@Nonnull final String internalName) throws ODataJPAModelException {
- final Optional a = super.getDeclaredAttribute(internalName);
+ final var a = super.getDeclaredAttribute(internalName);
if (a.isPresent())
return a;
return getKey(internalName);
@@ -212,7 +251,7 @@ public Optional> getQueryExtension(
@Override
public List getSearchablePath() throws ODataJPAModelException {
- final List allPath = getPathList();
+ final var allPath = getPathList();
final List searchablePath = new ArrayList<>();
for (final JPAPath p : allPath) {
if (p.getLeaf().isSearchable())
@@ -375,7 +414,7 @@ boolean dbEquals(final String dbCatalog, final String dbSchema, @Nonnull final S
}
boolean determineAbstract() {
- final int modifiers = jpaManagedType.getJavaType().getModifiers();
+ final var modifiers = jpaManagedType.getJavaType().getModifiers();
return Modifier.isAbstract(modifiers);
}
@@ -420,7 +459,7 @@ private void addKeyAttribute(final List intermediateKey, final Lis
private CsdlPropertyRef asPropertyRef(final JPAAttribute idAttribute) {
// TODO setAlias
- final CsdlPropertyRef keyElement = new CsdlPropertyRef();
+ final var keyElement = new CsdlPropertyRef();
keyElement.setName(idAttribute.getExternalName());
return keyElement;
}
@@ -433,10 +472,10 @@ private void buildEdmTypeIfEmpty() throws ODataJPAModelException {
private List buildEmbeddedIdKey(final JPAAttribute attribute) throws ODataJPAModelException {
- final JPAStructuredType id = ((IntermediateEmbeddedIdProperty) attribute).getStructuredType();
+ final var id = ((IntermediateEmbeddedIdProperty) attribute).getStructuredType();
final List keyElements = new ArrayList<>(id.getTypeClass().getDeclaredFields().length);
- final Field[] keyFields = id.getTypeClass().getDeclaredFields();
- for (int i = 0; i < keyFields.length; i++) {
+ final var keyFields = id.getTypeClass().getDeclaredFields();
+ for (var i = 0; i < keyFields.length; i++) {
id.getAttribute(keyFields[i].getName()).ifPresent(keyElements::add);
}
return keyElements;
@@ -455,7 +494,7 @@ private boolean determineAsEntitySet() {
}
private boolean determineAsSingleton() {
- final EdmEntityType jpaEntityType = this.jpaManagedType.getJavaType().getAnnotation(EdmEntityType.class);
+ final var jpaEntityType = this.jpaManagedType.getJavaType().getAnnotation(EdmEntityType.class);
return jpaEntityType != null && (jpaEntityType.as() == EdmTopLevelElementRepresentation.AS_SINGLETON
|| jpaEntityType.as() == EdmTopLevelElementRepresentation.AS_SINGLETON_ONLY);
}
@@ -473,7 +512,7 @@ private Optional> determineExtensio
extensionQueryProvider = Optional.of(Optional.empty());
final Optional jpaEntityType = getAnnotation(jpaJavaType, EdmEntityType.class);
if (jpaEntityType.isPresent()) {
- final Class provider = (Class) jpaEntityType
+ final var provider = (Class) jpaEntityType
.get().extensionProvider();
final Class> defaultProvider = EdmQueryExtensionProvider.class;
if (provider != null && provider != defaultProvider)
@@ -492,7 +531,7 @@ private void determineHasEtag() throws ODataJPAModelException {
etagPath = Optional.of(getPath(property.getValue().getExternalName(), false));
}
}
- if (getBaseType() instanceof IntermediateEntityType> baseEntityType)
+ if (getBaseType() instanceof final IntermediateEntityType> baseEntityType)
etagPath = Optional.ofNullable(baseEntityType.getEtagPath());
}
@@ -523,4 +562,5 @@ private List resolveEmbeddedId(final IntermediateEmbeddedIdPropert
throws ODataJPAModelException {
return ((IntermediateStructuredType) embeddedId.getStructuredType()).getEdmItem().getProperties();
}
+
}
diff --git a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateModelElement.java b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateModelElement.java
index f31f62471..3ca21ac60 100644
--- a/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateModelElement.java
+++ b/jpa/odata-jpa-metadata/src/main/java/com/sap/olingo/jpa/metadata/core/edm/mapper/impl/IntermediateModelElement.java
@@ -8,12 +8,16 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edm.provider.CsdlAbstractEdmItem;
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotation;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression.ConstantExpressionType;
+import org.apache.olingo.commons.api.edm.provider.annotation.CsdlDynamicExpression;
+import org.apache.olingo.commons.api.edm.provider.annotation.CsdlExpression;
+import org.apache.olingo.commons.api.edm.provider.annotation.CsdlPropertyValue;
import com.sap.olingo.jpa.metadata.api.JPAEdmMetadataPostProcessor;
import com.sap.olingo.jpa.metadata.core.edm.annotation.EdmAnnotation;
@@ -22,6 +26,7 @@
import com.sap.olingo.jpa.metadata.core.edm.extension.vocabularies.ODataAnnotatable;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAEdmNameBuilder;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;
+import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelInternalException;
import com.sap.olingo.jpa.metadata.core.edm.mapper.extension.IntermediateModelItemAccess;
abstract class IntermediateModelElement implements IntermediateModelItemAccess {
@@ -97,8 +102,8 @@ protected List extractEdmModelElements(
for (final IntermediateModelElement bufferItem : mappingBuffer.values()) {
if (!bufferItem.toBeIgnored) { // NOSONAR
- final IntermediateModelElement element = bufferItem;
- final CsdlAbstractEdmItem edmItem = element.getEdmItem();
+ final var element = bufferItem;
+ final var edmItem = element.getEdmItem();
if (!element.ignore())
extractionTarget.add((T) edmItem);
}
@@ -156,7 +161,7 @@ protected void getAnnotations(final List edmAnnotations, final C
* @return
*/
protected final String buildFQTableName(final String schema, final String name) {
- final StringBuilder fqt = new StringBuilder();
+ final var fqt = new StringBuilder();
if (schema != null && !schema.isEmpty()) {
fqt.append(schema);
fqt.append(".");
@@ -166,13 +171,12 @@ protected final String buildFQTableName(final String schema, final String name)
}
private void extractAnnotations(final List edmAnnotations, final AnnotatedElement element,
- final String internalName)
- throws ODataJPAModelException {
- final EdmAnnotation jpaAnnotation = element.getAnnotation(EdmAnnotation.class);
+ final String internalName) throws ODataJPAModelException {
+ final var jpaAnnotation = element.getAnnotation(EdmAnnotation.class);
if (jpaAnnotation != null) {
- final CsdlAnnotation edmAnnotation = new CsdlAnnotation();
- final String qualifier = jpaAnnotation.qualifier();
+ final var edmAnnotation = new CsdlAnnotation();
+ final var qualifier = jpaAnnotation.qualifier();
edmAnnotation.setTerm(jpaAnnotation.term());
edmAnnotation.setQualifier(qualifier.isEmpty() ? null : qualifier);
if (!(jpaAnnotation.constantExpression().type() == ConstantExpressionType.Int
@@ -244,7 +248,7 @@ protected IntermediateAnnotationInformation getAnnotationInformation() {
}
protected CsdlAnnotation filterAnnotation(final String alias, final String term) {
- final String annotationFqn = annotationInformation.getReferences().convertAlias(alias) + "." + term;
+ final var annotationFqn = annotationInformation.getReferences().convertAlias(alias) + "." + term;
return edmAnnotations.stream()
.filter(a -> annotationFqn.equals(a.getTerm()))
.findFirst()
@@ -256,6 +260,29 @@ protected void retrieveAnnotations(final ODataAnnotatable annotatable, final App
edmAnnotations.addAll(provider.getAnnotations(applicability, annotatable, annotationInformation.getReferences()));
}
+ protected Object getAnnotationValue(final String property, final CsdlExpression annotation)
+ throws ODataJPAModelInternalException {
+ if (annotation.isDynamic()) {
+ return getAnnotationDynamicValue(property, annotation.asDynamic());
+ }
+ return getAnnotationConstantValue(annotation.asConstant());
+ }
+
+ protected Object getAnnotationDynamicValue(final String property, final CsdlDynamicExpression expression)
+ throws ODataJPAModelInternalException {
+ return null;
+ }
+
+ protected Object getAnnotationConstantValue(final CsdlConstantExpression expression) {
+ return switch (expression.getType()) {
+ case Bool -> Boolean.valueOf(expression.getValue());
+ case Int -> Integer.valueOf(expression.getValue());
+ case String -> expression.getValue();
+ case EnumMember -> expression.getValue();
+ default -> throw new IllegalArgumentException("Unexpected value: " + expression.getType());
+ };
+ }
+
protected Map findJavaAnnotation(final String packageName, final Class> clazz) {
return findJavaAnnotation(packageName, clazz.getAnnotations());
}
@@ -275,4 +302,21 @@ private Map findJavaAnnotation(final String packageName, fin
return result;
}
+ protected Optional findAnnotationPropertyValue(final String property,
+ final CsdlDynamicExpression expression) {
+ return expression.asRecord()
+ .getPropertyValues().stream()
+ .filter(value -> property.equals(value.getProperty()))
+ .findFirst()
+ .map(CsdlPropertyValue::getValue);
+ }
+
+ protected Object getAnnotationCollectionValue(final CsdlDynamicExpression expression) {
+ final List