GithubHelp home page GithubHelp logo

davidmoten / odata-client Goto Github PK

View Code? Open in Web Editor NEW
33.0 6.0 8.0 9.59 MB

Java client generator for a service described by OData CSDL 4.0 metadata. Includes Microsoft Graph clients (v1.0 and Beta), Graph Explorer client, Analytics for DevOps, Dynamics CRM clients

License: Apache License 2.0

Java 98.82% HTML 0.18% CSS 0.24% Shell 0.76%
dynamics-crm microsoft-graph microsoft-graph-sdk azure-devops java odata odata-services

odata-client's Introduction

odata-client


Maven Central
codecov

Generates java client for a service described by OData v4 metadata. Includes client libraries for Microsoft Graph primary and beta APIs, Graph Explorer API, Azure Analytics for DevOps APIs, and Microsoft Dynamics CRM. Microsoft Dynamics Finance and Operations client is in another repo.

Status : released to Maven Central

Features

  • High level of type safety in generated code
  • Many unit tests (real service calls are made and recorded then used for unit tests)
  • Builders and method chaining for conciseness and discoverability
  • Immutability! (generated objects from the model are immutable)
  • OData inheritance supported for serialization and derialization
  • Interschema references supported (Thompson-Reuters Datascope API uses interschema references)
  • Http calls using java.net.URLConnection or using Apache HttpClient
  • Collections are Iterable and streamable (via .stream())
  • Paging is handled for you automatically when iterating collections
  • Bound and unbound actions and functions are supported
  • Custom requests supported via client._custom()
  • Generated code is very clean - well formatted, no redundant imports (example)
  • Microsoft Graph v1.0 client
  • Microsoft Graph Beta client
  • Graph Explorer client (test endpoint)
  • Microsoft Analytics for DevOps 1.0, 2.0, 3.0, 4.0 and custom clients.
  • Microsoft Dynamics CRM client
  • Microsoft Dynamics Finance and Operations client (see https://github.com/davidmoten/microsoft-dynamics-finance-client)
  • More generated clients can be added, just raise an issue
  • Runs on Java 8+ (including Java 11+, 17). When running <11 the jaxb dependencies can be excluded from odata-client-runtime.

How to build

mvn clean install

Background

OData is an OASIS standard for building and consuming REST APIs. The odata-client project focuses only on OData HTTP APIs implemented using JSON payloads. A client of an OData service can be generated completely from the metadata document published by the service. An example is the Microsoft Graph Odata metadata.

The main actively supported java clients for OData 4 services are Apache Olingo and the SDL OData Framework. However, neither of these projects generate all of the code you might need. Olingo generates some code but you still have to read the metadata xml to know what you can do with the generated classes. This project odata-client generates nearly all the code you need so that you just follow auto-complete on the available methods to navigate the service.

Microsoft Graph is an OData 4 service with a Java SDK being developed at https://github.com/microsoftgraph/msgraph-sdk-java. Progress is slow and steady (but happening) on that client (8 Nov 2018) and it can do a lot already. My frustrations with the design of that client gave rise to an investigation into generating clients for OData services in general and that investigation turned into this project.

How to generate java classes for an OData service

Add odata-client-maven-plugin and build-helper-maven-plugin to your pom.xml as per odata-client-msgraph/pom.xml. You'll also need to save a copy of the service metadata (at http://SERVICE_ROOT/$metadata) to a file in your src/odata directory. Once everything is in place a build of your maven project will generate the classes and make them available as source (that's what the build-helper-maven-plugin does).

Your plugin section will look like this:

<plugin>
   <groupId>com.github.davidmoten</groupId>
   <artifactId>odata-client-maven-plugin</artifactId>
   <version>VERSION_HERE</version>
   <executions>
     <execution>
       <id>generate-odata-models</id>
       <phase>generate-sources</phase>
       <goals>
         <goal>generate</goal>
       </goals>
       <configuration>
         <metadata>src/main/odata/PATH_TO_THE_METADATA_XML_FILE</metadata>
         <schemas>
           <schema>
            <namespace>NAMESPACE_IN_YOUR_METADATA_FILE</namespace>
            <packageName>package.of.generated.classes</packageName>
           </schema>
           <!-- More schemas -->
         </schemas>
       </configuration>
     </execution>
   </executions>
</plugin>
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.2.0</version>
     <executions>
      <execution>
        <id>add-source</id>
         <phase>generate-sources</phase>
         <goals>
           <goal>add-source</goal>
         </goals>
        <configuration>
          <sources>
            <source>${project.build.directory}/generated-sources/java</source>
          </sources>
        </configuration>
     </execution>
    </executions>
</plugin>

How to create a Client for your OData Service (non-Microsoft API) with Bearer Tokens.

This example considers a custom OData api available at $API_ENDPOINT that authorizes using Bearer tokens in the Authorization header. The custom api has a users list that we want to return with a call to the service. The example has a hardcoded token for simplicity.

The SchemaInfo List should contain the SchemaInfos generated for your service metadata. (See generated classes in packages ending in .schema).

    HttpClientBuilder b =
        HttpClientBuilder
            .create()
            .useSystemProperties();
            
    BearerAuthenticator authenticator =
        new BearerAuthenticator(
            () ->
                "INSERT_TOKEN_HERE",
            "$API_END_POINT");
            
    HttpService httpService =
        new ApacheHttpClientHttpService( //
            new Path(
                "$API_END_POINT",
                PathStyle.IDENTIFIERS_AS_SEGMENTS),
            b::build,
            authenticator::authenticate);
            
    final List<com.github.davidmoten.odata.client.SchemaInfo> schemaInfos =
        Arrays.asList(generate.pkg.schema.SchemaInfo.INSTANCE);
        
    Context context =
        new Context(Serializer.INSTANCE, httpService, Collections.emptyMap(), schemaInfos);

    // in this example Container is the name of the Container element in the metadata
    Container client = new Container(context);
    List<String> userEmails =
        client.users()
	.stream()
        .map(User::getEmail)
        .filter(Optional::isPresent)
        .map(x -> x.get())
        .collect(Collectors.toList());

Limitations

  • Just one key (with multiple properties if desired) per entity is supported (secondary keys are ignored in terms of code generation). I've yet to come across a service that uses multiple keys but it is possible.
  • see TODO

MsGraph Client

Use this dependency:

<dependency>
    <groupId>com.github.davidmoten</groupId>
    <artifactId>odata-client-msgraph</artifactId>
    <version>VERSION_HERE</version>
</dependency>

You can also use artifactId odata-client-msgraph-beta to access the beta endpoint (and they have different package names so you can use both clients in the same project if you want).

If you are running on less than Java 11 then you can exclude some dependencies but only if you are running odata-client version:

<dependency>
    <groupId>com.github.davidmoten</groupId>
    <artifactId>odata-client-msgraph</artifactId>
    <version>VERSION_HERE</version>
    <exclusions>
        <exclusion>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
        </exclusion>
    </exclusions>
</dependency>

As slf4j is used for logging you may wish to exclude the slf4j-api dependency and add the slf4j logging adapter dependency (e.g. slf4j-log4j12) for your logging library:

	<exclusion>
	    <groupId>org.slf4j</groupId>
	    <artifactId>slf4j-api</artifactId>
	</exclusion>

Create a client for the Graph Explorer

There's a web test page for queries called the Graph Explorer. You can use the client against this service for experiments. Bear in mind that the Graph Explorer service doesn't support lots but it is still a good place to get a feel for it.

GraphService client = MsGraph.explorer().build();

or behind a proxy:

GraphService client = MsGraph
  .explorer()
  .proxyHost(proxyHost)
  .proxyPort(8080)
  .build();

Here's an example:

client 
  .users()
  .select("displayName")
  .stream() 
  .limit(10)
  .map(user -> user.getDisplayName().orElse("?"))
  .forEach(System.out::println);

output:

Conf Room Adams
Adele Vance
MOD Administrator
Alex Wilber
Allan Deyoung
Conf Room Baker
Ben Walters
Brian Johnson (TAILSPIN)
Christie Cline
Conf Room Crystal

Here's a contrived example with Graph Explorer that gets 5 attachment names from messages and their sizes. The example makes good use of the streaming capabilities of the odata-client API:

