[csharp-netcore] fix OAuth2 integration when using httpclient library (#12954)

* Update RestSharp to v108

* Add OAuth2 Application (client_credentials) authentication

* Run generators and fix typos

* Undo accidental python and rust changes

* Add documentation, fix authenticator bug, and fix user agent bug

* Fix tests

Missed some changes in the mustache templates.
Also had to update the `netcoreapp2.0` test project to `netcoreapp3.1` for compatibility with RestSharp

* Switch HttpUtility to WebUtility for compatibility

* skip oauth file generation for httpclient

* fix templates

* remove files, regenerate samples

* add reference to system.web

Co-authored-by: Jared Bates <Jared.Bates@sight-sound.com>
This commit is contained in:
William Cheng 2022-07-20 20:30:56 +08:00 committed by GitHub
parent 2dcc319e13
commit 05f4792df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 93 additions and 212 deletions

View File

@ -785,17 +785,23 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
if (HTTPCLIENT.equals(getLibrary())) { if (HTTPCLIENT.equals(getLibrary())) {
supportingFiles.add(new SupportingFile("FileParameter.mustache", clientPackageDir, "FileParameter.cs")); supportingFiles.add(new SupportingFile("FileParameter.mustache", clientPackageDir, "FileParameter.cs"));
typeMapping.put("file", "FileParameter"); typeMapping.put("file", "FileParameter");
addRestSharpSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir);
additionalProperties.put("apiDocPath", apiDocPath); additionalProperties.put("apiDocPath", apiDocPath);
additionalProperties.put("modelDocPath", modelDocPath); additionalProperties.put("modelDocPath", modelDocPath);
} else if (GENERICHOST.equals(getLibrary())) { } else if (GENERICHOST.equals(getLibrary())) {
addGenericHostSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir); addGenericHostSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir);
additionalProperties.put("apiDocPath", apiDocPath + File.separatorChar + "apis"); additionalProperties.put("apiDocPath", apiDocPath + File.separatorChar + "apis");
additionalProperties.put("modelDocPath", modelDocPath + File.separatorChar + "models"); additionalProperties.put("modelDocPath", modelDocPath + File.separatorChar + "models");
} else { } else { //restsharp
addRestSharpSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir);
additionalProperties.put("apiDocPath", apiDocPath); additionalProperties.put("apiDocPath", apiDocPath);
additionalProperties.put("modelDocPath", modelDocPath); additionalProperties.put("modelDocPath", modelDocPath);
if (ProcessUtils.hasOAuthMethods(openAPI)) {
supportingFiles.add(new SupportingFile("auth/OAuthAuthenticator.mustache", authPackageDir, "OAuthAuthenticator.cs"));
supportingFiles.add(new SupportingFile("auth/TokenResponse.mustache", authPackageDir, "TokenResponse.cs"));
supportingFiles.add(new SupportingFile("auth/OAuthFlow.mustache", authPackageDir, "OAuthFlow.cs"));
}
} }
addTestInstructions(); addTestInstructions();
@ -851,8 +857,8 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
} }
} }
public void addRestSharpSupportingFiles(final String clientPackageDir, final String packageFolder, public void addSupportingFiles(final String clientPackageDir, final String packageFolder,
final AtomicReference<Boolean> excludeTests, final String testPackageFolder, final String testPackageName, final String modelPackageDir, final String authPackageDir) { final AtomicReference<Boolean> excludeTests, final String testPackageFolder, final String testPackageName, final String modelPackageDir, final String authPackageDir) {
supportingFiles.add(new SupportingFile("IApiAccessor.mustache", clientPackageDir, "IApiAccessor.cs")); supportingFiles.add(new SupportingFile("IApiAccessor.mustache", clientPackageDir, "IApiAccessor.cs"));
supportingFiles.add(new SupportingFile("Configuration.mustache", clientPackageDir, "Configuration.cs")); supportingFiles.add(new SupportingFile("Configuration.mustache", clientPackageDir, "Configuration.cs"));
supportingFiles.add(new SupportingFile("ApiClient.mustache", clientPackageDir, "ApiClient.cs")); supportingFiles.add(new SupportingFile("ApiClient.mustache", clientPackageDir, "ApiClient.cs"));
@ -899,12 +905,6 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
supportingFiles.add(new SupportingFile("appveyor.mustache", "", "appveyor.yml")); supportingFiles.add(new SupportingFile("appveyor.mustache", "", "appveyor.yml"));
supportingFiles.add(new SupportingFile("AbstractOpenAPISchema.mustache", modelPackageDir, "AbstractOpenAPISchema.cs")); supportingFiles.add(new SupportingFile("AbstractOpenAPISchema.mustache", modelPackageDir, "AbstractOpenAPISchema.cs"));
if (ProcessUtils.hasOAuthMethods(openAPI)) {
supportingFiles.add(new SupportingFile("auth/OAuthAuthenticator.mustache", authPackageDir, "OAuthAuthenticator.cs"));
supportingFiles.add(new SupportingFile("auth/TokenResponse.mustache", authPackageDir, "TokenResponse.cs"));
supportingFiles.add(new SupportingFile("auth/OAuthFlow.mustache", authPackageDir, "OAuthFlow.cs"));
}
} }
public void addGenericHostSupportingFiles(final String clientPackageDir, final String packageFolder, public void addGenericHostSupportingFiles(final String clientPackageDir, final String packageFolder,

View File

@ -12,8 +12,10 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Net.Http; using System.Net.Http;
{{#useRestSharp}}
{{#hasOAuthMethods}}using {{packageName}}.Client.Auth; {{#hasOAuthMethods}}using {{packageName}}.Client.Auth;
{{/hasOAuthMethods}} {{/hasOAuthMethods}}
{{/useRestSharp}}
namespace {{packageName}}.Client namespace {{packageName}}.Client
{ {
@ -331,6 +333,7 @@ namespace {{packageName}}.Client
/// <value>The access token.</value> /// <value>The access token.</value>
public virtual string AccessToken { get; set; } public virtual string AccessToken { get; set; }
{{#useRestSharp}}
{{#hasOAuthMethods}} {{#hasOAuthMethods}}
/// <summary> /// <summary>
/// Gets or sets the token URL for OAuth2 authentication. /// Gets or sets the token URL for OAuth2 authentication.
@ -357,6 +360,7 @@ namespace {{packageName}}.Client
public virtual OAuthFlow? OAuthFlow { get; set; } public virtual OAuthFlow? OAuthFlow { get; set; }
{{/hasOAuthMethods}} {{/hasOAuthMethods}}
{{/useRestSharp}}
/// <summary> /// <summary>
/// Gets or sets the temporary folder path to store the files downloaded from the server. /// Gets or sets the temporary folder path to store the files downloaded from the server.
/// </summary> /// </summary>
@ -684,12 +688,14 @@ namespace {{packageName}}.Client
Username = second.Username ?? first.Username, Username = second.Username ?? first.Username,
Password = second.Password ?? first.Password, Password = second.Password ?? first.Password,
AccessToken = second.AccessToken ?? first.AccessToken, AccessToken = second.AccessToken ?? first.AccessToken,
{{#useRestSharp}}
{{#hasOAuthMethods}} {{#hasOAuthMethods}}
OAuthTokenUrl = second.OAuthTokenUrl ?? first.OAuthTokenUrl, OAuthTokenUrl = second.OAuthTokenUrl ?? first.OAuthTokenUrl,
OAuthClientId = second.OAuthClientId ?? first.OAuthClientId, OAuthClientId = second.OAuthClientId ?? first.OAuthClientId,
OAuthClientSecret = second.OAuthClientSecret ?? first.OAuthClientSecret, OAuthClientSecret = second.OAuthClientSecret ?? first.OAuthClientSecret,
OAuthFlow = second.OAuthFlow ?? first.OAuthFlow, OAuthFlow = second.OAuthFlow ?? first.OAuthFlow,
{{/hasOAuthMethods}} {{/hasOAuthMethods}}
{{/useRestSharp}}
{{#hasHttpSignatureMethods}} {{#hasHttpSignatureMethods}}
HttpSigningConfiguration = second.HttpSigningConfiguration ?? first.HttpSigningConfiguration, HttpSigningConfiguration = second.HttpSigningConfiguration ?? first.HttpSigningConfiguration,
{{/hasHttpSignatureMethods}} {{/hasHttpSignatureMethods}}

View File

@ -4,8 +4,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
{{#useRestSharp}}
{{#hasOAuthMethods}}using {{packageName}}.Client.Auth; {{#hasOAuthMethods}}using {{packageName}}.Client.Auth;
{{/hasOAuthMethods}} {{/hasOAuthMethods}}
{{/useRestSharp}}
namespace {{packageName}}.Client namespace {{packageName}}.Client
{ {
@ -20,6 +22,7 @@ namespace {{packageName}}.Client
/// <value>Access token.</value> /// <value>Access token.</value>
string AccessToken { get; } string AccessToken { get; }
{{#useRestSharp}}
{{#hasOAuthMethods}} {{#hasOAuthMethods}}
/// <summary> /// <summary>
/// Gets the OAuth token URL. /// Gets the OAuth token URL.
@ -46,6 +49,7 @@ namespace {{packageName}}.Client
OAuthFlow? OAuthFlow { get; } OAuthFlow? OAuthFlow { get; }
{{/hasOAuthMethods}} {{/hasOAuthMethods}}
{{/useRestSharp}}
/// <summary> /// <summary>
/// Gets the API key. /// Gets the API key.
/// </summary> /// </summary>

View File

@ -49,4 +49,10 @@
{{/validatable}} {{/validatable}}
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -29,4 +29,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -97,9 +97,6 @@ src/Org.OpenAPITools/Api/UserApi.cs
src/Org.OpenAPITools/Client/ApiClient.cs src/Org.OpenAPITools/Client/ApiClient.cs
src/Org.OpenAPITools/Client/ApiException.cs src/Org.OpenAPITools/Client/ApiException.cs
src/Org.OpenAPITools/Client/ApiResponse.cs src/Org.OpenAPITools/Client/ApiResponse.cs
src/Org.OpenAPITools/Client/Auth/OAuthAuthenticator.cs
src/Org.OpenAPITools/Client/Auth/OAuthFlow.cs
src/Org.OpenAPITools/Client/Auth/TokenResponse.cs
src/Org.OpenAPITools/Client/ClientUtils.cs src/Org.OpenAPITools/Client/ClientUtils.cs
src/Org.OpenAPITools/Client/Configuration.cs src/Org.OpenAPITools/Client/Configuration.cs
src/Org.OpenAPITools/Client/ExceptionFactory.cs src/Org.OpenAPITools/Client/ExceptionFactory.cs

View File

@ -1,95 +0,0 @@
/*
* OpenAPI Petstore
*
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
*
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RestSharp;
using RestSharp.Authenticators;
namespace Org.OpenAPITools.Client.Auth
{
/// <summary>
/// An authenticator for OAuth2 authentication flows
/// </summary>
public class OAuthAuthenticator : AuthenticatorBase
{
readonly string _tokenUrl;
readonly string _clientId;
readonly string _clientSecret;
readonly string _grantType;
readonly JsonSerializerSettings _serializerSettings;
readonly IReadableConfiguration _configuration;
/// <summary>
/// Initialize the OAuth2 Authenticator
/// </summary>
public OAuthAuthenticator(
string tokenUrl,
string clientId,
string clientSecret,
OAuthFlow? flow,
JsonSerializerSettings serializerSettings,
IReadableConfiguration configuration) : base("")
{
_tokenUrl = tokenUrl;
_clientId = clientId;
_clientSecret = clientSecret;
_serializerSettings = serializerSettings;
_configuration = configuration;
switch (flow)
{
/*case OAuthFlow.ACCESS_CODE:
_grantType = "authorization_code";
break;
case OAuthFlow.IMPLICIT:
_grantType = "implicit";
break;
case OAuthFlow.PASSWORD:
_grantType = "password";
break;*/
case OAuthFlow.APPLICATION:
_grantType = "client_credentials";
break;
default:
break;
}
}
/// <summary>
/// Creates an authentication parameter from an access token.
/// </summary>
/// <param name="accessToken">Access token to create a parameter from.</param>
/// <returns>An authentication parameter.</returns>
protected override async ValueTask<Parameter> GetAuthenticationParameter(string accessToken)
{
var token = string.IsNullOrEmpty(Token) ? await GetToken() : Token;
return new HeaderParameter(KnownHeaders.Authorization, token);
}
/// <summary>
/// Gets the token from the OAuth2 server.
/// </summary>
/// <returns>An authentication token.</returns>
async Task<string> GetToken()
{
var client = new RestClient(_tokenUrl)
.UseSerializer(() => new CustomJsonCodec(_serializerSettings, _configuration));
var request = new RestRequest()
.AddParameter("grant_type", _grantType)
.AddParameter("client_id", _clientId)
.AddParameter("client_secret", _clientSecret);
var response = await client.PostAsync<TokenResponse>(request);
return $"{response.TokenType} {response.AccessToken}";
}
}
}

View File

@ -1,27 +0,0 @@
/*
* OpenAPI Petstore
*
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
*
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
namespace Org.OpenAPITools.Client.Auth
{
/// <summary>
/// Available flows for OAuth2 authentication
/// </summary>
public enum OAuthFlow
{
/// <summary>Authorization code flow</summary>
ACCESS_CODE,
/// <summary>Implicit flow</summary>
IMPLICIT,
/// <summary>Password flow</summary>
PASSWORD,
/// <summary>Client credentials flow</summary>
APPLICATION
}
}

View File

@ -1,22 +0,0 @@
/*
* OpenAPI Petstore
*
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
*
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
using Newtonsoft.Json;
namespace Org.OpenAPITools.Client.Auth
{
class TokenResponse
{
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
}
}

View File

@ -18,7 +18,6 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Net.Http; using System.Net.Http;
using Org.OpenAPITools.Client.Auth;
namespace Org.OpenAPITools.Client namespace Org.OpenAPITools.Client
{ {
@ -363,30 +362,6 @@ namespace Org.OpenAPITools.Client
/// <value>The access token.</value> /// <value>The access token.</value>
public virtual string AccessToken { get; set; } public virtual string AccessToken { get; set; }
/// <summary>
/// Gets or sets the token URL for OAuth2 authentication.
/// </summary>
/// <value>The OAuth Token URL.</value>
public virtual string OAuthTokenUrl { get; set; }
/// <summary>
/// Gets or sets the client ID for OAuth2 authentication.
/// </summary>
/// <value>The OAuth Client ID.</value>
public virtual string OAuthClientId { get; set; }
/// <summary>
/// Gets or sets the client secret for OAuth2 authentication.
/// </summary>
/// <value>The OAuth Client Secret.</value>
public virtual string OAuthClientSecret { get; set; }
/// <summary>
/// Gets or sets the flow for OAuth2 authentication.
/// </summary>
/// <value>The OAuth Flow.</value>
public virtual OAuthFlow? OAuthFlow { get; set; }
/// <summary> /// <summary>
/// Gets or sets the temporary folder path to store the files downloaded from the server. /// Gets or sets the temporary folder path to store the files downloaded from the server.
/// </summary> /// </summary>
@ -710,10 +685,6 @@ namespace Org.OpenAPITools.Client
Username = second.Username ?? first.Username, Username = second.Username ?? first.Username,
Password = second.Password ?? first.Password, Password = second.Password ?? first.Password,
AccessToken = second.AccessToken ?? first.AccessToken, AccessToken = second.AccessToken ?? first.AccessToken,
OAuthTokenUrl = second.OAuthTokenUrl ?? first.OAuthTokenUrl,
OAuthClientId = second.OAuthClientId ?? first.OAuthClientId,
OAuthClientSecret = second.OAuthClientSecret ?? first.OAuthClientSecret,
OAuthFlow = second.OAuthFlow ?? first.OAuthFlow,
HttpSigningConfiguration = second.HttpSigningConfiguration ?? first.HttpSigningConfiguration, HttpSigningConfiguration = second.HttpSigningConfiguration ?? first.HttpSigningConfiguration,
TempFolderPath = second.TempFolderPath ?? first.TempFolderPath, TempFolderPath = second.TempFolderPath ?? first.TempFolderPath,
DateTimeFormat = second.DateTimeFormat ?? first.DateTimeFormat, DateTimeFormat = second.DateTimeFormat ?? first.DateTimeFormat,

View File

@ -12,7 +12,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Org.OpenAPITools.Client.Auth;
namespace Org.OpenAPITools.Client namespace Org.OpenAPITools.Client
{ {
@ -27,30 +26,6 @@ namespace Org.OpenAPITools.Client
/// <value>Access token.</value> /// <value>Access token.</value>
string AccessToken { get; } string AccessToken { get; }
/// <summary>
/// Gets the OAuth token URL.
/// </summary>
/// <value>OAuth Token URL.</value>
string OAuthTokenUrl { get; }
/// <summary>
/// Gets the OAuth client ID.
/// </summary>
/// <value>OAuth Client ID.</value>
string OAuthClientId { get; }
/// <summary>
/// Gets the OAuth client secret.
/// </summary>
/// <value>OAuth Client Secret.</value>
string OAuthClientSecret { get; }
/// <summary>
/// Gets the OAuth flow.
/// </summary>
/// <value>OAuth Flow.</value>
OAuthFlow? OAuthFlow { get; }
/// <summary> /// <summary>
/// Gets the API key. /// Gets the API key.
/// </summary> /// </summary>

View File

@ -27,4 +27,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>

View File

@ -28,4 +28,10 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="System.Web" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project> </Project>