/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.modules.fo.restapi; import static org.olat.restapi.security.RestSecurityHelper.getUserRequest; import static org.olat.restapi.security.RestSecurityHelper.isAuthor; import static org.olat.restapi.security.RestSecurityHelper.isAuthorEditor; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.BaseSecurityManager; import org.olat.core.commons.services.notifications.NotificationsManager; import org.olat.core.commons.services.notifications.Subscriber; import org.olat.core.gui.UserRequest; import org.olat.core.id.Identity; import org.olat.core.util.StringHelper; import org.olat.core.util.nodes.INode; import org.olat.core.util.tree.Visitor; import org.olat.course.ICourse; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.FOCourseNode; import org.olat.course.properties.CoursePropertyManager; import org.olat.course.run.userview.CourseTreeVisitor; import org.olat.course.run.userview.VisibleTreeFilter; import org.olat.modules.ModuleConfiguration; import org.olat.modules.fo.Forum; import org.olat.modules.fo.Message; import org.olat.modules.fo.manager.ForumManager; import org.olat.properties.Property; import org.olat.restapi.repository.course.AbstractCourseNodeWebService; import org.olat.restapi.repository.course.CourseWebService; import org.olat.restapi.repository.course.CoursesWebService; import org.olat.restapi.security.RestSecurityHelper; /** * * Description:<br> * REST API implementation for forum course node * * <P> * Initial Date: 20.12.2010 <br> * @author skoeber */ @Path("repo/courses/{courseId}/elements/forum") public class ForumCourseNodeWebService extends AbstractCourseNodeWebService { /** * Retrieves metadata of the published course node * @response.representation.200.qname {http://www.example.com}forumVOes * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The course node metadatas * @response.representation.200.example {@link org.olat.modules.fo.restapi.Examples#SAMPLE_FORUMVOes} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course or parentNode not found * @param courseId The course resourceable's id * @param httpRequest The HTTP request * @return The persisted structure element (fully populated) */ @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getForums(@PathParam("courseId") Long courseId, @Context HttpServletRequest httpRequest) { final ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (!CourseWebService.isCourseAccessible(course, false, httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } UserRequest ureq = getUserRequest(httpRequest); final Set<Long> subcribedForums = new HashSet<Long>(); NotificationsManager man = NotificationsManager.getInstance(); List<String> notiTypes = Collections.singletonList("Forum"); List<Subscriber> subs = man.getSubscribers(ureq.getIdentity(), notiTypes); for(Subscriber sub:subs) { Long forumKey = Long.parseLong(sub.getPublisher().getData()); subcribedForums.add(forumKey); } final List<ForumVO> forumVOs = new ArrayList<ForumVO>(); new CourseTreeVisitor(course, ureq.getUserSession().getIdentityEnvironment()).visit(new Visitor() { @Override public void visit(INode node) { if(node instanceof FOCourseNode) { FOCourseNode forumNode = (FOCourseNode)node; ForumVO forum = createForumVO(course, forumNode, subcribedForums); forumVOs.add(forum); } } }, new VisibleTreeFilter()); ForumVOes voes = new ForumVOes(); voes.setForums(forumVOs.toArray(new ForumVO[forumVOs.size()])); voes.setTotalCount(forumVOs.size()); return Response.ok(voes).build(); } /** * This attaches a Forum Element onto a given course. The element will be * inserted underneath the supplied parentNodeId. * @response.representation.200.qname {http://www.example.com}courseNodeVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The course node metadatas * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_COURSENODEVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course or parentNode not found * @param courseId The course resourceable's id * @param parentNodeId The node's id which will be the parent of this single * page * @param position The node's position relative to its sibling nodes (optional) * @param shortTitle The node short title * @param longTitle The node long title * @param objectives The node learning objectives * @param visibilityExpertRules The rules to view the node (optional) * @param accessExpertRules The rules to access the node (optional) * @param request The HTTP request * @return The persisted Forum Element (fully populated) */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response attachForumPost(@PathParam("courseId") Long courseId, @FormParam("parentNodeId") String parentNodeId, @FormParam("position") Integer position, @FormParam("shortTitle") @DefaultValue("undefined") String shortTitle, @FormParam("longTitle") @DefaultValue("undefined") String longTitle, @FormParam("objectives") @DefaultValue("undefined") String objectives, @FormParam("visibilityExpertRules") String visibilityExpertRules, @FormParam("accessExpertRules") String accessExpertRules, @FormParam("moderatorExpertRules") String moderatorExpertRules, @FormParam("posterExpertRules") String posterExpertRules, @FormParam("readerExpertRules") String readerExpertRules, @Context HttpServletRequest request) { return attachForum(courseId, parentNodeId, position, shortTitle, longTitle, objectives, visibilityExpertRules, accessExpertRules, moderatorExpertRules, posterExpertRules, readerExpertRules, request); } /** * This attaches a Forum Element onto a given course. The element will be * inserted underneath the supplied parentNodeId. * @response.representation.200.qname {http://www.example.com}courseNodeVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The course node metadatas * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_COURSENODEVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course or parentNode not found * @param courseId The course resourceable id * @param parentNodeId The node's id which will be the parent of this single * page * @param position The node's position relative to its sibling nodes (optional) * @param shortTitle The node short title * @param longTitle The node long title * @param objectives The node learning objectives * @param visibilityExpertRules The rules to view the node (optional) * @param accessExpertRules The rules to access the node (optional) * @param request The HTTP request * @return The persisted Forum Element (fully populated) */ @PUT @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response attachForum(@PathParam("courseId") Long courseId, @QueryParam("parentNodeId") String parentNodeId, @QueryParam("position") Integer position, @QueryParam("shortTitle") @DefaultValue("undefined") String shortTitle, @QueryParam("longTitle") @DefaultValue("undefined") String longTitle, @QueryParam("objectives") @DefaultValue("undefined") String objectives, @QueryParam("visibilityExpertRules") String visibilityExpertRules, @QueryParam("accessExpertRules") String accessExpertRules, @QueryParam("moderatorExpertRules") String moderatorExpertRules, @QueryParam("posterExpertRules") String posterExpertRules, @QueryParam("readerExpertRules") String readerExpertRules, @Context HttpServletRequest request) { ForumCustomConfig config = new ForumCustomConfig(moderatorExpertRules, posterExpertRules, readerExpertRules); return attach(courseId, parentNodeId, "fo", position, shortTitle, longTitle, objectives, visibilityExpertRules, accessExpertRules, config, request); } /** * Retrieves metadata of the published course node * @response.representation.200.qname {http://www.example.com}forumVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The course node metadatas * @response.representation.200.example {@link org.olat.modules.fo.restapi.Examples#SAMPLE_FORUMVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The course or parentNode not found * @param courseId The course resourceable's id * @param nodeId The node's id * @param httpRequest The HTTP request * @return The persisted structure element (fully populated) */ @GET @Path("{nodeId}") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getForum(@PathParam("courseId") Long courseId, @PathParam("nodeId") String nodeId, @Context HttpServletRequest httpRequest) { ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (!CourseWebService.isCourseAccessible(course, false, httpRequest)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } CourseNode courseNode = course.getRunStructure().getNode(nodeId); if(courseNode == null || !(courseNode instanceof FOCourseNode)) { return Response.serverError().status(Status.NOT_FOUND).build(); } UserRequest ureq = getUserRequest(httpRequest); CourseTreeVisitor courseVisitor = new CourseTreeVisitor(course, ureq.getUserSession().getIdentityEnvironment()); if(courseVisitor.isAccessible(courseNode, new VisibleTreeFilter())) { FOCourseNode forumNode = (FOCourseNode)courseNode; Set<Long> subscriptions = new HashSet<Long>(); NotificationsManager man = NotificationsManager.getInstance(); List<String> notiTypes = Collections.singletonList("Forum"); List<Subscriber> subs = man.getSubscribers(ureq.getIdentity(), notiTypes); for(Subscriber sub:subs) { Long forumKey = Long.parseLong(sub.getPublisher().getData()); subscriptions.add(forumKey); } ForumVO forumVo = createForumVO(course, forumNode, subscriptions); return Response.ok(forumVo).build(); } else { return Response.serverError().status(Status.UNAUTHORIZED).build(); } } @Path("{nodeId}/forum") public ForumWebService getForumContent(@PathParam("courseId") Long courseId, @PathParam("nodeId") String nodeId, @Context HttpServletRequest request) { ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { throw new WebApplicationException(Response.serverError().status(Status.NOT_FOUND).build()); } else if (!CourseWebService.isCourseAccessible(course, false, request)) { throw new WebApplicationException(Response.serverError().status(Status.UNAUTHORIZED).build()); } CourseNode courseNode = course.getRunStructure().getNode(nodeId); if(courseNode == null || !(courseNode instanceof FOCourseNode)) { throw new WebApplicationException(Response.serverError().status(Status.NOT_FOUND).build()); } UserRequest ureq = getUserRequest(request); CourseTreeVisitor courseVisitor = new CourseTreeVisitor(course, ureq.getUserSession().getIdentityEnvironment()); if(courseVisitor.isAccessible(courseNode, new VisibleTreeFilter())) { FOCourseNode forumNode = (FOCourseNode)courseNode; Forum forum = forumNode.loadOrCreateForum(course.getCourseEnvironment()); return new ForumWebService(forum); } else { throw new WebApplicationException(Response.serverError().status(Status.UNAUTHORIZED).build()); } } /** * Creates a new thread in the forum of the course node * @response.representation.200.qname {http://www.example.com}messageVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The root message of the thread * @response.representation.200.example {@link org.olat.modules.fo.restapi.Examples#SAMPLE_MESSAGEVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The author, forum or message not found * @param courseId The id of the course. * @param nodeId The id of the course node. * @param title The title for the first post in the thread * @param body The body for the first post in the thread * @param identityName The author identity name (optional) * @param sticky Creates sticky thread. * @param request The HTTP request * @return The new thread * * @deprecated use the {nodeId}/forum/threads instead */ @PUT @Path("{nodeId}/thread") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response newThreadToForum(@PathParam("courseId") Long courseId, @PathParam("nodeId") String nodeId, @QueryParam("title") String title, @QueryParam("body") String body, @QueryParam("identityName") String identityName, @QueryParam("sticky") Boolean isSticky, @Context HttpServletRequest request) { return addMessage(courseId, nodeId, null, title, body, identityName, isSticky, request); } /** * Creates a new forum message in the forum of the course node * @response.representation.200.qname {http://www.example.com}messageVO * @response.representation.200.mediaType application/xml, application/json * @response.representation.200.doc The root message of the thread * @response.representation.200.example {@link org.olat.modules.fo.restapi.Examples#SAMPLE_MESSAGEVO} * @response.representation.401.doc The roles of the authenticated user are not sufficient * @response.representation.404.doc The author, forum or message not found * @param courseId The id of the course. * @param nodeId The id of the course node. * @param parentMessageId The id of the parent message. * @param title The title for the first post in the thread * @param body The body for the first post in the thread * @param identityName The author identity name (optional) * @param request The HTTP request * @return The new thread * * @deprecated use the {nodeId}/forum/messages instead */ @PUT @Path("{nodeId}/message") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response newMessageToForum(@PathParam("courseId") Long courseId, @PathParam("nodeId") String nodeId, @QueryParam("parentMessageId") Long parentMessageId, @QueryParam("title") String title, @QueryParam("body") String body, @QueryParam("identityName") String identityName, @Context HttpServletRequest request) { if(parentMessageId == null || parentMessageId == 0L) { return Response.serverError().status(Status.NOT_FOUND).build(); } return addMessage(courseId, nodeId, parentMessageId, title, body, identityName, false, request); } /** * Internal helper method to add a message to a forum. * @param courseId * @param nodeId * @param parentMessageId can be null (will lead to new thread) * @param title * @param body * @param identityName * @param isSticky only necessary when adding new thread * @param request * @return */ private Response addMessage(Long courseId, String nodeId, Long parentMessageId, String title, String body, String identityName, Boolean isSticky, HttpServletRequest request) { if(!isAuthor(request)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } BaseSecurity securityManager = BaseSecurityManager.getInstance(); Identity identity; if (identityName != null) { identity = securityManager.findIdentityByName(identityName); } else { identity = RestSecurityHelper.getIdentity(request); } if(identity == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } //load forum ICourse course = CoursesWebService.loadCourse(courseId); if(course == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } else if (!isAuthorEditor(course, request)) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } CourseNode courseNode = getParentNode(course, nodeId); if(courseNode == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager(); Property forumKeyProp = cpm.findCourseNodeProperty(courseNode, null, null, FOCourseNode.FORUM_KEY); Forum forum = null; ForumManager fom = ForumManager.getInstance(); if(forumKeyProp!=null) { // Forum does already exist, load forum with key from properties Long forumKey = forumKeyProp.getLongValue(); forum = fom.loadForum(forumKey); } if(forum == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } MessageVO vo; if(parentMessageId == null || parentMessageId == 0L) { // creating the thread (a message without a parent message) Message newThread = fom.createMessage(forum, identity, false); if (isSticky != null && isSticky.booleanValue()) { // set sticky org.olat.modules.fo.Status status = new org.olat.modules.fo.Status(); status.setSticky(true); newThread.setStatusCode(org.olat.modules.fo.Status.getStatusCode(status)); } newThread.setTitle(title); newThread.setBody(body); // open a new thread fom.addTopMessage(newThread); vo = new MessageVO(newThread); } else { // adding response message (a message with a parent message) Message threadMessage = fom.loadMessage(parentMessageId); if(threadMessage == null) { return Response.serverError().status(Status.NOT_FOUND).build(); } // create new message Message message = fom.createMessage(forum, identity, false); message.setTitle(title); message.setBody(body); fom.replyToMessage(message, threadMessage); vo = new MessageVO(message); } return Response.ok(vo).build(); } //fxdiff: RESTAPI add special expert rules for forums private class ForumCustomConfig implements CustomConfigDelegate { private final String preConditionModerator; private final String preConditionPoster; private final String preConditionReader; public ForumCustomConfig(String preConditionModerator, String preConditionPoster, String preConditionReader) { this.preConditionModerator = preConditionModerator; this.preConditionPoster = preConditionPoster; this.preConditionReader = preConditionReader; } @Override public boolean isValid() { return true; } @Override public void configure(ICourse course, CourseNode newNode, ModuleConfiguration moduleConfig) { // create the forum ForumManager fom = ForumManager.getInstance(); CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager(); Forum forum = fom.addAForum(); Long forumKey = forum.getKey(); Property forumKeyProperty = cpm.createCourseNodePropertyInstance(newNode, null, null, FOCourseNode.FORUM_KEY, null, forumKey, null, null); cpm.saveProperty(forumKeyProperty); // special rules if(StringHelper.containsNonWhitespace(preConditionModerator)) { ((FOCourseNode)newNode).setPreConditionModerator(createExpertCondition("moderator", preConditionModerator)); } if(StringHelper.containsNonWhitespace(preConditionPoster)) { ((FOCourseNode)newNode).setPreConditionPoster(createExpertCondition("poster", preConditionPoster)); } if(StringHelper.containsNonWhitespace(preConditionReader)) { ((FOCourseNode)newNode).setPreConditionReader(createExpertCondition("reader", preConditionReader)); } } } public static ForumVO createForumVO(ICourse course, FOCourseNode forumNode, Set<Long> subscribed) { Forum forum = forumNode.loadOrCreateForum(course.getCourseEnvironment()); ForumVO forumVo = new ForumVO(); forumVo.setName(course.getCourseTitle()); forumVo.setDetailsName(forumNode.getShortTitle()); if(subscribed != null && subscribed.contains(forum.getKey())) { forumVo.setSubscribed(true); } else { forumVo.setSubscribed(false); } forumVo.setCourseKey(course.getResourceableId()); forumVo.setCourseNodeId(forumNode.getIdent()); forumVo.setForumKey(forum.getKey()); return forumVo; } }