GraphService client = MsGraph.explorer().build();
client
  .me()
  .messages()
  .select("id")
  .stream()
  .flatMap(m -> m.getAttachments().select("name, size").stream())
  .limit(5)
  .map(a -> a.getName().orElse("?") + " " + a.getSize().orElse(-1) + "B")
  .forEach(System.out::println);

output:

analytics_icon.png 2281B
lock_circle_teal.png 2713B
collab_hero_left_2x.png 6496B
ProfileImage_320_48d31887-5fad-4d73-a9f5-3c356e68a038.png 335675B
collab_hero_right_2x.png 6611B

Create a client for Graph v1.0

The first step is to create a client that will be used for all calls in your application.

GraphService client = MsGraph 
    .tenantName(tenantName) 
    .clientId(clientId) 
    .clientSecret(clientSecret) 
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .refreshBeforeExpiry(5, TimeUnit.MINUTES) 
    .build();

Create a client for Graph v1.0 behind a proxy

GraphService client = MsGraph 
    .tenantName(tenantName) 
    .clientId(clientId) 
    .clientSecret(clientSecret) 
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .refreshBeforeExpiry(5, TimeUnit.MINUTES) 
    .proxyHost(proxyHost)
    .proxyPort(proxyPort)
    .proxyUsername(proxyUsername)
    .proxyPassword(proxyPassword)
    .build();

Specify the authentication endpoint

This client supports Client Credential authentication only at the moment. Raise an issue if you need a different sort.

To change the authentication endpoint used to retrieve access tokens (the default is AuthenticationEndpoint.GLOBAL):

GraphService client = MsGraph
    .tenantName(tenantName)
    .clientId(clientId)
    .clientSecret(clientSecret)
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .refreshBeforeExpiry(5, TimeUnit.MINUTES)
    .authenticationEndpoint(AuthenticationEndpoint.GERMANY)
    .build();

If you want to do really complicated things with proxies or http in general you can use the .httpClientProvider or .httpClientBuilderExtras methods (coupled to Apache HttpClient).

Usage example 1 - simple

Here's example usage of the odata-client-msgraph artifact (model classes generated from the MsGraph metadata). Let's connect to the Graph API and list all messages in the Inbox that are unread. Note that paging is completely handled for you in the .stream() method!

String mailbox = "me";
client
    .users(mailbox) 
    .mailFolders("Inbox") 
    .messages() 
    .filter("isRead eq false") 
    .expand("attachments") 
    .stream() 
    .map(x -> x.getSubject().orElse("")) 
    .forEach(System.out::println);

Usage example 2 - more complex

Here's a more complex example which does the following:

  • Counts the number of messages in the Drafts folder
  • Prepares a new message
  • Saves the message in the Drafts folder
  • Changes the subject of the saved message
  • Checks the subject has changed by reloading
  • Checks the count has increased by one
  • Deletes the message
MailFolderRequest drafts = client //
    .users(mailbox) //
    .mailFolders("Drafts");

// count number of messages in Drafts
long count = drafts.messages() //
    .metadataNone() //
    .stream() //
    .count(); //

// Prepare a new message
String id = UUID.randomUUID().toString().substring(0, 6);
Message m = Message
    .builderMessage() //
    .subject("hi there " + id) //
    .body(ItemBody
        .builder() //
        .content("hello there how are you") //
        .contentType(BodyType.TEXT) 
        .build()) //
    .from(Recipient
         .builder() //
         .emailAddress(EmailAddress
             .builder() //
             .address(mailbox) //
             .build())
         .build())
    .build();

// Create the draft message
Message saved = drafts.messages().post(m);

// change subject
client //
    .users(mailbox) //
    .messages(saved.getId().get()) //
    .patch(saved.withSubject("new subject " + id));

// check the subject has changed by reloading the message
String amendedSubject = drafts //
    .messages(saved.getId().get()) //
    .get() //
    .getSubject() //
    .get();
if (!("new subject " + id).equals(amendedSubject)) {
    throw new RuntimeException("subject not amended");
}

long count2 = drafts //
    .messages() //
    .metadataNone() //
    .stream() //
    .count(); //
if (count2 != count + 1) {
    throw new RuntimeException("unexpected count");
}

// Delete the draft message
drafts //
    .messages(saved.getId().get()) //
    .delete();

Collections and paging

odata-client handles the annoying paging stuff under the covers so you don't have to worry about it. Let's look at some examples.

We'll get the list of users:

List<User> users = client.users().toList();

That was easy wasn't it! Under the covers odata-client kept calling pages (using @odata.nextLink) and building the list to return. This won't always be a good idea because the Collection might be very big and you might not want all the data anyway. What are some alternatives?

client.users() is itself an Iterable so you can do this:

for (User user: client.users()) 
  System.out.println(user);

or

Iterator<User> it = client.users().iterator();
while (it.hasNext()) 
  System.out.println(it.next());

You can use .stream():

client.users()
  .stream() 
  .forEach(System.out::println);

Again, all paging is taking care of. When the current page runs out the library requests another one.

If you do want to do explicit paging then you can do this:

CollectionPage<User> users = client.users().get();

CollectionPage has methods currentPage and nextPage.

Your own page size

You can request a different page size than the default via a special HTTP request header but the server may choose to ignore your request:

List<User> users = client.users().maxPageSize(200).get().currentPage();

So what if you want to receive the collection in custom size pages regardless of the page size being delivered by the server? odata-client has two utility methods to help you out. Let's chop the stream of User into pages of 15 elements:

import com.github.davidmoten.odata.client.Util;

Iterator<List<User>> it = Util.buffer(client.users().iterator(), 15);

or with streams:

Stream<List<User>> users = Util.buffer(client.users().stream(), 15);

Getting a collection from a link

Some users need stateless paging so want to recommence the next page from the odata.nextLink value in the page json. Here's an example:

String nextLink = "https://...";
List<User> users = client
  .users()
  .urlOverride(nextLink)
  .get()
  .currentPage();

Getting json for the collection

Some users want to do a passthrough of Graph API responses in JSON format through to their clients. You can obtain a simplified collection page response as JSON like this:

CollectionPage<User> page = client.users().get();
String json = page.toJsonMinimal();

Filtering a collection by type

To only return items of a certain type from a collection request (this is called "restriction to instances of the derived type" in the OData specification):

client
  .directoryObjects()
  .filter(User.class)
  .forEach(System.out::println);

Updating Microsoft Graph metadata

Developer instructions:

cd odata-client-msgraph
./update-metadata.sh
cd ..
## check if still builds!
mvn clean install

Note that the latest metadata is downloaded from microsoft but that also:

  • HasStream="true" is added to EntityType itemAttachment
  • HasStream="true" is added to EntityType message

URLs are:

Timeouts

Connect and read timeouts can be specified when creating the client and apply globally:

GraphService client = MsGraph
    .tenantName(tenantName)
    .clientId(clientId)
    .clientSecret(clientSecret)
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .refreshBeforeExpiry(5, TimeUnit.MINUTES)
    .authenticationEndpoint(AuthenticationEndpoint.GERMANY)
    .build();

Global timeouts can be overriden by request:

List<User> users = client
  .users()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(30, Timeout.SECONDS)
  .toList();

Download an email in SMTP MIME format

Use Message.getStream().

Here's an example using Graph Explorer where we download the MIME format of a random email returned by the service (you can run this exact code yourself to test):

GraphService client = MsGraph.explorer().build();
String mimeMessage = 
  client
    .me()
    .messages()
    .stream()
    .findFirst()
    .get()
    .getStream()
    .get()
    .getStringUtf8();
System.out.println(mimeMessage);

Output:

From: Sundar Ganesan <[email protected]>
To: Megan Bowen <[email protected]>, Alex Wilber
Subject:
Thread-Index: AQHWhnph+xDTvdQDAJzfEoyf/g4tCA==
X-MS-Exchange-MessageSentRepresentingType: 1
Date: Wed, 9 Sep 2020 07:25:22 +0000
Message-ID: 1599636322761
Content-Language: en-US
X-MS-Exchange-Organization-SupervisoryReview-TeamsContext:
	{"SenderId":"19806f8c015344149305dc48012b4789","AadGroupId":null,"ThreadId":"19:[email protected]","MessageId":1599636322761,"ParentMessageId":null,"TenantId":"91c1bcb4-2349-4abd-83c1-6ae4ffaf7f6c","ThreadType":"chat","CorrelationVector":"KuDTOICw9UWVpDHto11XFw.1.1.1.2441281688.1.0"}
