FolderResource.java
/*
* Copyright (C) 2000 - 2026 Silverpeas
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* As a special exception to the terms and conditions of version 3.0 of the GPL, you may
* redistribute this Program in connection with Free/Libre Open Source Software ("FLOSS")
* applications as described in Silverpeas's FLOSS exception. You should have received a copy of the
* text describing the FLOSS exception, and it is also available here:
* "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.silverpeas.components.kmelia.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
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 jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.owasp.encoder.Encode;
import org.silverpeas.components.kmelia.service.KmeliaHelper;
import org.silverpeas.components.kmelia.service.KmeliaService;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.annotation.WebService;
import org.silverpeas.core.node.model.NodeDetail;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.node.service.NodeService;
import org.silverpeas.core.web.rs.RESTWebService;
import org.silverpeas.core.web.rs.annotation.Authorized;
import org.silverpeas.core.webapi.node.NodeAttrEntity;
import org.silverpeas.core.webapi.node.NodeEntity;
import org.silverpeas.core.webapi.node.NodeType;
import org.silverpeas.kernel.bundle.LocalizationBundle;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.kernel.logging.SilverLogger;
import java.net.URI;
import java.util.*;
/**
* A REST Web resource representing a given node. It is a web service that provides access to a
* node referenced by its URL.
*/
@WebService
@Authorized
@Path(FolderResource.PATH + "/{componentId}")
public class FolderResource extends RESTWebService {
static final String PATH = "folders";
@PathParam("componentId")
private String componentId;
@Inject
private KmeliaService kmeliaService;
/**
* Get the root of the application and its children.
*
* @return the application root and its children
*/
@Operation(summary = "Gets the root folder of the application with its children.")
@ApiResponse(responseCode = "200", description = "The root folder and its children.",
content = @Content(schema = @Schema(implementation = NodeEntity.class)))
@GET
@Produces(MediaType.APPLICATION_JSON)
public NodeEntity getRoot(@QueryParam("lang") String language) {
NodeDetail root;
try {
root = getKmeliaService().getRoot(componentId, getUser().getId());
} catch (Exception e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
final URI uri;
if (getUri().getRequestUri().toString().endsWith("/" + NodePK.ROOT_NODE_ID)) {
uri = getUri().getRequestUri();
} else {
uri = getUri().getRequestUriBuilder().path(root.getNodePK().getId()).build();
}
return asNodeEntity(root, uri, language);
}
/**
* Get any node of the application and its children.
*
* @return NodeEntity representing asking node
*/
@Operation(summary = "Gets the folder at the given path with its children.",
description = "The path is made up of the identifiers of the folders, separated by a " +
"slash, from a child of the root down to the asked folder.")
@ApiResponse(responseCode = "200", description = "The asked folder and its children.",
content = @Content(schema = @Schema(implementation = NodeEntity.class)))
@GET
@Path("{path: \\d+(/\\d+)*}")
@Produces(MediaType.APPLICATION_JSON)
public NodeEntity getNode(@PathParam("path") String path, @QueryParam("lang") String language) {
String nodeId = getNodeIdFromURI(path);
NodeDetail node = getKmeliaService().getFolder(getNodePK(nodeId), getUser().getId());
URI uri = getUri().getRequestUri();
return asNodeEntity(node, uri, language);
}
/**
* Get the path of any node of the application, from the root down to that node.
*
* @return a list of NodeEntity representing the path of the node
*/
@Operation(summary = "Gets the folders making the path of the given folder.",
description = "The folders are ordered from the root of the application down to the asked " +
"folder, both included.")
@ApiResponse(responseCode = "200", description = "The folders making the path.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NodeEntity.class))))
@GET
@Path("{path: \\d+(/\\d+)*}/path")
@Produces(MediaType.APPLICATION_JSON)
public List<NodeEntity> getPath(@PathParam("path") String path,
@QueryParam("lang") String language) {
NodePK nodePK = new NodePK(getNodeIdFromURI(path), componentId);
List<NodeDetail> nodes;
try {
nodes = new ArrayList<>(getNodeService().getPath(nodePK));
Collections.reverse(nodes);
return asNodeEntities(nodes, language, nodePK.isTrash());
} catch (Exception e1) {
throw new WebApplicationException(e1, Status.INTERNAL_SERVER_ERROR);
}
}
@Nonnull
private List<NodeEntity> asNodeEntities(final Collection<NodeDetail> nodes,
final String language,
final boolean decorate) {
String requestUri = getUri().getRequestUri().toString();
String uri = requestUri.substring(0, requestUri.lastIndexOf('/'));
List<NodeEntity> entities = new ArrayList<>();
final SilverpeasRole highestUserRole = getHighestUserRoleIfAny();
for (NodeDetail node : nodes) {
entities.add(NodeEntity.fromNodeDetail(highestUserRole, node, uri, language));
}
return decorate ? decorateRootChildren(entities, language) : entities;
}
/**
* Get all children of any node of the application.
*
* @return a list of NodeEntity representing children
*/
@Operation(summary = "Gets the direct children of the given folder.")
@ApiResponse(responseCode = "200", description = "The children of the asked folder.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NodeEntity.class))))
@GET
@Path("{path: \\d+(/\\d+)*}/children")
@Produces(MediaType.APPLICATION_JSON)
public List<NodeEntity> getChildren(@PathParam("path") String path,
@QueryParam("lang") String language) {
NodePK nodePK = new NodePK(getNodeIdFromURI(path), componentId);
try {
Collection<NodeDetail> children =
getKmeliaService().getFolderChildren(nodePK, getUser().getId());
return asNodeEntities(children, language, nodePK.isRoot());
} catch (Exception e) {
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Creates a node corresponding to the given node entity and whose parent matches the specified
* path.
*
* @param path The path of the parent node.
* @param nodeEntity The description of the node to create.
* @return a response containing the entity describing the newly created node.
*/
@Operation(summary = "Creates a folder as a child of the folder at the specified path.")
@ApiResponse(responseCode = "201", description = "The newly created folder.",
content = @Content(schema = @Schema(implementation = NodeEntity.class)))
@ApiResponse(responseCode = "204", description = "No name was given to the folder to create.")
@ApiResponse(responseCode = "406",
description = "A folder with the same name already exists at the specified path.")
@Path("{path: \\d+(/\\d+)*}")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response createNode(@PathParam("path") String path, final NodeEntity nodeEntity) {
String parentNodeId = getNodeIdFromURI(path);
NodePK nodePK = getNodePK(parentNodeId);
String nodeName = nodeEntity.getText();
if (StringUtils.isEmpty(nodeName)) {
throw new WebApplicationException(Status.NO_CONTENT);
}
Collection<NodeDetail> children;
try {
children = getKmeliaService().getFolderChildren(nodePK, getUser().getId());
} catch (Exception e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
if (children != null) {
for (NodeDetail node : children) {
if (nodeName.equals(node.getName())) {
throw new WebApplicationException(Status.NOT_ACCEPTABLE);
}
}
}
NodeAttrEntity nodeAttr = nodeEntity.getAttr();
String userId = nodeAttr.getCreatorId();
if (StringUtils.isEmpty(userId) && nodeAttr.getCreator() != null) {
userId = nodeAttr.getCreator().getId();
}
String description = nodeAttr.getDescription();
try {
NodeDetail node = getKmeliaService().createTopic(
componentId, parentNodeId, null, userId, nodeName, description);
URI uri = getUri().getRequestUri();
NodeEntity newNodeEntity = NodeEntity.fromNodeDetail(getHighestUserRoleIfAny(), node, uri);
return Response.created(uri).entity(newNodeEntity).build();
} catch (Exception ex) {
throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
}
}
private void decorateRoot(NodeEntity root, String lang) {
root.getState().setOpened(true);
root.setType(NodeType.ROOT);
decorateRootChildren(root.getChildren(), lang);
}
private List<NodeEntity> decorateRootChildren(List<NodeEntity> children, String lang) {
// case of special nodes (bin, to validate)
LocalizationBundle messages =
ResourceLocator.getLocalizationBundle("org.silverpeas.kmelia.multilang.kmeliaBundle", lang);
for (NodeEntity child : children) {
if (KmeliaHelper.isToValidateFolder(child.getAttr().getId())) {
child.setType(NodeType.TO_VALIDATE);
child.getState().opened(false).setSelected(false);
child.setText(Encode.forHtml(messages.getString("ToValidateShort")));
child.getAttr().setDescription(messages.getString("kmelia.tovalidate.desc"));
} else if (child.getAttr().getId().equalsIgnoreCase(NodePK.BIN_NODE_ID)) {
child.setType(NodeType.BIN);
child.getState().opened(false).setSelected(false);
child.setText(Encode.forHtml(messages.getString("kmelia.basket")));
child.getAttr().setDescription(messages.getString("kmelia.basket.desc"));
} else if (KmeliaHelper.isNonVisiblePubsFolder(child.getAttr().getId())) {
child.setType(NodeType.NOT_VISIBLE_CONTRIBUTIONS);
child.getState().opened(false).setSelected(false);
child.setText(Encode.forHtml(messages.getString("kmelia.folder.nonvisiblepubs")));
child.getAttr().setDescription(messages.getString("kmelia.folder.nonvisiblepubs.desc"));
}
}
return children;
}
/**
* Get the tree of the folders of the application, expanded down to the given folder.
*
* @return the root of the application with the branch leading to the given folder expanded
*/
@Operation(summary = "Gets the tree of folders expanded down to the given folder.",
description = "The whole tree of the application is returned, but only the branch leading " +
"to the asked folder is expanded.")
@ApiResponse(responseCode = "200", description = "The root folder with the expanded branch.",
content = @Content(schema = @Schema(implementation = NodeEntity.class)))
@GET
@Path("{path: \\d+}/treeview")
@Produces(MediaType.APPLICATION_JSON)
public NodeEntity getTreeview(@PathParam("path") String path, @QueryParam("lang") String language) {
String nodeId = getNodeIdFromURI(path);
try {
List<NodeDetail> nodes = new ArrayList<>(getNodeService().getPath(new NodePK(nodeId,
componentId)));
Collections.reverse(nodes);
NodeDetail root = getKmeliaService().getExpandedPathToNode(new NodePK(nodeId, componentId),
getUser().getId());
String requestUri = getUri().getRequestUri().toString();
String uri = requestUri.substring(0, requestUri.lastIndexOf('/'));
NodeEntity rootEntity = NodeEntity.fromNodeDetail(getHighestUserRoleIfAny(), root, uri, language);
decorateRoot(rootEntity, language);
setOpenState(rootEntity.getChildren(), nodes);
return rootEntity;
} catch (Exception e) {
SilverLogger.getLogger(this).error("Error in getTreeview()",e);
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
}
private void setOpenState(List<NodeEntity> children, List<NodeDetail> path) {
for (NodeEntity child : children) {
if (isInPath(child, path)) {
child.getState().setOpened(true);
setOpenState(child.getChildren(), path);
}
}
}
private boolean isInPath(NodeEntity entity, List<NodeDetail> path) {
for (NodeDetail node : path) {
if (node.getNodePK().getId().equals(entity.getAttr().getId())) {
return true;
}
}
return false;
}
@Override
protected String getResourceBasePath() {
return PATH;
}
@Override
public String getComponentId() {
return componentId;
}
private String getNodeIdFromURI(String uri) {
String[] nodeIds = uri.split("/");
return nodeIds[nodeIds.length - 1];
}
private NodeEntity asNodeEntity(final NodeDetail node, final URI uri, final String language) {
final NodeEntity entity = NodeEntity.fromNodeDetail(getHighestUserRoleIfAny(), node, uri, language);
if (node.isRoot()) {
decorateRoot(entity, language);
}
return entity;
}
private NodePK getNodePK(String id) {
return new NodePK(id, getComponentId());
}
private NodeService getNodeService() {
try {
return NodeService.get();
} catch (Exception e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
}
private KmeliaService getKmeliaService() {
return kmeliaService;
}
private SilverpeasRole getHighestUserRoleIfAny() {
return getUser() != null && getComponentId() != null ? getHighestUserRole() : null;
}
}