Swagger fixes and additions
This commit is contained in:
parent
bba4834e21
commit
2712a3d20e
|
@ -14,7 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.opencdmp.audit.AuditableAction;
|
import org.opencdmp.audit.AuditableAction;
|
||||||
import org.opencdmp.controllers.swagger.SwaggerHelpers;
|
import org.opencdmp.controllers.swagger.SwaggerHelpers;
|
||||||
import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader;
|
import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader;
|
||||||
import org.opencdmp.model.Lock;
|
import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses;
|
||||||
import org.opencdmp.model.Tenant;
|
import org.opencdmp.model.Tenant;
|
||||||
import org.opencdmp.models.Account;
|
import org.opencdmp.models.Account;
|
||||||
import org.opencdmp.models.AccountBuilder;
|
import org.opencdmp.models.AccountBuilder;
|
||||||
|
@ -32,12 +32,17 @@ import java.util.List;
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/principal/")
|
@RequestMapping("/api/principal/")
|
||||||
@Tag(name = "Principal", description = "Get user account information")
|
@Tag(name = "Principal", description = "Get user account information")
|
||||||
|
@SwaggerCommonErrorResponses
|
||||||
public class PrincipalController {
|
public class PrincipalController {
|
||||||
|
|
||||||
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(PrincipalController.class));
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(PrincipalController.class));
|
||||||
|
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
|
|
||||||
private final CurrentPrincipalResolver currentPrincipalResolver;
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
|
||||||
private final AccountBuilder accountBuilder;
|
private final AccountBuilder accountBuilder;
|
||||||
|
|
||||||
private final TenantService tenantService;
|
private final TenantService tenantService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -51,7 +56,7 @@ public class PrincipalController {
|
||||||
this.tenantService = tenantService;
|
this.tenantService = tenantService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = "me", method = RequestMethod.GET )
|
@RequestMapping(path = "me", method = RequestMethod.GET)
|
||||||
@OperationWithTenantHeader(summary = "Fetch auth information of the logged in user", description = "",
|
@OperationWithTenantHeader(summary = "Fetch auth information of the logged in user", description = "",
|
||||||
responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(
|
responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
|
|
|
@ -12,7 +12,6 @@ import gr.cite.tools.fieldset.FieldSet;
|
||||||
import gr.cite.tools.logging.LoggerService;
|
import gr.cite.tools.logging.LoggerService;
|
||||||
import gr.cite.tools.logging.MapLogEntry;
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
@ -33,7 +32,6 @@ import org.opencdmp.data.ReferenceEntity;
|
||||||
import org.opencdmp.model.builder.reference.ReferenceBuilder;
|
import org.opencdmp.model.builder.reference.ReferenceBuilder;
|
||||||
import org.opencdmp.model.censorship.reference.ReferenceCensor;
|
import org.opencdmp.model.censorship.reference.ReferenceCensor;
|
||||||
import org.opencdmp.model.persist.ReferencePersist;
|
import org.opencdmp.model.persist.ReferencePersist;
|
||||||
import org.opencdmp.model.planblueprint.PlanBlueprint;
|
|
||||||
import org.opencdmp.model.reference.Reference;
|
import org.opencdmp.model.reference.Reference;
|
||||||
import org.opencdmp.model.result.QueryResult;
|
import org.opencdmp.model.result.QueryResult;
|
||||||
import org.opencdmp.query.ReferenceQuery;
|
import org.opencdmp.query.ReferenceQuery;
|
||||||
|
@ -74,7 +72,6 @@ public class ReferenceController {
|
||||||
|
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ReferenceController(
|
public ReferenceController(
|
||||||
BuilderFactory builderFactory,
|
BuilderFactory builderFactory,
|
||||||
|
@ -126,10 +123,27 @@ public class ReferenceController {
|
||||||
return new QueryResult<>(models, count);
|
return new QueryResult<>(models, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("search")
|
@PostMapping("search")
|
||||||
@Hidden
|
@OperationWithTenantHeader(summary = "Query all references including results from external APIs", description = SwaggerHelpers.Reference.endpoint_search, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.Reference.endpoint_search_request_body, content = @Content(
|
||||||
//TODO thgiannos add swagger docs
|
examples = {
|
||||||
|
@ExampleObject(
|
||||||
|
name = "Pagination and projection",
|
||||||
|
description = "Simple paginated request using a property projection list and pagination info",
|
||||||
|
value = SwaggerHelpers.Reference.endpoint_search_request_body_example
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)), responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = Reference.class
|
||||||
|
)
|
||||||
|
),
|
||||||
|
examples = @ExampleObject(
|
||||||
|
name = "First page",
|
||||||
|
description = "Example with the first page of paginated results",
|
||||||
|
value = SwaggerHelpers.Reference.endpoint_search_response_example
|
||||||
|
))))
|
||||||
|
@Swagger404
|
||||||
public List<Reference> searchReferenceWithDefinition(@RequestBody ReferenceSearchLookup lookup) throws MyNotFoundException, InvalidApplicationException {
|
public List<Reference> searchReferenceWithDefinition(@RequestBody ReferenceSearchLookup lookup) throws MyNotFoundException, InvalidApplicationException {
|
||||||
logger.debug("search with db definition {}", Reference.class.getSimpleName());
|
logger.debug("search with db definition {}", Reference.class.getSimpleName());
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import gr.cite.tools.fieldset.FieldSet;
|
||||||
import gr.cite.tools.logging.LoggerService;
|
import gr.cite.tools.logging.LoggerService;
|
||||||
import gr.cite.tools.logging.MapLogEntry;
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
@ -37,9 +36,11 @@ import org.opencdmp.model.builder.PlanAssociatedUserBuilder;
|
||||||
import org.opencdmp.model.builder.UserBuilder;
|
import org.opencdmp.model.builder.UserBuilder;
|
||||||
import org.opencdmp.model.censorship.PlanAssociatedUserCensor;
|
import org.opencdmp.model.censorship.PlanAssociatedUserCensor;
|
||||||
import org.opencdmp.model.censorship.UserCensor;
|
import org.opencdmp.model.censorship.UserCensor;
|
||||||
import org.opencdmp.model.persist.*;
|
import org.opencdmp.model.persist.UserMergeRequestPersist;
|
||||||
|
import org.opencdmp.model.persist.UserPersist;
|
||||||
|
import org.opencdmp.model.persist.UserRolePatchPersist;
|
||||||
|
import org.opencdmp.model.persist.UserTenantUsersInviteRequest;
|
||||||
import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist;
|
import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist;
|
||||||
import org.opencdmp.model.plan.Plan;
|
|
||||||
import org.opencdmp.model.result.QueryResult;
|
import org.opencdmp.model.result.QueryResult;
|
||||||
import org.opencdmp.model.user.User;
|
import org.opencdmp.model.user.User;
|
||||||
import org.opencdmp.query.UserQuery;
|
import org.opencdmp.query.UserQuery;
|
||||||
|
@ -143,7 +144,25 @@ public class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("plan-associated/query")
|
@PostMapping("plan-associated/query")
|
||||||
@Hidden
|
@OperationWithTenantHeader(summary = "Query all plan associated users", description = SwaggerHelpers.User.endpoint_query_plan_associated, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = SwaggerHelpers.User.endpoint_query_request_body, content = @Content(
|
||||||
|
examples = {
|
||||||
|
@ExampleObject(
|
||||||
|
name = "Pagination and projection",
|
||||||
|
description = "Simple paginated request using a property projection list and pagination info",
|
||||||
|
value = SwaggerHelpers.User.endpoint_query_plan_associated_request_body_example
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)), responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = PlanAssociatedUser.class
|
||||||
|
)
|
||||||
|
),
|
||||||
|
examples = @ExampleObject(
|
||||||
|
name = "First page",
|
||||||
|
description = "Example with the first page of paginated results",
|
||||||
|
value = SwaggerHelpers.User.endpoint_query_plan_associated_response_example
|
||||||
|
))))
|
||||||
public QueryResult<PlanAssociatedUser> queryPlanAssociated(@RequestBody UserLookup lookup) throws MyApplicationException, MyForbiddenException {
|
public QueryResult<PlanAssociatedUser> queryPlanAssociated(@RequestBody UserLookup lookup) throws MyApplicationException, MyForbiddenException {
|
||||||
logger.debug("querying {}", User.class.getSimpleName());
|
logger.debug("querying {}", User.class.getSimpleName());
|
||||||
|
|
||||||
|
@ -219,8 +238,11 @@ public class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/export/csv/{hasTenantAdminMode}")
|
@GetMapping("/export/csv/{hasTenantAdminMode}")
|
||||||
@Hidden
|
@OperationWithTenantHeader(summary = "Export users in a .csv file", description = "",
|
||||||
public ResponseEntity<byte[]> exportCsv(@PathVariable("hasTenantAdminMode") Boolean hasTenantAdminMode) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
|
responses = @ApiResponse(description = "OK", responseCode = "200"))
|
||||||
|
public ResponseEntity<byte[]> exportCsv(
|
||||||
|
@Parameter(name = "hasTenantAdminMode", description = "Controls whether to fetch users as a tenant admin or not", example = "false", required = true) @PathVariable("hasTenantAdminMode") Boolean hasTenantAdminMode
|
||||||
|
) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException, InvalidApplicationException {
|
||||||
logger.debug(new MapLogEntry("export" + User.class.getSimpleName()).And("hasTenantAdminMode", hasTenantAdminMode));
|
logger.debug(new MapLogEntry("export" + User.class.getSimpleName()).And("hasTenantAdminMode", hasTenantAdminMode));
|
||||||
|
|
||||||
// this.censorFactory.censor(UserCensor.class).censor(fieldSet, null);
|
// this.censorFactory.censor(UserCensor.class).censor(fieldSet, null);
|
||||||
|
|
|
@ -3346,6 +3346,190 @@ public final class SwaggerHelpers {
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_search =
|
||||||
|
"""
|
||||||
|
This endpoint is used to fetch all the available references.<br/>
|
||||||
|
It also allows to restrict the results using a query object passed in the request body.<br/>
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_search_request_body =
|
||||||
|
"""
|
||||||
|
Let's explore the options this object gives us.
|
||||||
|
|
||||||
|
### <u>General query parameters:</u>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><b>page:</b>
|
||||||
|
This is an object controlling the pagination of the results. It contains two properties.
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
<li><b>offset:</b>
|
||||||
|
How many records to omit.
|
||||||
|
</li>
|
||||||
|
<li><b>size:</b>
|
||||||
|
How many records to include in each page.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
For example, if we want the third page, and our pages to contain 15 elements, we would pass the following object:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"offset": 30,
|
||||||
|
"size": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><b>order:</b>
|
||||||
|
This is an object controlling the ordering of the results.
|
||||||
|
It contains a list of strings called <i>items</i> with the names of the properties to use.
|
||||||
|
<br/>If the name of the property is prefixed with a <b>'-'</b>, the ordering direction is <b>DESC</b>. Otherwise, it is <b>ASC</b>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
For example, if we wanted to order based on the field 'createdAt' in descending order, we would pass the following object:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
"-createdAt"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><b>metadata:</b>
|
||||||
|
This is an object containing metadata for the request. There is only one available option.
|
||||||
|
<ul>
|
||||||
|
<li><b>countAll:</b>
|
||||||
|
If this is set to true, the count property included in the response will account for all the records regardless the pagination,
|
||||||
|
with all the rest of filtering options applied of course.
|
||||||
|
Otherwise, if it is set to false or not present, only the returned results will be counted.
|
||||||
|
<br/>The first option is useful for the UI clients to calculate how many result pages are available.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>project:</b>
|
||||||
|
This is an object controlling the data projection of the results.
|
||||||
|
It contains a list of strings called <i>fields</i> with the names of the properties to project.
|
||||||
|
<br/>You can also include properties that are deeper in the object tree by prefixing them with dots.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
### <u>Reference specific query parameters:</u>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><b>like:</b>
|
||||||
|
If there is a like parameter present in the query, only the reference entities that include the contents of the parameter either in their labels, descriptions or the references will be in the response.
|
||||||
|
</li>
|
||||||
|
<li><b>typeId:</b>
|
||||||
|
This is the type id of the references we want in the response. <br/>If empty, every record is included.
|
||||||
|
</li>
|
||||||
|
<li><b>key:</b>
|
||||||
|
This is the id of the external source (API) we want results from.
|
||||||
|
<br/>If not present, no external reference is included.
|
||||||
|
</li>
|
||||||
|
<li><b>dependencyReferences:</b>
|
||||||
|
This is a list and determines which records we want to include in the response, based on the references they depend on.
|
||||||
|
<br/>If not present, every record is included.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_search_request_body_example =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"project":{
|
||||||
|
"fields":[
|
||||||
|
"id",
|
||||||
|
"hash",
|
||||||
|
"label",
|
||||||
|
"type",
|
||||||
|
"type.id",
|
||||||
|
"description",
|
||||||
|
"definition.fields.code",
|
||||||
|
"definition.fields.dataType",
|
||||||
|
"definition.fields.value",
|
||||||
|
"reference",
|
||||||
|
"abbreviation",
|
||||||
|
"source",
|
||||||
|
"sourceType"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"page":{
|
||||||
|
"size":100,
|
||||||
|
"offset":0
|
||||||
|
},
|
||||||
|
"typeId":"5b9c284f-f041-4995-96cc-fad7ad13289c",
|
||||||
|
"dependencyReferences":[
|
||||||
|
\s
|
||||||
|
],
|
||||||
|
"order":{
|
||||||
|
"items":[
|
||||||
|
"label"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_search_response_example =
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "A Randomized, Double-Blind, Placebo-Controlled, Multi-Center Study to Evaluate the Efficacy of ManNAc in Subjects with GNE Myopathy (nih_________::5U01AR070498-04)",
|
||||||
|
"type": {
|
||||||
|
"id": "5b9c284f-f041-4995-96cc-fad7ad13289c"
|
||||||
|
},
|
||||||
|
"description": "A Randomized, Double-Blind, Placebo-Controlled, Multi-Center Study to Evaluate the Efficacy of ManNAc in Subjects with GNE Myopathy",
|
||||||
|
"definition": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"code": "referenceType",
|
||||||
|
"dataType": 0,
|
||||||
|
"value": "Grants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "key",
|
||||||
|
"dataType": 0,
|
||||||
|
"value": "openaire"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reference": "nih_________::5U01AR070498-04",
|
||||||
|
"source": "openaire",
|
||||||
|
"sourceType": 1,
|
||||||
|
"hash": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "A genome scale census of virulence factors in the major mould pathogen of human lungs, Aspergillus fumigatus (ukri________::1640253)",
|
||||||
|
"type": {
|
||||||
|
"id": "5b9c284f-f041-4995-96cc-fad7ad13289c"
|
||||||
|
},
|
||||||
|
"description": "A genome scale census of virulence factors in the major mould pathogen of human lungs, Aspergillus fumigatus",
|
||||||
|
"definition": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"code": "referenceType",
|
||||||
|
"dataType": 0,
|
||||||
|
"value": "Grants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "key",
|
||||||
|
"dataType": 0,
|
||||||
|
"value": "openaire"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reference": "ukri________::1640253",
|
||||||
|
"source": "openaire",
|
||||||
|
"sourceType": 1,
|
||||||
|
"hash": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ReferenceType {
|
public static final class ReferenceType {
|
||||||
|
@ -3779,6 +3963,12 @@ public final class SwaggerHelpers {
|
||||||
It also allows to restrict the results using a query object passed in the request body.<br/>
|
It also allows to restrict the results using a query object passed in the request body.<br/>
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_query_plan_associated =
|
||||||
|
"""
|
||||||
|
This endpoint is used to fetch all the available users.<br/>
|
||||||
|
It also allows to restrict the results using a query object passed in the request body.<br/>
|
||||||
|
""";
|
||||||
|
|
||||||
public static final String endpoint_query_request_body =
|
public static final String endpoint_query_request_body =
|
||||||
"""
|
"""
|
||||||
Let's explore the options this object gives us.
|
Let's explore the options this object gives us.
|
||||||
|
@ -3908,6 +4098,32 @@ public final class SwaggerHelpers {
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_query_plan_associated_request_body_example =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"project":{
|
||||||
|
"fields":[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"page":{
|
||||||
|
"size":100,
|
||||||
|
"offset":0
|
||||||
|
},
|
||||||
|
"isActive":[
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"order":{
|
||||||
|
"items":[
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"like":"user%"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
public static final String endpoint_query_response_example =
|
public static final String endpoint_query_response_example =
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
|
@ -4241,6 +4457,25 @@ public final class SwaggerHelpers {
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static final String endpoint_query_plan_associated_response_example =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"items":[
|
||||||
|
{
|
||||||
|
"id":"d26916c6-8763-450e-9048-b06e1114d0b4",
|
||||||
|
"name":"user3 user3",
|
||||||
|
"email":"user3@dmp.gr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"02832fb6-0b12-469f-a886-7685406959d4",
|
||||||
|
"name":"user4",
|
||||||
|
"email":"user4@dmp.gr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count":2
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Principal {
|
public static final class Principal {
|
||||||
|
|
Loading…
Reference in New Issue