X-MS-Has-Attach:
X-MS-TNEF-Correlator:
X-MS-Exchange-Organization-RecordReviewCfmType: 0
Content-Type: multipart/alternative; boundary="_000_1599636322761_"
MIME-Version: 1.0

--_000_1599636322761_
Content-Type: text/plain; charset="us-ascii"

test

--_000_1599636322761_
Content-Type: text/html; charset="us-ascii"

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
</head>
<body>
test
</body>
</html>

--_000_1599636322761_--

Sending an email with an attachment

Unfortunately sending an email with an attachment is complicated somewhat by Microsoft Graph forcing users to use two different methods to upload an attachment depending on the size of the attachment. Default maximum size for an attachment is apparently 25MB according to Microsoft docs though an administrator can increase the limit up to 150MB if required.

To help send an email a helper utility class called Email exists. Here's an example that sends an email with two attachments:

Email
  .mailbox(mailbox) 
  .subject("hi there " + new Date())
  .bodyType(BodyType.TEXT)
  .body("hello there how are you")
  .to("[email protected]")
  .header("x-security-classification", "OFFICIAL")
  .attachment(file)
  .name("list.txt")
  .contentMimeType("text/plain")
  .chunkSize(512 * 1024)
  .attachment("hi there")
  .name("greeting.txt")
  .contentMimeType("text/plain")
  .send(client);

The builder code above does quite a lot for you. For reference here's how you do it using the generated classes only:

GraphService client = ...;
File file = ...;
String contentType = "text/plain";
String mailbox = "[email protected]";

// create new outbound mail in Drafts folder
MailFolderRequest drafts = client //
    .users(mailbox) //
    .mailFolders("Drafts");
Message m = Message.builderMessage() //
    .subject("hi there " + new Date()) //
    .body(ItemBody.builder() //
            .content("hello there how are you") //
            .contentType(BodyType.TEXT).build()) //
    .from(Recipient.builder() //
            .emailAddress(EmailAddress.builder() //
                    .address(mailbox) //
                    .build()) //
            .build()) //
    .toRecipients(Recipient.builder() //
            .emailAddress(EmailAddress.builder() //
                    .address("[email protected]") //
                    .build()) //
            .build()) //
    .build();

m = drafts.messages().post(m);

// Upload attachment to the new mail
// We use different methods depending on the size of the attachment
// because will fail if doesn't match the right size window
if (file.length() < 3000000) {
    client.users(mailbox) //
        .messages(m.getId().get()) //
        .attachments() //
        .post(FileAttachment.builderFileAttachment() //
           .name(file.getName()) //
           .contentBytes(Files.readAllBytes(file.toPath())) //
           .contentType(contentType).build());
} else {
    AttachmentItem a = AttachmentItem //
        .builder() //
        .attachmentType(AttachmentType.FILE) //
        .contentType(contentType) //
        .name(file.getName()) //
        .size(file.length()) //
        .build();
    
    int chunkSize = 500*1024;
    client //
        .users(mailbox) //
        .messages(m.getId().get()) //
        .attachments() //
        .createUploadSession(a) //
        .get() //
        .putChunked() //
        .readTimeout(10, TimeUnit.MINUTES) //
        .upload(file, chunkSize, Retries.builder().maxRetries(2).build());
}

// send the email 
// you can't just use m.send().call() because Graph doesn't 
// honour the path corresponding to m for send method and throws
// a 405 status code
client
  .users(mailbox)
  .messages(m.getId().get())
  .send()
  .call();

Delta collections

Some functions return delta collections which track changes to resource collections. In the Graph API v1.0 there are delta functions on users, calendar events, and messages.

Here's an example of usage:

// get a delta that returns no users but marks where future deltas will be based on
CollectionPage<User> delta = client.users().delta().deltaTokenLatest().get();

// a while later
delta = delta.nextDelta().get();

// delta is a CollectionPage and can be iterated upon or paged through as you like
// if you don't iterate through the collection the next call to 
// nextDelta() will do it for you because the deltaLink is on the last page 
// of the current collection 

// print out all users that have changed
delta.forEach(System.out::println);

// a while later
delta = delta.nextDelta().get();
...

A special streaming method is available also that returns a list of wrapped delta objects then the delta link to use for the next delta call:

Stream<ObjectOrDeltaLink<User>> delta = 
    client
      .users()
      .delta()
      .get()
      .streamWithDeltaLink();

Using the delta link from the last element of the stream the next delta call can be made like so:

Stream<ObjectOrDeltaLink<User>> delta = 
    client 
      .users()
      .overrideUrl(deltaLink)
      .get()
      .streamWithDeltaLink();

