From 8a0fa55c3d7c9389a04385dbc19db91d25dfceb7 Mon Sep 17 00:00:00 2001 From: Ajmal Kottilingal <90693406+ajmalab@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:06:22 +0300 Subject: [PATCH 01/13] Add repository_dispatch endpoint (#186) * add repositry_dispatch endpoint Signed-off-by: Ajmal Kottilingal * remove unused import Signed-off-by: Ajmal Kottilingal * remove unused import Signed-off-by: Ajmal Kottilingal * change order of owner and repo in path Signed-off-by: Ajmal Kottilingal * make param final Signed-off-by: Ajmal Kottilingal * add header Signed-off-by: Ajmal Kottilingal * update comment Signed-off-by: Ajmal Kottilingal * change access of method Signed-off-by: Ajmal Kottilingal * remove nullable annotation Signed-off-by: Ajmal Kottilingal * remove nullable import Signed-off-by: Ajmal Kottilingal * add test Signed-off-by: Ajmal Kottilingal * remove unused import Signed-off-by: Ajmal Kottilingal --------- Signed-off-by: Ajmal Kottilingal --- .../github/v3/clients/RepositoryClient.java | 14 ++++++ .../v3/repos/requests/RepositoryDispatch.java | 44 +++++++++++++++++++ .../v3/clients/RepositoryClientTest.java | 25 +++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/main/java/com/spotify/github/v3/repos/requests/RepositoryDispatch.java diff --git a/src/main/java/com/spotify/github/v3/clients/RepositoryClient.java b/src/main/java/com/spotify/github/v3/clients/RepositoryClient.java index 97fd5bc1..c2c49c04 100644 --- a/src/main/java/com/spotify/github/v3/clients/RepositoryClient.java +++ b/src/main/java/com/spotify/github/v3/clients/RepositoryClient.java @@ -85,6 +85,7 @@ public class RepositoryClient { private static final String BRANCH_TEMPLATE = "/repos/%s/%s/branches/%s"; private static final String LIST_BRANCHES_TEMPLATE = "/repos/%s/%s/branches"; private static final String CREATE_COMMENT_TEMPLATE = "/repos/%s/%s/commits/%s/comments"; + private static final String CREATE_REPOSITORY_DISPATCH_EVENT_TEMPLATE = "/repos/%s/%s/dispatches"; private static final String COMMENT_TEMPLATE = "/repos/%s/%s/comments/%s"; private static final String LANGUAGES_TEMPLATE = "/repos/%s/%s/languages"; private static final String MERGE_TEMPLATE = "/repos/%s/%s/merges"; @@ -698,4 +699,17 @@ private String getContentPath(final String path, final String query) { } return String.format(CONTENTS_URI_TEMPLATE, owner, repo, path, query); } + + /** + * Create a repository_dispatch event. + * + * @param request The repository dispatch request. + */ + + public CompletableFuture createRepositoryDispatchEvent(final RepositoryDispatch request) { + final String path = String.format(CREATE_REPOSITORY_DISPATCH_EVENT_TEMPLATE, owner, repo); + return github + .post(path, github.json().toJsonUnchecked(request)) + .thenApply(response -> response.code() == NO_CONTENT); //should always return a 204 + } } diff --git a/src/main/java/com/spotify/github/v3/repos/requests/RepositoryDispatch.java b/src/main/java/com/spotify/github/v3/repos/requests/RepositoryDispatch.java new file mode 100644 index 00000000..5c7c79e1 --- /dev/null +++ b/src/main/java/com/spotify/github/v3/repos/requests/RepositoryDispatch.java @@ -0,0 +1,44 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2023 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.github.v3.repos.requests; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.spotify.github.GithubStyle; +import java.util.Optional; +import org.immutables.value.Value; + +@Value.Immutable +@GithubStyle +@JsonSerialize(as = ImmutableRepositoryDispatch.class) +@JsonDeserialize(as = ImmutableRepositoryDispatch.class) +public interface RepositoryDispatch { + + /** The custom webhook event name */ + + String eventType(); + + /** JSON payload with extra information about the webhook event + * that your action or workflow may use. */ + Optional clientPayload(); + +} diff --git a/src/test/java/com/spotify/github/v3/clients/RepositoryClientTest.java b/src/test/java/com/spotify/github/v3/clients/RepositoryClientTest.java index 3f1e8e56..bd8b7ace 100644 --- a/src/test/java/com/spotify/github/v3/clients/RepositoryClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/RepositoryClientTest.java @@ -42,6 +42,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.io.Resources; @@ -71,6 +73,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import okhttp3.MediaType; import okhttp3.Protocol; @@ -714,4 +717,26 @@ public void shouldReturnEmptyOptionalWhenResponseBodyNotPresent() throws Excepti Optional response = repoClient.downloadZipball("master").get(); assertThat(response, is(Optional.empty())); } + + @Test + public void shouldReturnEmptyResponseWhenRepositoryDispatchEndpointTriggered() throws Exception { + final Response response = mock(Response.class); + when(response.code()).thenReturn(204); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode clientPayload = mapper.createObjectNode(); + clientPayload.put("my-custom-true-property","true"); + clientPayload.put("my-custom-false-property", "false"); + + RepositoryDispatch repositoryDispatchRequest = ImmutableRepositoryDispatch.builder() + .eventType("my-custom-event") + .clientPayload(clientPayload) + .build(); + + when(github.post("/repos/someowner/somerepo/dispatches", json.toJsonUnchecked(repositoryDispatchRequest))).thenReturn(completedFuture(response)); + + boolean repoDispatchResult = repoClient.createRepositoryDispatchEvent(repositoryDispatchRequest).get(); + assertTrue(repoDispatchResult); + } + } From f65195dcee64a039d8f29c46f85c591340e212fa Mon Sep 17 00:00:00 2001 From: Ellie Kelsch <37074698+ebk45@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:18:18 +0100 Subject: [PATCH 02/13] feat: update setup-java action version (#190) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f07f445..7544666b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: 11 distribution: corretto From a5c095aae6e7049b48f83a937dee086382bb8030 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 14 Jun 2024 07:06:01 +0000 Subject: [PATCH 03/13] [maven-release-plugin] prepare release v0.2.18 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 477b73cf..8e80ddf9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.18-SNAPSHOT + 0.2.18 com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - HEAD + v0.2.18 @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1710939443 + 1718348716 spotbugsexclude.xml error checkstyle.xml From a8848f55184b9128c1141b4cee2cd0dd651ef17f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 14 Jun 2024 07:06:03 +0000 Subject: [PATCH 04/13] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 8e80ddf9..caf9ba35 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.18 + 0.2.19-SNAPSHOT com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - v0.2.18 + HEAD @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718348716 + 1718348763 spotbugsexclude.xml error checkstyle.xml From 392b5059ece0280f02e1fe33901ced19e6c23656 Mon Sep 17 00:00:00 2001 From: Annelle de Jager <137896331+annelled@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:53:45 +0100 Subject: [PATCH 05/13] Add support to get a user installation for the authenticated app (#191) * Fix merge with master conflicts * Add support to get a user installation for the authenticated app * Revert tag changes in pom after master merge * Create the github app client from the user client and add tests --- .../github/v3/clients/GitHubClient.java | 9 ++++-- .../github/v3/clients/GithubAppClient.java | 10 ++++++ .../spotify/github/v3/clients/UserClient.java | 12 +++++-- .../github/v3/clients/UserClientTest.java | 31 +++++++++++++++++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java index f32dccdd..81a72bf9 100644 --- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java @@ -431,8 +431,13 @@ public OrganisationClient createOrganisationClient(final String org) { return OrganisationClient.create(this, org); } - public UserClient createUserClient() { - return UserClient.create(this); + /** + * Create user API client + * + * @return user API client + */ + public UserClient createUserClient(final String owner) { + return UserClient.create(this, owner); } Json json() { diff --git a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java index 9198fc60..02845801 100644 --- a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java @@ -45,6 +45,7 @@ public class GithubAppClient { refer to the organisation in the installation endpoint */ private static final String GET_INSTALLATION_ORG_URL = "/orgs/%s/installation"; + private static final String GET_INSTALLATION_USER_URL = "/users/%s/installation"; private final GitHubClient github; private final String owner; @@ -114,6 +115,15 @@ private CompletableFuture getOrgInstallation() { String.format(GET_INSTALLATION_ORG_URL, owner), Installation.class); } + /** + * Get an installation of a user + * @return an Installation + */ + public CompletableFuture getUserInstallation() { + return github.request( + String.format(GET_INSTALLATION_USER_URL, owner), Installation.class); + } + /** * Authenticates as an installation * diff --git a/src/main/java/com/spotify/github/v3/clients/UserClient.java b/src/main/java/com/spotify/github/v3/clients/UserClient.java index 3ea24979..0d48e027 100644 --- a/src/main/java/com/spotify/github/v3/clients/UserClient.java +++ b/src/main/java/com/spotify/github/v3/clients/UserClient.java @@ -27,15 +27,21 @@ public class UserClient { public static final int NO_CONTENT = 204; private final GitHubClient github; + private final String owner; private static final String SUSPEND_USER_TEMPLATE = "/users/%s/suspended"; - UserClient(final GitHubClient github) { + UserClient(final GitHubClient github, final String owner) { this.github = github; + this.owner = owner; } - static UserClient create(final GitHubClient github) { - return new UserClient(github); + static UserClient create(final GitHubClient github, final String owner) { + return new UserClient(github, owner); + } + + public GithubAppClient createGithubAppClient() { + return new GithubAppClient(this.github, this.owner); } /** diff --git a/src/test/java/com/spotify/github/v3/clients/UserClientTest.java b/src/test/java/com/spotify/github/v3/clients/UserClientTest.java index 0211769b..ce8e7259 100644 --- a/src/test/java/com/spotify/github/v3/clients/UserClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/UserClientTest.java @@ -19,16 +19,25 @@ */ package com.spotify.github.v3.clients; +import static com.google.common.io.Resources.getResource; +import static java.nio.charset.Charset.defaultCharset; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.common.io.Resources; import com.spotify.github.jackson.Json; +import com.spotify.github.v3.checks.Installation; import com.spotify.github.v3.user.requests.ImmutableSuspensionReason; + +import java.io.IOException; import java.util.concurrent.CompletableFuture; + import okhttp3.Response; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,12 +46,17 @@ public class UserClientTest { private GitHubClient github; private UserClient userClient; + private String owner = "github"; + private Json json; + private static String getFixture(String resource) throws IOException { + return Resources.toString(getResource(TeamClientTest.class, resource), defaultCharset()); + } @BeforeEach public void setUp() { github = mock(GitHubClient.class); - userClient = new UserClient(github); - Json json = Json.create(); + userClient = new UserClient(github, owner); + json = Json.create(); when(github.json()).thenReturn(json); } @@ -81,4 +95,17 @@ public void testUnSuspendUserFailure() throws Exception { final CompletableFuture result = userClient.unSuspendUser("username", ImmutableSuspensionReason.builder().reason("That's why").build()); assertFalse(result.get()); } + + @Test + public void testAppClient() throws Exception { + final GithubAppClient githubAppClient = userClient.createGithubAppClient(); + final CompletableFuture fixture = + completedFuture(json.fromJson(getFixture("../githubapp/installation.json"), Installation.class)); + when(github.request("/users/github/installation", Installation.class)).thenReturn(fixture); + + final Installation installation = githubAppClient.getUserInstallation().get(); + + assertThat(installation.id(), is(1)); + assertThat(installation.account().login(), is("github")); + } } From cbe009a49207a9b6eba4aabd57738a5a8fa114d7 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 14 Jun 2024 12:57:41 +0000 Subject: [PATCH 06/13] [maven-release-plugin] prepare release v0.2.19 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index caf9ba35..1c83c31d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.19-SNAPSHOT + 0.2.19 com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - HEAD + v0.2.19 @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718348763 + 1718369817 spotbugsexclude.xml error checkstyle.xml From d539b0966e366a40fa2b09767cdc45f84f1d86c8 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 14 Jun 2024 12:57:43 +0000 Subject: [PATCH 07/13] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1c83c31d..600a61fd 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.19 + 0.2.20-SNAPSHOT com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - v0.2.19 + HEAD @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718369817 + 1718369863 spotbugsexclude.xml error checkstyle.xml From eff291075c097757895040a010182a8550895b97 Mon Sep 17 00:00:00 2001 From: Ellie Kelsch <37074698+ebk45@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:21:58 +0100 Subject: [PATCH 08/13] fix: make one line change to trigger release (#194) --- .../java/com/spotify/github/v3/clients/GithubAppClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java index 02845801..e9150d96 100644 --- a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java @@ -79,7 +79,7 @@ public CompletableFuture> getInstallations() { } /** - * Get Installation + * Get Installation of repo or org * * @return an Installation */ From fc276af566617689a746b3c1b3d4aa7c420f230a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 19 Jun 2024 11:30:42 +0000 Subject: [PATCH 09/13] [maven-release-plugin] prepare release v0.3.0 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 600a61fd..f482f022 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.20-SNAPSHOT + 0.2.20 com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - HEAD + v0.3.0 @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718369863 + 1718796604 spotbugsexclude.xml error checkstyle.xml From 55ad5a363401745b8a4a0e39f6d4f52f57b652cd Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 19 Jun 2024 11:30:44 +0000 Subject: [PATCH 10/13] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f482f022..db8b9275 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.2.20 + 0.3.1-SNAPSHOT com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - v0.3.0 + HEAD @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718796604 + 1718796644 spotbugsexclude.xml error checkstyle.xml From 8a9f799cd67a233cd0cbd32fca610e4d5aec7d49 Mon Sep 17 00:00:00 2001 From: Mitchell Hentges <110673802+mitchhentgesspotify@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:38:10 +0200 Subject: [PATCH 11/13] Store `installationTokens` in a `ConcurrentHashMap` (#195) Hopefully this is sufficient to make `GitHubClient` thread-safe. No other usages of `HashMap` or `ArrayList` were found, except for `Languages` which is a return structure, so is less necessary to be thread-safe. --- src/main/java/com/spotify/github/v3/clients/GitHubClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java index 81a72bf9..8f41df5e 100644 --- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java @@ -48,12 +48,12 @@ import java.lang.invoke.MethodHandles; import java.net.URI; import java.time.ZonedDateTime; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import javax.ws.rs.core.HttpHeaders; @@ -145,7 +145,7 @@ private GitHubClient( this.privateKey = privateKey; this.appId = appId; this.installationId = installationId; - this.installationTokens = new HashMap<>(); + this.installationTokens = new ConcurrentHashMap<>(); } /** From 223020168312ba1c3acf5c612431058750026484 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 10 Jul 2024 13:43:21 +0000 Subject: [PATCH 12/13] [maven-release-plugin] prepare release v0.3.1 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index db8b9275..591edb43 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.3.1-SNAPSHOT + 0.3.1 com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - HEAD + v0.3.1 @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1718796644 + 1720618953 spotbugsexclude.xml error checkstyle.xml From 7cf19e51121f3d4b7aee3a760422c69cda86e02b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 10 Jul 2024 13:43:23 +0000 Subject: [PATCH 13/13] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 591edb43..9c89bcd9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.3.1 + 0.3.2-SNAPSHOT com.spotify @@ -23,7 +23,7 @@ scm:git:https://github.com/spotify/github-java-client.git scm:git:git@github.com:spotify/github-java-client.git scm:https://github.com/spotify/github-java-client/ - v0.3.1 + HEAD @@ -84,7 +84,7 @@ UTF-8 UTF-8 - 1720618953 + 1720619003 spotbugsexclude.xml error checkstyle.xml