Skip to content

Commit e5b3393

Browse files
committed
feat: implement openapi spec, mapper and resource for navigation items creation.
1 parent 77bf00c commit e5b3393

File tree

7 files changed

+488
-9
lines changed

7 files changed

+488
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.management.v2.rest.mapper;
17+
18+
import io.gravitee.apim.core.portal_page.model.PortalArea;
19+
import io.gravitee.apim.core.portal_page.model.PortalNavigationFolder;
20+
import io.gravitee.apim.core.portal_page.model.PortalNavigationLink;
21+
import io.gravitee.apim.core.portal_page.model.PortalNavigationPage;
22+
import io.gravitee.apim.core.portal_page.model.PortalPageContentId;
23+
import io.gravitee.apim.core.portal_page.model.PortalPageNavigationId;
24+
import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationItem;
25+
import io.gravitee.rest.api.management.v2.rest.model.PortalNavigationItem;
26+
import org.mapstruct.Mapper;
27+
import org.mapstruct.Mapping;
28+
import org.mapstruct.Named;
29+
import org.mapstruct.factory.Mappers;
30+
31+
@Mapper
32+
public interface PortalNavigationItemsMapper {
33+
PortalNavigationItemsMapper INSTANCE = Mappers.getMapper(PortalNavigationItemsMapper.class);
34+
35+
@Mapping(target = "id", qualifiedByName = "mapId")
36+
@Mapping(target = "parentId", qualifiedByName = "mapParentId")
37+
@Mapping(target = "type", expression = "java(mapType(portalNavigationItem))")
38+
@Mapping(target = "contentId", expression = "java(mapContentId(portalNavigationItem))")
39+
@Mapping(target = "uri", expression = "java(mapUri(portalNavigationItem))")
40+
PortalNavigationItem map(io.gravitee.apim.core.portal_page.model.PortalNavigationItem portalNavigationItem);
41+
42+
@Named("mapId")
43+
default String mapId(PortalPageNavigationId id) {
44+
return id == null ? null : id.toString();
45+
}
46+
47+
@Named("mapParentId")
48+
default String mapParentId(PortalPageNavigationId id) {
49+
return id == null ? null : id.toString();
50+
}
51+
52+
default PortalNavigationItem.TypeEnum mapType(io.gravitee.apim.core.portal_page.model.PortalNavigationItem portalNavigationItem) {
53+
return switch (portalNavigationItem) {
54+
case PortalNavigationFolder ignored -> PortalNavigationItem.TypeEnum.FOLDER;
55+
case PortalNavigationPage ignored -> PortalNavigationItem.TypeEnum.PAGE;
56+
case PortalNavigationLink ignored -> PortalNavigationItem.TypeEnum.LINK;
57+
};
58+
}
59+
60+
default String mapContentId(io.gravitee.apim.core.portal_page.model.PortalNavigationItem portalNavigationItem) {
61+
if (portalNavigationItem instanceof PortalNavigationPage portalNavigationPage) {
62+
return portalNavigationPage.getContentId().toString();
63+
}
64+
return null;
65+
}
66+
67+
default String mapUri(io.gravitee.apim.core.portal_page.model.PortalNavigationItem portalNavigationItem) {
68+
if (portalNavigationItem instanceof PortalNavigationLink portalNavigationLink) {
69+
return portalNavigationLink.getHref();
70+
}
71+
return null;
72+
}
73+
74+
default io.gravitee.apim.core.portal_page.model.PortalNavigationItem map(
75+
String organizationId,
76+
String environmentId,
77+
CreatePortalNavigationItem createPortalNavigationItem
78+
) {
79+
final var id = PortalPageNavigationId.random();
80+
final var title = createPortalNavigationItem.getTitle();
81+
final var area = createPortalNavigationItem.getArea();
82+
final var parentIdStr = createPortalNavigationItem.getParentId();
83+
final var parentId = parentIdStr == null ? null : PortalPageNavigationId.of(parentIdStr);
84+
final var contentIdStr = createPortalNavigationItem.getContentId();
85+
final var contentId = contentIdStr == null ? null : PortalPageContentId.of(contentIdStr);
86+
final var uri = createPortalNavigationItem.getUri();
87+
88+
return switch (createPortalNavigationItem.getType()) {
89+
case FOLDER -> new PortalNavigationFolder(
90+
id,
91+
organizationId,
92+
environmentId,
93+
title,
94+
PortalArea.valueOf(area.getValue()),
95+
parentId
96+
);
97+
case PAGE -> new PortalNavigationPage(
98+
id,
99+
organizationId,
100+
environmentId,
101+
title,
102+
PortalArea.valueOf(area.getValue()),
103+
parentId,
104+
contentId
105+
);
106+
case LINK -> new PortalNavigationLink(
107+
id,
108+
organizationId,
109+
environmentId,
110+
title,
111+
PortalArea.valueOf(area.getValue()),
112+
parentId,
113+
uri
114+
);
115+
};
116+
}
117+
}

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/installation/EnvironmentResource.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.gravitee.rest.api.management.v2.rest.resource.group.GroupsResource;
3131
import io.gravitee.rest.api.management.v2.rest.resource.kafka_console.ProxyKafkaConsoleResource;
3232
import io.gravitee.rest.api.management.v2.rest.resource.ui.PortalMenuLinksResource;
33+
import io.gravitee.rest.api.management.v2.rest.resource.ui.PortalNavigationItemsResource;
3334
import io.gravitee.rest.api.management.v2.rest.resource.ui.ThemesResource;
3435
import io.gravitee.rest.api.service.EnvironmentService;
3536
import io.gravitee.rest.api.service.common.GraviteeContext;
@@ -73,6 +74,11 @@ public PortalMenuLinksResource getPortalMenuLinksResource() {
7374
return resourceContext.getResource(PortalMenuLinksResource.class);
7475
}
7576