ObjectOrDeltaLink is serializable to JSON via its Jackson annotations and at least one user is using the streamWithDeltaLink method to pass large deltas over a network via WebFlux (see issue #44).

Expand

Special support is provided for the $expand option. Here's an example:

Attachment attachment = client
  .users(emailId)
  .mailFolders(folderId)
  .messages(messageId)
  .attachments(attachmentId)
  .expand("microsoft.graph.itemattachment/item")
  .get();
if (attachment instanceof ItemAttachment) {
    ItemAttachment a = (ItemAttachment) attachment;
    OutlookItem item = a.getItem().get();
    ...
}

When the above code is called, the line OutlookItem item = a.getItem().get() will not actually make a network call but rather use the json in unmapped fields of attachment. If the json is not present in the unmapped fields then a network call will be made.

This support is present for both Entity requests and Enitty Collection requests.

Contained Navigation Properties

If in the OData metadata a Navigation Property has ContainsTarget=true then efficiences are enabled for reading and writing such properties (as of 0.1.93).

For example, attachments and/or singleValueExtendedProperties can be defined on a message at creation time of the message so only one network call is required.

Likewise, if .expand("attachments") is set on retrieval of a Message then attachments will be included in the JSON response and the paged collection method Message.getAttachments() will actually use already loaded attachments rather than making an extra network call.

Logging

The default http client Apache httpclient uses Apache Commons Logging and the odata-client libraries use slf4j. To get full access to all logs you'll need to ensure that the right adapters are present that pipe logs to your preferred logging library. Tests in odata-client-msgraph demonstrate the use of log4j as the preferred logger. You'll note that these dependencies are present:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${slf4j.version}</version>
</dependency>

 <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
</dependency>

A configuration file for log4j is also present (in src/test/resources but you might want it in src/main/resources):

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="INFO">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

If you want DEBUG logging just set the Root level to DEBUG.

Serialization

odata-client generated classes are annotated with Jackson JSON annotations specifically to support internal serialization and deserialization for communication with the service. Since 0.1.20 entities are annotated with @JsonInclude(Include.NON_NULL) so that default serialization with Jackson will exclude null values (internally this annotation is overriden for certain use cases such as when we want tell the service to update a field to null).

Custom requests

To get more precise control over the interaction with an OData api you can use the methods of CustomRequest. Here's an example that posts json to the service and recieves a json response:

CustomRequest c = client._custom().withRelativeUrls();
String json = c.getString("me/mailFolders/inbox/messages?$select=id&count=true", RequestOptions.EMPTY, RequestHeader.ODATA_VERSION);
System.out.println(json);

Output:

{
	"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/mailFolders('inbox')/messages(id)",
	"@odata.count": 21,
	"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?$select=id&count=true&$skip=10",
	"value": [
		{
			"@odata.etag": "W/\"CQAAABYAAAAiIsqMbYjsT5e/T7KzowPTAAL0pwit\"",
			"id": "AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAAL0sBcuAAA="
		},
                ...
	]
}

Usage Notes

Streams

To find the read url for a property that is of type Edm.Stream you generally need to read the entity containing the stream property with the Accept: odata.metadata=full request header (set .metadataFull() before calling get() on an entity). As of 0.1.54 this request header is the default for Media Entities or entities with stream properties (but not for collections of these entities).

Implementation Notes

Generator

Some of the metadata access patterns are O(N2) in the generator but does not appear to be significant. If you think it is then the library can start using maps for lookups.

HasStream

Suppose Person has a Navigation Property of Photo then using the TripPin service example, calling HTTP GET of

https://services.odata.org/V4/(S(itwk4e1fqfe4tchtlieb5rhb))/TripPinServiceRW/People('russellwhyte')/Photo

returns:

{
"@odata.context": "http://services.odata.org/V4/(S(itwk4e1fqfe4tchtlieb5rhb))/TripPinServiceRW/$metadata#Photos/$entity",
"@odata.id": "http://services.odata.org/V4/(S(itwk4e1fqfe4tchtlieb5rhb))/TripPinServiceRW/Photos(2)",
"@odata.editLink": "http://services.odata.org/V4/(S(itwk4e1fqfe4tchtlieb5rhb))/TripPinServiceRW/Photos(2)",
"@odata.mediaContentType": "image/jpeg",
"@odata.mediaEtag": "W/\"08D6394B7BA10B11\"",
"Id": 2,
"Name": "My Photo 2"
}

We then grab the @odata.editLink url and call that with /$value on the end to GET the photo bytes:

https://services.odata.org/V4/(S(itwk4e1fqfe4tchtlieb5rhb))/TripPinServiceRW/Photos(2)/$value

Generated classes, final fields and setting via constructors

The initial design for setting fields was via constructors with final fields but Msgraph Beta service goes for gold on the number of fields and well exceeds the JVM limit of 256 with the Windows10GeneralConfiguration entity. As a consequence we have private theoretically mutable fields and a protected no-arg constructor. In practice though the fields are not mutable as the only creation access is through a builder() or with*(...) which return new instances.

PATCH support

If choosing the JVM Http client (via HttpURLConnection) then the HTTP verb PATCH is not supported. However, if using this client and a ProtocolException is thrown then the client attempts the call using the special request header X-HTTP-Override-Method and the verb POST. The detection of the ProtocolException is cached and future PATCH calls will then use the alternative methods (for the runtime life of the application).

TODO

  • support OpenType (arbitrary extra fields get written)
  • support EntityContainer inheritance (maybe, no sample that I've found uses it so far)
  • support precision, scale (maybe)
  • more decoupling of model from presentation in generation
  • use annotations (docs) in javadoc
  • support function composition
  • support count
  • support raw
  • support $ref
  • support updating/creating streams
  • support geographical primitive types (where's the spec?!!)
  • support references to other metadata files (imports)
  • auto-rerequest with odata.metadata=full header if Edm.Stream is read
  • support TypeDefinition
  • only generate classes that are actually used (e.g. not every Entity or ComplexType needs a corresponding Collection request)
  • remove context property "modify.stream.edit.link" from MsGraph client once they support their own @odata.mediaEditLink!
  • add helper methods to generated UploadUrl class in MsGraph
  • add optional retry logic to StreamUploaderChunked
  • increase chunked upload efficiency by writing read bytes to destination immediately, retaining the ability to retry the chunk

odata-client's People

Contributors

corrspt avatar davidmoten avatar dependabot[bot] avatar github-actions[bot] avatar kneringerjohann avatar kriegfrj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

odata-client's Issues

upload file to onedrive through createUploadSession - itemWithPath() not found

Hi David,
Found some posts on githud to do following for creating uploading session to upload an item to a drive using graph client. But i dont see the itemWithPath() available on root() for odataClientFactory. Can you please check on this. or suggest an alternative to CreateUploadSession, to upload large files to ondrive.
I'm referring to this api : https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online

graphClient
.users(oneDriveUserId)
.drive()
.root()
.itemWithPath("filename.pdf")
.createUploadSession(new DriveItemUploadableProperties())
.buildRequest()
.post();

Thanks,
Pavani

Getting contents of a file in drive using DriveItem getContent()

Hi David,

I am trying to get the raw contents of a file in drive using the DriveItem getContent() method. But even though the file has contents, it is null in the DriveItem class. getContent() hits a null pointer exception even though the graph API on the itemid /content works.

Optional<StreamProvider> sp = graphService
                    .drives(driveId)
                    .items(itemId)
                    .get()
                    .getContent();

Can you please confirm the way to use this method? Am I missing something?

Thanks

Odata library doesn't have function to check whether a contact has photo and different odata method to get contact photo

Hi David,

I am looking for an API to directly retrieve a contact photo without having to call get metadata "/photo" first and then call "/photo/$value" which internally does in the below graph library.

graphService.users(mailid).contacts(contactid).photo().get().getStream().get().get()

I can achieve with CustomRequest call, but I am looking some other way without having to form URL.
Can you please help me out on this.
Also, can you please help me out on how to check if a contact has photo. Right now Contact metadata doesnt have property something like "hasPhoto". It would be great to have this property in contact metadata

Accessing Azure response code in Odata Client

Hi David,

The checkResponseCode method in RequestHelper.java is suppressing the actual HTTP status code from MS Graph API and that value is now part of ClientException message. To access this value, we will need to parse ClientException message.

public static void checkResponseCode(String url, HttpResponse response,
            int expectedResponseCodeMin, int expectedResponseCodeMax) {
        if (response.getResponseCode() < expectedResponseCodeMin
                || response.getResponseCode() > expectedResponseCodeMax) {
            throw new ClientException("responseCode=" + response.getResponseCode() + " from url="
                    + url + ", expectedResponseCode in [" + expectedResponseCodeMin + ", "
                    + expectedResponseCodeMax + "], message=\n" + response.getText());
        }
    }

Consider this use case, a SPA client makes a call to Odata client based REST service and passes an invalid parameter. When this invalid parameter is passed to MS Graph API, it throws a HTTP 400, with a message body stating reasons for this HTTP code. I would like to pass response.getResponseCode() and response.getText() back to the client.

responseCode=400 from url=https://graph.microsoft.com/v1.0/organization/xyz, expectedResponseCode in [200, 200]

Any specific reason this pattern is used?

patchOrPut method always return 204 if success in TestingService class

Hi David,

While testing "graphService._service().put(ConfigurationService.getContactPhotoURI(mailid, contactid),
requestHeaderList, inputStream, rawContent.length, RequestOptions.EMPTY)
", I am expecting different HttpResponse code. But It always return 204 if success. I think it should be configuration as TestingService.ContainerBuilder allow us to mock responseStatusCode as well as shown below code

clientBuilder()
.expectRequest(path)
.withMethod(HttpMethod.PUT)
.withPayload(payLoadResource)
.withResponseStatusCode(200)
.withRequestHeaders(requestHeaders)
.build();

Can you please fix it.

Error while calling Mailbox Quota

Hi David,
Odata library is giving below error while getting mailbox quota. Can you please take a look?

API Call:
res = MSGraph.reports().getMailboxUsageDetail("D7").get();

Exception:
{
"timestamp": "2020-12-26T18:27:53.465+00:00",
"path": "/test/quota",
"status": "BAD_REQUEST",
"error": "Bad Request",
"message": "responseCode=400 from url=https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail/(period%3D%22D7%22), expectedResponseCode in [200, 200], message=\n{\r\n "error": {\r\n "code": "BadRequest",\r\n "message": "Resource not found for the segment 'getMailboxUsageDetail'.",\r\n
}

Stream mailFolder messages along with '@odata.deltaLink'

Hi David,
In our use case we are trying to stream all the messages from 'inbox' mailFolder(using delta()) to an agent application as shown below.

Stream<Message> messages = OdataFactory
                    .request()
                    .users('[email protected]')
                    .mailFolders('inbox')
                    .messages()
                    .delta()
                    .stream();

Here we are trying to append '@odata.deltaLink' in the final data to the agent, so that next time agent can use the '@odata.deltaLink' to get incremental changes in the mailFolder. Can you please help to fulfill this requirement using stream?

Thanks
Madan

Invalid Java package names

Azure-Boards-Metadata-v3.0.zip

Using latest release the Java classes generated against Azure Boards includes prefix of "default.x" in generated package names in .java files. I think there is a missing check for reserved words against the package names generated.

Metadata sample file attached. I manually modified the .java files to work around.

Very helpful project.

Connection to Dynamics 365Business Central

Hello David,

I have been playing around with using your library to generate the code to interface with Dynamics 365 Business Central api. This is looking very promising so far and I am able to retrieve all of the entity sets. For example I can retrieve all of the PurchaseInvoice entries for a specific company in Business Central by specifying a baseUrl of:

https://api.businesscentral.dynamics.com/v2.0/environmentname/Sandbox/api/v2.0/companies(comanyid)/

then calling: client.purchaseInvoices().toList()

However when I try to get a specific invoice by executing: client.purchaseInvoices("3681423d-eed6-4a6a-a612-fffc5562cb60").get() i run into this error:

com.github.davidmoten.odata.client.ClientException: responseCode=404 from url=https://api.businesscentral.dynamics.com/v2.0/environmentname/Sandbox/api/v2.0/companies(companyid)/purchaseInvoices/3681423d-eed6-4a6a-a612-fffc5562cb60, expectedResponseCode in [200, 200], message=
{"error":{"code":"BadRequest_NotFound","message":"The request URI is not valid. Since the segment 'purchaseInvoices' refers to a collection, this must be the last segment in the request URI or it must be followed by an function or action that can be bound to it otherwise all intermediate segments must refer to a single resource. CorrelationId: 4d419359-ae9b-41b4-928b-1781d99acb36."}}

As far as I can tell, the problem seems to be in the url. The resulting url from the call is:

https://api.businesscentral.dynamics.com/v2.0/environmentname/Sandbox/api/v2.0/companies(companyid)/purchaseInvoices/3681423d-eed6-4a6a-a612-fffc5562cb60

But the Business Central api is expecting a function call like this:

https://api.businesscentral.dynamics.com/v2.0//Sandbox/api/v2.0/companies(companyid)/purchaseInvoices(3681423d-eed6-4a6a-a612-fffc5562cb60)

Any chance you can point me in the direction of where I might be going wrong?

Many thanks,

Morten

Dynamics 365 CRM (Entity crmbaseentity has no keys)

Hi,
I'm attempting to load a dynamics 365 (CRM) manifest but it contains multiple abstract EntityTypes without a key. Example: <EntityType Name="crmbaseentity" Abstract="true" /> from which all other base types point to as BaseType:
eg. <EntityType Name="account" BaseType="mscrm.crmbaseentity"><Key><PropertyRef Name="accountid" /></Key>...

This results in:
java.lang.IllegalStateException: Entity crmbaseentity has no keys!

Full stack trace:
java.lang.IllegalStateException: Entity crmbaseentity has no keys! at com.github.davidmoten.odata.client.generator.model.EntityType.getFirstKey(EntityType.java:125) at com.github.davidmoten.odata.client.generator.Generator.fieldNames(Generator.java:1115) at com.github.davidmoten.odata.client.generator.Generator.writeEntity(Generator.java:540) at com.github.davidmoten.odata.client.generator.Generator.lambda$generate$5(Generator.java:163) at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1654) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497) at com.github.davidmoten.odata.client.generator.Generator.generate(Generator.java:163) at com.github.davidmoten.odata.client.generator.GeneratorTest.testGenerateDynamics365(GeneratorTest.java:94) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

