Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions celements-navigation-rest/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.celements</groupId>
<artifactId>celements-parent</artifactId>
<version>7.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>celements-navigation-rest</artifactId>
<version>7.1-SNAPSHOT</version>
<description>Celements Navigation REST API</description>
<dependencies>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-model</artifactId>
<scope>provided</scope>
</dependency>
<!-- Runtime component registration is supplied by this artifact. -->
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-spring-security</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-xwiki-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-core-model</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-component</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-commons</artifactId>
<scope>test</scope>
</dependency>
<!-- Test doubles reflect over APIs whose provided signatures use these artifacts. -->
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-reference</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-observation</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-shared-tests</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-subsystem-migration-manager</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-xwiki-velocity</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
<version>1.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>test</scope>
</dependency>
<!-- Springdoc's ModelResolver loads XmlRootElement reflectively on Java 21. -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<!-- Runtime/component and reflective uses are invisible to bytecode analysis. -->
<usedDependencies>
<usedDependency>com.celements:celements-servlet</usedDependency>
<usedDependency>com.celements:celements-commons</usedDependency>
<usedDependency>com.celements:celements-reference</usedDependency>
<usedDependency>com.celements:celements-observation</usedDependency>
<usedDependency>com.celements:celements-subsystem-migration-manager</usedDependency>
<usedDependency>com.celements:celements-xwiki-velocity</usedDependency>
<usedDependency>javax.xml.bind:jaxb-api</usedDependency>
</usedDependencies>
</configuration>
</plugin>
</plugins>
</build>
<scm>
<connection>scm:git:git@github.com:celements/celements-base.git</connection>
<developerConnection>scm:git:git@github.com:celements/celements-base.git</developerConnection>
<url>https://github.com/celements/celements-base/celements-navigation-rest</url>
<tag>HEAD</tag>
</scm>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.celements.navigation.api;

import javax.inject.Inject;

import org.springframework.stereotype.Component;
import org.xwiki.model.reference.DocumentReference;

import com.celements.model.context.ModelContext;
import com.celements.model.util.ModelUtils;
import com.celements.navigation.cmd.MultilingualMenuNameCommand;
import com.celements.url.UrlService;

@Component
class DefaultNavigationNodeValueResolver {

private final ModelUtils modelUtils;
private final ModelContext modelContext;
private final UrlService urlService;
private final MultilingualMenuNameCommand menuNameCommand;

@Inject
DefaultNavigationNodeValueResolver(ModelUtils modelUtils, ModelContext modelContext,
UrlService urlService) {
this(modelUtils, modelContext, urlService, new MultilingualMenuNameCommand());
}

DefaultNavigationNodeValueResolver(ModelUtils modelUtils, ModelContext modelContext,
UrlService urlService, MultilingualMenuNameCommand menuNameCommand) {
this.modelUtils = modelUtils;
this.modelContext = modelContext;
this.urlService = urlService;
this.menuNameCommand = menuNameCommand;
}

public String serialize(DocumentReference docRef) {
return modelUtils.serializeRefLocal(docRef);
}

public String resolveTitle(DocumentReference docRef, String language) {
return menuNameCommand.getMultilingualMenuName(modelUtils.serializeRefLocal(docRef), language,
modelContext.getXWikiContext());
}

public String resolveUrl(DocumentReference docRef, String language) {
return urlService.getURL(docRef, "view", "language=" + language);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.celements.navigation.api;

import org.springframework.http.HttpStatus;

final class NavigationApiException extends RuntimeException {

private static final long serialVersionUID = 1L;

private final HttpStatus status;
private final String code;

NavigationApiException(HttpStatus status, String code, String message) {
super(message);
this.status = status;
this.code = code;
}

HttpStatus status() {
return status;
}

String code() {
return code;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.celements.navigation.api;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

@RestController
@RequestMapping("/v1/navigation")
public class NavigationController {

private static final Logger LOGGER = LoggerFactory.getLogger(NavigationController.class);
static final String CACHE_CONTROL = "private, no-store";

private final NavigationRequestResolver requestResolver;
private final NavigationTreeBuilder treeBuilder;

@Inject
NavigationController(NavigationRequestResolver requestResolver,
NavigationTreeBuilder treeBuilder) {
this.requestResolver = requestResolver;
this.treeBuilder = treeBuilder;
}

@GetMapping(path = "/{nodeSpace}", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("permitAll()")
@Operation(summary = "Get the current wiki's navigation tree")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Navigation tree"),
@ApiResponse(responseCode = "400",
description = "Invalid reference or parameter, or unsupported language",
content = @Content(schema = @Schema(implementation = NavigationErrorResponse.class))),
@ApiResponse(responseCode = "404",
description = "The active node is absent, inaccessible, out of root, or part-excluded",
content = @Content(schema = @Schema(implementation = NavigationErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "Navigation infrastructure is unavailable",
content = @Content(schema = @Schema(implementation = NavigationErrorResponse.class))) })
public ResponseEntity<NavigationTreeResponse> getNavigation(
@Parameter(description = "Canonical local space reference") @PathVariable String nodeSpace,
@Parameter(description = "Canonical local document reference") @RequestParam(
required = false) String currentNode,
@Parameter(
description = "Allowed wiki language; defaults to the request language") @RequestParam(
required = false) String language,
@Parameter(
description = "Case-sensitive root part filter; missing or blank returns all parts")
@RequestParam(required = false) String partName,
@Parameter(description = "Inactive expansion threshold from 0 through 100",
schema = @Schema(defaultValue = "0", minimum = "0", maximum = "100")) @RequestParam(
name = "show_inactive_to_level", defaultValue = "0") int showInactiveToLevel) {
Comment on lines +45 to +70

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation soup.

Springdoc already infers request parameters, success responses, and record schemas. Remove inline JSON examples and annotations that merely restate names or types. Keep only a short operation summary, non-obvious constraints, and explicit error responses. Simplify OpenAPI tests to verify structural contract instead of prose and examples. Do not move duplication into interfaces, wrappers, or custom annotations.

Applies to:

  • NavigationController.java
  • NavigationErrorResponse.java
  • NavigationNodeDto.java
  • NavigationSegmentDto.java
  • NavigationTreeResponse.java
  • NavigationOpenApiContractTest.java

var request = requestResolver.resolve(nodeSpace, currentNode, language, partName,
showInactiveToLevel);
var response = treeBuilder.build(request);
return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL).body(response);
}

@ExceptionHandler(NavigationApiException.class)
public ResponseEntity<NavigationErrorResponse> handleNavigationApiException(
NavigationApiException exception) {
return errorResponse(exception.status(),
new NavigationErrorResponse(exception.code(), exception.getMessage()));
}

@ExceptionHandler({ MethodArgumentTypeMismatchException.class, ConversionFailedException.class })
public ResponseEntity<NavigationErrorResponse> handleInvalidParameter() {
return errorResponse(HttpStatus.BAD_REQUEST,
new NavigationErrorResponse("invalid_parameter", "The parameter is invalid."));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<NavigationErrorResponse> handleUnexpectedException(Exception exception) {
LOGGER.error("Navigation REST request failed.", exception);
return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, new NavigationErrorResponse(
"navigation_unavailable", "Navigation is currently unavailable."));
}

private ResponseEntity<NavigationErrorResponse> errorResponse(HttpStatus status,
NavigationErrorResponse body) {
return ResponseEntity.status(status)
.header(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL)
.body(body);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.celements.navigation.api;

public record NavigationErrorResponse(String code, String message) {

}
Loading