77+
@Path("/portal-navigation-items")
78+
public PortalNavigationItemsResource getPortalNavigationItemsResource() {
79+
return resourceContext.getResource(PortalNavigationItemsResource.class);
80+
}
81+
7682
@Path("/ui/themes")
7783
public ThemesResource getThemesResource() {
7884
return resourceContext.getResource(ThemesResource.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.management.v2.rest.resource.ui;
17+
18+
import io.gravitee.apim.core.portal_page.use_case.CreatePortalNavigationItemUseCase;
19+
import io.gravitee.common.http.MediaType;
20+
import io.gravitee.rest.api.management.v2.rest.mapper.PortalNavigationItemsMapper;
21+
import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationItem;
22+
import io.gravitee.rest.api.management.v2.rest.model.Error;
23+
import io.gravitee.rest.api.management.v2.rest.model.ErrorDetailsInner;
24+
import io.gravitee.rest.api.management.v2.rest.resource.AbstractResource;
25+
import io.gravitee.rest.api.management.v2.rest.validation.PortalNavigationItemsValidator;
26+
import io.gravitee.rest.api.model.permissions.RolePermission;
27+
import io.gravitee.rest.api.model.permissions.RolePermissionAction;
28+
import io.gravitee.rest.api.rest.annotation.Permission;
29+
import io.gravitee.rest.api.rest.annotation.Permissions;
30+
import io.gravitee.rest.api.service.common.GraviteeContext;
31+
import jakarta.inject.Inject;
32+
import jakarta.validation.Valid;
33+
import jakarta.validation.constraints.NotNull;
34+
import jakarta.ws.rs.*;
35+
import jakarta.ws.rs.core.Response;
36+
import java.util.stream.Collectors;
37+
import lombok.extern.slf4j.Slf4j;
38+
import org.openapitools.jackson.nullable.JsonNullable;
39+
40+
@Slf4j
41+
public class PortalNavigationItemsResource extends AbstractResource {
42+
43+
@Inject
44+
private CreatePortalNavigationItemUseCase createPortalNavigationItemUseCase;
45+
46+
private final PortalNavigationItemsMapper mapper = PortalNavigationItemsMapper.INSTANCE;
47+
48+
@POST
49+
@Permissions({ @Permission(value = RolePermission.ENVIRONMENT_SETTINGS, acls = { RolePermissionAction.UPDATE }) })
50+
@Consumes(MediaType.APPLICATION_JSON)
51+
@Produces(MediaType.APPLICATION_JSON)
52+
public Response createPortalNavigationItem(@Valid @NotNull final CreatePortalNavigationItem createPortalNavigationItem) {
53+
final var errors = PortalNavigationItemsValidator.validate(createPortalNavigationItem);
54+
55+
if (!errors.isEmpty()) {
56+
return Response.status(Response.Status.BAD_REQUEST)
57+
.entity(
58+
new Error()
59+
.httpStatus(Response.Status.BAD_REQUEST.getStatusCode())
60+
.message("Validation error")
61+
.details(
62+
errors
63+
.stream()
64+
.map(error ->
65+
new ErrorDetailsInner()
66+
.message(error.getErrorMessage())
67+
.invalidValue(error.isPresent() ? JsonNullable.of(error.getLocation()) : JsonNullable.undefined())
68+
.location(error.getLocation())
69+
)
70+
.toList()
71+
)
72+
)
73+
.build();
74+
}
75+
76+
final var executionContext = GraviteeContext.getExecutionContext();
77+
78+
final var output = createPortalNavigationItemUseCase.execute(
79+
new CreatePortalNavigationItemUseCase.Input(
80+
mapper.map(executionContext.getOrganizationId(), executionContext.getEnvironmentId(), createPortalNavigationItem)
81+
)
82+
);
83+
84+
return Response.created(this.getLocationHeader(output.item().getId().toString())).entity(mapper.map(output.item())).build();
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.management.v2.rest.validation;
17+
18+
import static io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationItem.TypeEnum;
19+
20+
import io.gravitee.apim.core.portal_page.query_service.PortalNavigationItemsQueryService;
21+
import io.gravitee.common.utils.UUID;
22+
import io.gravitee.rest.api.management.v2.rest.model.CreatePortalNavigationItem;
23+
import java.util.HashSet;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
26+
import java.util.stream.Stream;
27+
import lombok.Builder;
28+
import lombok.Getter;
29+
30+
public class PortalNavigationItemsValidator {
31+
32+
private static final String MESSAGE_INVALID_UUID = "Invalid UUID string: %s";
33+
private static final String MESSAGE_INAPPLICABLE_PARAMETER = "Parameter '%s' is only applicable to items of type %s";
34+
private static final String MESSAGE_REQUIRED_PARAMETER = "Parameter '%s' is required for items of type %s";
35+
private static final String CONTENT_ID = "contentId";
36+
private static final String PARENT_ID = "parentId";
37+
private static final String URI = "uri";
38+
39+
private PortalNavigationItemsValidator() {}
40+
41+
public static Set<ValidationError> validate(CreatePortalNavigationItem createPortalNavigationItem) {
42+
final var commonErrors = new HashSet<ValidationError>();
43+
final var parentId = createPortalNavigationItem.getParentId();
44+
if (parentId != null) {
45+
final var error = validateUUID(PARENT_ID, parentId);
46+
if (error != null) {
47+
commonErrors.add(error);
48+
}
49+
}
50+
final var type = createPortalNavigationItem.getType();
51+
final var specificErrors = switch (type) {
52+
case FOLDER -> validateFolderCreationParams(createPortalNavigationItem);
53+
case PAGE -> validatePageCreationParams(createPortalNavigationItem);
54+
case LINK -> validateLinkCreationParams(createPortalNavigationItem);
55+
};
56+
return Stream.concat(commonErrors.stream(), specificErrors.stream()).collect(Collectors.toSet());
57+
}
58+
59+
private static Set<ValidationError> validateFolderCreationParams(CreatePortalNavigationItem createPortalNavigationItem) {
60+
final var errors = new HashSet<ValidationError>();
61+
if (createPortalNavigationItem.getContentId() != null) {
62+
errors.add(
63+
ValidationError.builder()
64+
.errorMessage(String.format(MESSAGE_INAPPLICABLE_PARAMETER, CONTENT_ID, TypeEnum.PAGE.name()))
65+
.location(CONTENT_ID)
66+
.present(true)
67+
.build()
68+
);
69+
}
70+
if (createPortalNavigationItem.getUri() != null) {
71+
errors.add(
72+
ValidationError.builder()
73+
.errorMessage(String.format(MESSAGE_INAPPLICABLE_PARAMETER, URI, TypeEnum.LINK.name()))
74+
.location(URI)
75+
.present(true)
76+
.build()
77+
);
78+
}
79+
return errors;
80+
}
81+
82+
private static Set<ValidationError> validatePageCreationParams(CreatePortalNavigationItem createPortalNavigationItem) {
83+
final var errors = new HashSet<ValidationError>();
84+
if (createPortalNavigationItem.getContentId() == null) {
85+
errors.add(
86+
ValidationError.builder()
87+
.errorMessage(String.format(MESSAGE_REQUIRED_PARAMETER, CONTENT_ID, TypeEnum.PAGE.name()))
88+
.location(CONTENT_ID)
89+
.present(false)
90+
.build()
91+
);
92+
} else {
93+
final var uuidError = validateUUID(CONTENT_ID, createPortalNavigationItem.getContentId());
94+
if (uuidError != null) {
95+
errors.add(uuidError);
96+
}
97+
}
98+
if (createPortalNavigationItem.getUri() != null) {
99+
errors.add(
100+
ValidationError.builder()
101+
.errorMessage(String.format(MESSAGE_INAPPLICABLE_PARAMETER, URI, TypeEnum.LINK.name()))
102+
.location(URI)
103+
.present(true)
104+
.build()
105+
);
106+
}
107+
return errors;
108+
}
109+
110+
private static Set<ValidationError> validateLinkCreationParams(CreatePortalNavigationItem createPortalNavigationItem) {
111+
final var errors = new HashSet<ValidationError>();
112+
if (createPortalNavigationItem.getUri() == null) {
113+
errors.add(
114+
ValidationError.builder()
115+
.errorMessage(String.format(MESSAGE_REQUIRED_PARAMETER, URI, TypeEnum.LINK.name()))
116+
.location(URI)
117+
.present(false)
118+
.build()
119+
);
120+
}
121+
if (createPortalNavigationItem.getContentId() != null) {
122+
errors.add(
123+
ValidationError.builder()
124+
.errorMessage(String.format(MESSAGE_INAPPLICABLE_PARAMETER, CONTENT_ID, TypeEnum.PAGE.name()))
125+
.location(CONTENT_ID)
126+
.present(true)
127+
.build()
128+
);
129+
}
130+
return errors;
131+
}
132+
133+
private static ValidationError validateUUID(String field, String uuid) {
134+
try {
135+
UUID.fromString(uuid);
136+
return null;
137+
} catch (IllegalArgumentException e) {
138+
return ValidationError.builder().errorMessage(String.format(MESSAGE_INVALID_UUID, uuid)).location(field).present(true).build();
139+
}
140+
}
141+
142+
@Getter
143+
@Builder
144+
public static class ValidationError {
145+
146+
private String errorMessage;
147+
private String location;
148+
private boolean present;
149+
}
150+
}

0 commit comments

Comments
 (0)