Cannot deserialize UnsignedByte using Serializer.deserialize

Serialized some Object retrieved by my odata client that contains :

private UnsignedByte userDataPresent;

using com.github.davidmoten.odata.client.Serializer.serializePrettyPrint.

JSON snippet:

"UserDataPresent" : { "value" : 1 },

After that tried to deserialize using Serializer.deserialize(String text, Class<? extends T> cls, ContextPath contextPath, boolean addKeysToContextPath) and got an Exception:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of com.github.davidmoten.odata.client.edm.UnsignedByte (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) at [.... "UserDataPresent" : { "value" : 1 }, ...] (through reference chain: com.somecompany.odata.entity.Order["UserDataPresent"]) at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588) at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195) at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:402) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516) at com.github.davidmoten.odata.client.Serializer.deserialize(Serializer.java:85) ... 3 more

Currently using version 0.1.58.

Deserialization works in case JSON is changed to:

"UserDataPresent" : 1,

Batch requests?

So I've just learned that the remote OData service that I'm trying to connect to requires that all requests (even singletons) be sent as batch requests.

Unless I'm mistaken, the current client generator does not generate clients that support batch requests out-of-the-box?

Paging via @odata.nextLink

Hello David

Thanks for all your efforts. As per your changes in https://github.com/davidmoten/odata-client/#getting-a-collection-from-a-link, we can now do explicit paging and retrieve next page via Odata.nextLink property.

From my understanding, the Odata.nextLink can be retrieved as below
OdataClientFactory .request() .users() .top(n) .get() .nextLink()

While the current page with usage of top() can be retrieved as below
OdataClientFactory.request().users().top(long n).get().currentPage()

By any chance can we get the nextLink as part of the currentPage data itself ? For example when using top in MS Graph api, we get the below output

{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users", "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$top=3&$skiptoken=X%2744537074020001000000253A616268697368656B30324064706F3336356261722E6F6E6D6963726F736F66742E636F6D29557365725F64623435656264662D613139662D343834342D386438652D653535306531396265356163B900000000000000000000%27", "value": [ { "businessPhones": [ "4251001000" ], }

As you can see, here the nextLink is available as part of the retrieved page content itself. Do we have a similar facility in the Odata-client ? Bascially, I would like to have the nextLink returned as par of my currentPage itself. I need to return this information to a client which will then decide how to use the nextLink.

Items returned by collection request have parent's contextPath

In the code below using the msgraph client the object x has the contextPath (graph url) corresponding to the .messages() call whereas ideally it should have the id appended. This provokes an error when x.getAttachments().get() is called.

client
    .users(mailbox) //
    .mailFolders("Inbox");
    .messages() //
    .filter("isRead eq false") //
    .get() //
    .stream() //
    .flatMap(x -> x.getAttachments().get().stream())
    .forEach();

Fix in generator is either to append the id to context path at time that x.getAttachments is called OR ensure that each message object has the correct context path.

Filter output by use of filter() method

Hi

As part of a unit test I am trying to mock a call to filter(string clause). The effective code is below

String filterQuery = "startswith(displayName,'ab')";
expectedUsersPage = graphService.users().filter(filterQuery).get();

I end up getting the below error :
java.lang.RuntimeException: GET response not found for url=https://graph.microsoft.com/v1.0/users?$filter=startswith(displayName%2C'ab'), headers=[[name=OData-Version, value=4.0], [name=Accept, value=application/json;odata.metadata=minimal]]

By the looks of it, the filterQuery seems to have been URL encoded somewhere along the flow. The , has been replaced by %2C in the final URL. Not sure if this is expected but the same thing executes when not encoded on the MS graph explorer
https://developer.microsoft.com/en-us/graph/graph-explorer

I did try to debug this and could find the place where most probably the encoded filterQuery is encoded and appended to the base URL. I think its the below function of code -> public ContextPath addQueries(Map<String, String> queries) . But I haven't been able to figure where or how exactly the encoding is happening in the function.

Is this behavior expected ? In that case shall I be passing the filterQuery in some other way to the filter() method ? I tried putting backslashes before the colons in hope that it will act as an escape character but that didn't help. Can you please point to the problem area here?

OData v2 support

This looks like a great generator, I particularly like the type safety. But unfortunately the server I need to connect to is OData v2. How hard would it be to modify the generator to generate v2 compatible clients?

Create client for Graph v1.0 behind a proxy doesn't work

Hi David,
Good Morning, can you please look into the below issue?
We are trying to create GraphService object using proxy details but it gets failed and below is the stack trace.

Stack trace:
		at com.github.davidmoten.microsoft.authentication.ClientCredentialsAccessTokenProvider.refreshAccessToken(ClientCredentialsAccessTokenProvider.java:182) ~[odata-client-microsoft-client-builder-0.1.54.jar:na]
		at com.github.davidmoten.microsoft.authentication.ClientCredentialsAccessTokenProvider.get(ClientCredentialsAccessTokenProvider.java:114) ~[odata-client-microsoft-client-builder-0.1.54.jar:na]
		at com.github.davidmoten.microsoft.authentication.ClientCredentialsAccessTokenProvider.get(ClientCredentialsAccessTokenProvider.java:32) ~[odata-client-microsoft-client-builder-0.1.54.jar:na]
		at com.github.davidmoten.microsoft.authentication.BearerAuthenticator.authenticate(BearerAuthenticator.java:39) ~[odata-client-microsoft-client-builder-0.1.54.jar:na]
		at com.github.davidmoten.odata.client.internal.ApacheHttpClientHttpService.getResponse(ApacheHttpClientHttpService.java:108) ~[odata-client-runtime-0.1.54.jar:na]
		at com.github.davidmoten.odata.client.internal.ApacheHttpClientHttpService.get(ApacheHttpClientHttpService.java:62) ~[odata-client-runtime-0.1.54.jar:na]
		at com.github.davidmoten.odata.client.CollectionPageEntityRequest.get(CollectionPageEntityRequest.java:52) ~[odata-client-runtime-0.1.54.jar:na]
		at com.github.davidmoten.odata.client.CollectionEntityRequestOptionsBuilder.get(CollectionEntityRequestOptionsBuilder.java:178) ~[odata-client-runtime-0.1.54.jar:na]
		at com.mf.dp.edgeservice.o365.users.mails.MailsService.getUserMailFoldersService(MailsService.java:48) ~[classes/:na]
		at com.mf.dp.edgeservice.o365.users.mails.MailsHandler.getUserMailFoldersHandler(MailsHandler.java:50) ~[classes/:na]
		at org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:61) ~[spring-webflux-5.3.2.jar:5.3.2]
		at org.springframework.web.reactive.DispatcherHandler.invokeHandler(DispatcherHandler.java:161) ~[spring-webflux-5.3.2.jar:5.3.2]
		at org.springframework.web.reactive.DispatcherHandler.lambda$handle$1(DispatcherHandler.java:146) ~[spring-webflux-5.3.2.jar:5.3.2]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:177) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:428) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFromFluxOperator.subscribe(MonoFromFluxOperator.java:81) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:208) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:80) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:148) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:99) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:99) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoAny$AnySubscriber.onNext(MonoAny.java:116) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:421) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:686) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onNext(FluxFlattenIterable.java:250) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:94) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:270) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:313) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1766) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2346) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:110) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoCurrentContext.subscribe(MonoCurrentContext.java:36) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:487) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:420) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:210) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:270) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:228) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:370) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:173) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onComplete(FluxPeekFuseable.java:940) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:84) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2348) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2154) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2028) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4046) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoNext$NextSubscriber.onComplete(MonoNext.java:102) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:845) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:607) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:587) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.onComplete(FluxFlatMap.java:464) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onComplete(FluxPeekFuseable.java:277) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:292) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:228) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:370) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:107) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxMap$MapConditionalSubscriber.onComplete(FluxMap.java:269) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1785) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.signalCached(MonoCacheTime.java:328) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.onNext(MonoCacheTime.java:345) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:251) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.4.1.jar:3.4.1]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.4.1.jar:3.4.1]
		at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
		at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
		at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
		at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
		at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

Enable POST of lists of items to collections

As mentioned in #103, would be nice to be able to post lists rather than just single items:

client.companies().purchaseInvoices("123").purchaseInvoiceLines().post(list);

In fact it seems that some paths in Microsoft OData APIs (like Dynamics CRM) accept a post of a purchaseInvoice with multiple lines inline (discussed in #103).

Method call getPasswordCredentials of Application entity throws Null pointer exception

Hi David,

When I upgraded library to 0.1.20 version, I observed method call getPasswordCredentials of Application entity is failing. It is throwing null pointer exception.
In earlier version(0.1.19) it was working as expected by returning CollectionPage.

Sample code:
List passwordCredentialList = new ArrayList<>();
passwordCredentialList.add(PasswordCredential.builder().secretText("Application secret Text").build());

Application application = Application.builderApplication().passwordCredentials(passwordCredentialList).build();

//Throws Null Pointer Exception in 0.1.20 version of library
application.getPasswordCredentials();

Is it failing because of recent changes on handling null List by CollectionPage?

Issue with Microsoft Dynamics EntityDefinitions/RelationshipDefinitions

Hello,

i'm trying to generate Java classes from a metadata xml following the logic of the odata-client-microsoft-dynamics client but the process fails with the following error:

manyToManyRelationships() in microsoft.dynamics.crm.entity.set.EntityDefinitions cannot override manyToManyRelationships() in microsoft.dynamics.crm.entity.collection.request.EntityMetadataCollectionRequest

The XML definitions for RelationshipDefinitions and EntityDefinitions in my metadata file are similar with those of https://github.com/davidmoten/odata-client/blob/master/odata-client-microsoft-dynamics/src/main/odata/microsoft-dynamics-crm4-v9.1-metadata.xml:

<EntitySet Name="RelationshipDefinitions" EntityType="Microsoft.Dynamics.CRM.RelationshipMetadataBase" />

<EntitySet Name="EntityDefinitions" EntityType="Microsoft.Dynamics.CRM.EntityMetadata">
  <NavigationPropertyBinding Path="Microsoft.Dynamics.CRM.BooleanAttributeMetadata/GlobalOptionSet" Target="GlobalOptionSetDefinitions" />
  <NavigationPropertyBinding Path="ManyToManyRelationships" Target="RelationshipDefinitions" />
  <NavigationPropertyBinding Path="ManyToOneRelationships" Target="RelationshipDefinitions" />
  <NavigationPropertyBinding Path="OneToManyRelationships" Target="RelationshipDefinitions" />
  <NavigationPropertyBinding Path="Microsoft.Dynamics.CRM.EnumAttributeMetadata/GlobalOptionSet" Target="GlobalOptionSetDefinitions" />
</EntitySet>

but the code generated (e.g. for manyToManyRelationships()) is:

public final class EntityDefinitions extends EntityMetadataCollectionRequest {

    public RelationshipDefinitions manyToManyRelationships() {
        return new RelationshipDefinitions(contextPath.addSegment("ManyToManyRelationships"));
    }	
}

public class EntityMetadataCollectionRequest extends CollectionPageEntityRequest<EntityMetadata, EntityMetadataRequest> {
    
	public ManyToManyRelationshipMetadataCollectionRequest manyToManyRelationships() {
        return new ManyToManyRelationshipMetadataCollectionRequest(contextPath.addSegment("ManyToManyRelationships"), Optional.empty());
    }
}

Am i doing something wrong? If i comment out the EntityDefinitions entity set, the process completes successfully. Can this be considered a solution to my problem or it will result a non functional client? What is the purpose of the EntityDefinitions entity set?

Thank you.

Paging example

Hi David,

Thanks for this excellent library. I couldn't agree more to issues mentioned here

Can you share an example of how we can page data using odata client? Does it internally use skiptoken? Would it be possible to set the pagesize?

NPE when running odata-client-maven-plugin through Java 11

If you try and launch Maven using a Java 11 JDK, the Mojo fails with an NPE here:

It runs ok if you launch Maven from a Java 8 JDK.

I presume that this has something to do with the available of the javax.xml.bind packages under Java 11.

It would be nice if the Mojo could be adapted to avoid this error. Next best thing would be for the Mojo to be a bit more graceful about reacting to this condition.

httpResponse of odata.client.HttpResponse put(String s, .List<RequestHeader> list, InputStream inputStream, int i,HttpRequestOptions httpRequestOptions) does not give reponse body in httpResponse.getText() as post() method gives.

Hi David,
I tried to create upload session using below post method.
_com.github.davidmoten.odata.client.HttpService public com.github.davidmoten.odata.client.HttpResponse post(String url, java.util.List<com.github.davidmoten.odata.client.RequestHeader> requestHeaders, String content, com.github.davidmoten.odata.client.HttpRequestOptions options)
Maven: com.github.davidmoten:odata-client-runtime:0.1.59

I'm receiving response like below from httpResponse.getText() here:
"{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession\",\"expirationDateTime\":\"2021-07-22T17:25:42.05Z\",\"nextExpectedRanges\":[\"0-\"],\"uploadUrl\":\"https://xyz-my.sharepoint.com/personal/user_xyz_onmicrosoft_com/_api/v2.0/drives/{driveid}/items/{itemid}/uploadSession?guid='....'&overwrite=True&rename=False&dc=0&tempauth=......\"}"

And then, I'm trying to upload content using upload url received from above method. I'm using below put method to execute the uploadurl. But i receive { "responseCode": 202, "bytes": null, "text": null } in httpResponse.
com.github.davidmoten.odata.client.HttpService public abstract com.github.davidmoten.odata.client.HttpResponse put(String s,
java.util.List<com.github.davidmoten.odata.client.RequestHeader> list, java.io.InputStream inputStream, int i, com.github.davidmoten.odata.client.HttpRequestOptions httpRequestOptions)
Maven: com.github.davidmoten:odata-client-runtime:0.1.59

As per documentation, i would need to get response as below:
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"expirationDateTime": "2015-01-29T09:21:55.523Z",
"nextExpectedRanges": ["26-"]
}
OR
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "912310013A123",
"name": "largefile.vhd",
"size": 128,
"file": { }
}

Can you please check on why httpResponse.getText() is null in the response. Also please suggest if any alternative, as i would need the actual reponse text.

Thanks,
Pavani.

Dynamics CRM metadata broken due to missing EntitySet owners

As mentioned in #51 this is not a problem with odata-client but rather a problem with Dynamics published metadata in that generating code fails due to the missing EntitySet owners. This link suggests that the owners EntitySet path is for internal (to Microsoft) use. In that case it seems like the appropriate action is to add an empty EntitySet owners to the metadata.

Issue with the /delta query

Hi David,
Can you please help us here, we are getting 'user' information successfully via 'users().stream();' but when we use delta, to get incremental data its reporting exception.

Request:

                     users()
                    .delta()
                    .stream();

Response: Exception

{
    "timestamp": "2020-08-13T09:35:43.017+00:00",
    "path": "/v1/users",
    "status": "METHOD_NOT_ALLOWED",
    "error": "Method Not Allowed",
    "message": "responseCode=405 from url=https://graph.microsoft.com/v1.0/users/delta, expectedResponseCode in [200, 200], message=\n{\r\n  \"error\": {\r\n    \"code\": \"Request_BadRequest\",\r\n    \"message\": \"Specified HTTP method is not allowed for the request target.\",\r\n    \"innerError\": {\r\n      \"date\": \"2020-08-13T09:35:42\",\r\n      \"request-id\": \"23b657e4-634a-4679-b95f-b98486d2d971\"\r\n    }\r\n  }\r\n}",
    "requestId": "952a8c7e-1",
    "statusCode": 405
}

ODATA: How to get list of users using '@odata.nextLink'

Hi David,
We appreciate that odata client library is a powerful tool to get data from exchange server. Currently in our use case the challenge is to get the list of users, given the value of '@odata.nextLink'.

As an example below MS Graph Java SDK code can be used to get list of users based on '@odata.nextLink'.

IUserCollectionRequestBuilder userCollReqBuilder = new UserCollectionRequestBuilder(@odata.nextLink, graphClient, null);
if (userCollReqBuilder != null) {
    IUserCollectionPage response = userCollReqBuilder.buildRequest().get();
    List<User> users = response.getCurrentPage();
}

It will be great if you can help us to get user list using '@odata.nextLink'.

Thanks
Madan

How to get partial range of content of a drive item(file)

Hi David,
We have implemented code to get item content as per you suggestion earlier in issue #128.
Optional sp = graphService
.drives(driveId)
.items(itemId)
.get()
.getContent();

Now i'm looking to get partial range of contents. I need to give Range header, to same get content api. Can you please help with how to give range headers to it.

Thanks,
Pavani

Retrieve events with calendarView

Hi David,
As per MS Graph API, startdatetime & enddatetime are required parameters for 'calendarView' API. Can you please suggest how to pass startdatetime & enddatetime parameters in the below 'calendarView' request?

graphServiceObject()
.users(mailId)
.calendars(calendarId)
.calendarView()
.delta()
.stream();

Listing users and sub-groups inside a group separately

Hi David,

Thanks a lot for the Odata-client library and I appreciate that odata client library is a very powerful tool.
I had use cases where I wanted to list all the users in a group and sub-group inside a group separately(On going through Graph documentation, I could see that I can create only Security group inside security group).

I have gone through Odata library and saw that I can use members() for group but it is listing groups as well as users also.

I can differentiate users and sub-groups inside a group by @odata.type property.
For group, it would be "@odata.type": "#microsoft.graph.group",
For user, it would be "@odata.type": "#microsoft.graph.user"

I tried to filter by this property but I think it is an limitation from Graph API that we cannot filter using @odata.type property.
Can you please help me with how I can differentiate the members in a group by @odata.type property using Odata-client

Error code missing for incorrect application credential

Hi David,
Good Evening.... Can you please take a look on the below case?
We are using 'MsGraph' to get Azure authentication token and expecting 401 'Error: Unauthorized' for incorrect application credential but error code is missing in the response.

Error when application credential are incorrect:
exception = {ClientException@10436} "com.github.davidmoten.odata.client.ClientException: Unable to authenticate request"
statusCode = {Optional@10730} "Optional.empty"
value = null
backtrace = {Object[5]@10731}
0 = {short[32]@10883}
1 = {int[32]@10884}
2 = {Object[32]@10885}
3 = {long[32]@10886}
4 = {Object[5]@10887}
detailMessage = "Unable to authenticate request"
value = {byte[30]@10888}
coder = 0
hash = 0

Calendar Event Attributes

Hi David,
Can you please help us on the below request?
transactionId is one of the attribute in a calendar event, currently this attribute is not available in Event.class(odata.msgraph.client.entity). In our use case this attribute is required, is it possible to include transactionId attribute?

Below are the event attributes from MS Graph API.

{
	"@odata.type": "#microsoft.graph.event",
	"@odata.etag": "W/\"21tPFP6RokilGqK6dhLjjwAAWGHb1w==\"",
	"createdDateTime": "2020-09-23T17:31:22.3683256Z",
	"transactionId": "c8bd03c3-2b4b-214e-5a23-82b132a73e41",
	"originalStartTimeZone": "India Standard Time",
         ..............
         <other attributes>
}

Support entities without keys

At the moment odata-client ignores with a warning or fails (configurable) if it encounters an Entity without a Key (inherited or otherwise). In fact this is sometimes legal as per spec:

An entity is uniquely identified within an entity set by its key. A key MAY be specified if the entity type does not specify a base type that already has a key declared.

In order to be specified as the type of an entity set or a collection-valued containment navigation property, the entity type MUST either specify a key or inherit its key from its base type.

In OData 4.01 responses entity types used for singletons or single-valued navigation properties do not require a key. In OData 4.0 responses entity types used for singletons or single-valued navigation properties MUST have a key defined.

An example of this is from Dynamics CRM v9.1 metadata:

<EntityType Name="expando" BaseType="mscrm.crmbaseentity" OpenType="true"/>

Neither expando nor crmbaseentity have a key but this is probably legal as there is no reference to expando anywhere else in the document (of course this prompts a question as to its usefulness).

This appears to be very unusual in OData services so this can be considered a low priority task.

[Documentation] Authentication with OData Service

Hi

I've found this library while needing to connect to an OData API provided by another team in my company (internal API). I've managed to generate the java classes for the $metadata, but I can't seem to figure out how to connect to the API itself.

I can't seem to figure out how to specify the URL for connecting to the API and how to make it so that I can pass a JWT Token in the Authorization: Bearer header.

I've found that I can do this... but I'm not sure about that "test()" method call, I've seen it in tests inside this repository, so it doesn't seem right.

        Container.test()
            .baseUrl("MY_CUSTOM_URL")
            .build();

And it fails anyway, since I can't really provide my access tokens.

I've read through the documentation, source code and tests to try and figure out, but I can seem only to find examples on how to do stuff like this for connecting the MSGraph (or other MS Libraries)...

Am I trying to do something this library is not meant to do?

I'm happy to make a PR with documentation to help out other users that want to make the same as I do.

Nulls in response

Hi David,

In the older version of library (v0.1.12), the response from Odata client did not contain nulls, which MS Graph SDK notoriously did. It was something like this for User endpoint -

{
  "businessPhones": [],
  "displayName": "John Martin",
  "givenName": "John",
  "surname": "Martin",
  "userPrincipalName": "[email protected]",
  "id": "8fce020a-ff47-45b1-a533-d2cf7e954ebd"
}

But with v0.1.17, I see this -

{
  "@odata.type": null,
  "accountEnabled": null,
  "ageGroup": null,
  "assignedLicenses": null,
  "assignedPlans": null,
  "businessPhones": [],
  "city": null,
  "companyName": null,
  "consentProvidedForMinor": null,
  "country": null,
  "creationType": null,
  "department": null,
  "displayName": "John Martin",
  "employeeId": null,
  "externalUserState": null,
  "externalUserStateChangeDateTime": null,
  "faxNumber": null,
  "givenName": "John",
  "identities": null,
  "imAddresses": null,
  "isResourceAccount": null,
  "jobTitle": null,
  "lastPasswordChangeDateTime": null,
  "legalAgeGroupClassification": null,
  "licenseAssignmentStates": null,
  "mail": null,
  "mailNickname": null,
  "mobilePhone": null,
  "onPremisesDistinguishedName": null,
  "onPremisesExtensionAttributes": null,
  "onPremisesImmutableId": null,
  "onPremisesLastSyncDateTime": null,
  "onPremisesProvisioningErrors": null,
  "onPremisesSecurityIdentifier": null,
  "onPremisesSyncEnabled": null,
  "onPremisesDomainName": null,
  "onPremisesSamAccountName": null,
  "onPremisesUserPrincipalName": null,
  "otherMails": null,
  "passwordPolicies": null,
  "passwordProfile": null,
  "officeLocation": null,
  "postalCode": null,
  "preferredLanguage": null,
  "provisionedPlans": null,
  "proxyAddresses": null,
  "showInAddressList": null,
  "signInSessionsValidFromDateTime": null,
  "state": null,
  "streetAddress": null,
  "surname": "Martin",
  "usageLocation": null,
  "userPrincipalName": "[email protected]",
  "userType": null,
  "mailboxSettings": null,
  "deviceEnrollmentLimit": null,
  "aboutMe": null,
  "birthday": null,
  "hireDate": null,
  "interests": null,
  "mySite": null,
  "pastProjects": null,
  "preferredName": null,
  "responsibilities": null,
  "schools": null,
  "skills": null,
  "id": "8fce020a-ff47-45b1-a533-d2cf7e954ebd",
  "deletedDateTime": null,
  "assignedLicenses@nextLink": null,
  "assignedPlans@nextLink": null,
  "businessPhones@nextLink": null,
  "identities@nextLink": null,
  "imAddresses@nextLink": null,
  "licenseAssignmentStates@nextLink": null,
  "onPremisesProvisioningErrors@nextLink": null,
  "otherMails@nextLink": null,
  "provisionedPlans@nextLink": null,
  "proxyAddresses@nextLink": null,
  "interests@nextLink": null,
  "pastProjects@nextLink": null,
  "responsibilities@nextLink": null,
  "schools@nextLink": null,
  "skills@nextLink": null
}

Is this a defect or was this design change intentional?

usage of postStringReturnsString() returns "Method Not Allowed"

Hello David

Hope you are doing fine.

As part of my project, I wrote an api that is using the _custom() method to execute a request( I am trying to acheive json batching here). The concerned piece of code is as below

.map(graphService -> graphService ._custom() .postStringReturnsString(url, batchContent, RequestOptions.EMPTY, RequestHeader.contentType(MediaType.APPLICATION_JSON_VALUE))));

This was working with version 0.1.40 of the odata client and was giving the expected response but after bumping the version to 0.1.41, I am getting error response with status as METHOD_NOT_ALLOWED. I have tested the scenario with version 0.1.40 and there hasn't been any change in my part of the code since then.

The complete response looks as below
{ "timestamp": "2020-10-08T13:12:30.453+00:00", "path": "/api/o365/v1/users/mail-folders/batch", "status": "METHOD_NOT_ALLOWED", "error": "Method Not Allowed", "message": "responseCode=405 from url=https://graph.microsoft.com/v1.0/$batch, expectedResponseCode in [200, 299], message=\n{\r\n \"error\": {\r\n \"code\": \"UnknownError\",\r\n \"message\": \"Method not allowed\",\r\n \"innerError\": {\r\n \"date\": \"2020-10-08T13:12:30\",\r\n \"request-id\": \"476267ba-8baf-48cb-ad5c-823154a52922\",\r\n \"client-request-id\": \"476267ba-8baf-48cb-ad5c-823154a52922\"\r\n }\r\n }\r\n}", "requestId": "804939a3-1", "statusCode": 405 }

Could you please advise if there has been change from version 0.1.41 on-wards and if I need to use some other method to achieve this now?

Regards
Abhishek

Expanding item attachments in attachment list

Hi David,

I am trying to get the list of attachments in an email and get the details of each attachment with the $expand=microsoft.graph.itemattachment/item option to expand item details if present. However, the item details go under unmappedFields hash map and are not visible in the JSON response. I am not sure how to map it to an object as the type of item could vary. Could you please help me out with an example of how to include expanded item details as part of the response JSON?

Attachment attachmentDetails = OdataClientFactory
                    .request()
                    .users(emailId)
                    .mailFolders(folderId)
                    .messages(messageId)
                    .attachments(attachmentId)
                    .expand("microsoft.graph.itemattachment/item")
                    .get();

Support cross-schema aliases

The msgraph v1.0 metadata as of 30/05/2020 includes an extra callRecords schema (previously there was only one). The current generator doesn't support references in the callRecords schema to the graph schema using the graph alias. Let's add that support.

API to renew client secret succeed but response code mismatch

Hi David,
Can you please take a look?
When we create a new client secret using odata library, it gets successful with response code 200. But odata API shows failure as it expects response code 201.

graphReq
    .applications(objId)
    .addPassword(PasswordCredential.builder()
        .displayName(name)
        .endDateTime(date)
        .build())
    .get();

DriveItem does not have the @microsoft.graph.downloadUrl property

Hi David,
I'm trying to get @microsoft.graph.downloadUrl from drive item retrieved as shown below. But the driveItem does not have the downloadurl property. How can i get the download url.
DriveItem item = graphService
.drives(driveId)
.items(itemId)
.select("@microsoft.graph.downloadUrl")
.get();

Thanks,
Pavani

com.github.davidmoten.odata.client.internal not strictly internal

As discussed in reviews of #6, public references need to be removed from the internal package com.github.davidmoten.odata.client.internal.

Export com.github.davidmoten.odata.client, has 1, private references [com.github.davidmoten.odata.client.internal]

There are public references to com.github.davidmoten.odata.client.internal.TypedObject in at least one class (e.g. the constructor for ActionFunctionRequestBase)

Until all public references are removed, the internal packages need to be exported or the module will fail to resolve when loaded into an OSGI runtime.

POST call not working for MS batch api

Hello David

I believe this is perhaps related to issue #64 .
While trying to execute an unit test that targets https://graph.microsoft.com/v1.0/$batch on MS side via _custom() method, I am getting a POST request not expected error.

Concerned code that I am trying to execute:

graphService = com.mf.dp.edgeservice.o365.utilities.GraphServiceMock. CreateGraphServiceMockObject("/$batch", getUnitTestFileValidUserMailFoldersByBatch, RequestHeader.ODATA_VERSION, RequestHeader.ACCEPT_JSON_METADATA_MINIMAL);

var mailFoldersList = graphService._custom().postStringReturnsString(ConfigurationService.getMSGraphBatchRequestURI(), mockedBatchContent, RequestOptions.EMPTY, RequestHeader.contentType(MediaType.APPLICATION_JSON_VALUE));

mockedBatchContent has a valid string already populated in it. The same thing works when tried through an actual graph service object( not a mock one).

Error call stack:

Calling:
POST
https://graph.microsoft.com/v1.0/$batch
Content-Type=application/json

java.lang.RuntimeException: POST request not expected for url=https://graph.microsoft.com/v1.0/$batch, headers=[[name=Content-Type, value=application/json]]

at com.github.davidmoten.odata.client.TestingService$BuilderBase$1.post(TestingService.java:295)
at com.github.davidmoten.odata.client.HttpService.submitWithContent(HttpService.java:89)
at com.github.davidmoten.odata.client.HttpService.submitWithContent(HttpService.java:100)
at com.github.davidmoten.odata.client.CustomRequest.submitStringReturnsString(CustomRequest.java:126)
at com.github.davidmoten.odata.client.CustomRequest.postStringReturnsString(CustomRequest.java:92)

From what I could understand, the POST call is not being recognized in the TestingService class for the concerned end point.
Could you please check this ?

Regards
Abhishek

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.