/* * Copyright 2013-2020 Erudika. https://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.scoold.api; import com.erudika.para.annotations.Locked; import com.erudika.para.client.ParaClient; import com.erudika.para.core.ParaObject; import com.erudika.para.core.Sysprop; import com.erudika.para.core.Tag; import com.erudika.para.core.User; import com.erudika.para.core.Webhook; import com.erudika.para.core.utils.ParaObjectUtils; import com.erudika.para.utils.Config; import com.erudika.para.utils.Pager; import com.erudika.para.utils.Utils; import com.erudika.para.validation.ValidationUtils; import com.erudika.scoold.ScooldServer; import static com.erudika.scoold.ScooldServer.AUTH_USER_ATTRIBUTE; import static com.erudika.scoold.ScooldServer.CONTEXT_PATH; import static com.erudika.scoold.ScooldServer.REST_ENTITY_ATTRIBUTE; import com.erudika.scoold.controllers.CommentController; import com.erudika.scoold.controllers.PeopleController; import com.erudika.scoold.controllers.ProfileController; import com.erudika.scoold.controllers.QuestionController; import com.erudika.scoold.controllers.QuestionsController; import com.erudika.scoold.controllers.ReportsController; import com.erudika.scoold.controllers.RevisionsController; import com.erudika.scoold.controllers.TagsController; import com.erudika.scoold.controllers.VoteController; import com.erudika.scoold.core.Comment; import com.erudika.scoold.core.Post; import com.erudika.scoold.core.Profile; import com.erudika.scoold.core.Question; import com.erudika.scoold.core.Reply; import com.erudika.scoold.core.Report; import com.erudika.scoold.core.Revision; import com.erudika.scoold.core.UnapprovedQuestion; import com.erudika.scoold.core.UnapprovedReply; import com.erudika.scoold.utils.ScooldUtils; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.BadRequestException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; 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.context.request.WebRequest; /** * Scoold REST API * @author Alex Bogdanovski [[email protected]] */ @RestController @RequestMapping("/api") @SuppressWarnings("unchecked") public class ApiController { public static final Logger logger = LoggerFactory.getLogger(ApiController.class); private static final String[] POST_TYPES = new String[] {Utils.type(Question.class), Utils.type(Reply.class)}; private final ScooldUtils utils; private final ParaClient pc; @Inject private QuestionsController questionsController; @Inject private QuestionController questionController; @Inject private VoteController voteController; @Inject private CommentController commentController; @Inject private PeopleController peopleController; @Inject private ProfileController profileController; @Inject private RevisionsController revisionsController; @Inject private TagsController tagsController; @Inject private ReportsController reportsController; @Inject public ApiController(ScooldUtils utils) { this.utils = utils; this.pc = utils.getParaClient(); } @GetMapping public Map<String, Object> get(HttpServletRequest req, HttpServletResponse res) { if (!utils.isApiEnabled()) { res.setStatus(HttpStatus.FORBIDDEN.value()); return null; } Map<String, Object> intro = new HashMap<>(); intro.put("message", Config.APP_NAME + " API, see docs at " + ScooldServer.getServerURL() + CONTEXT_PATH + "/apidocs"); boolean healthy; try { healthy = pc != null && pc.getTimestamp() > 0; } catch (Exception e) { healthy = false; } intro.put("healthy", healthy); intro.put("pro", false); if (!healthy) { res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); } return intro; } @PostMapping("/posts") public Map<String, Object> createPost(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (!entity.containsKey(Config._TYPE)) { entity.put(Config._TYPE, POST_TYPES[0]); } else if (!StringUtils.equalsAnyIgnoreCase((CharSequence) entity.get(Config._TYPE), POST_TYPES)) { badReq("Invalid post type - could be one of " + Arrays.toString(POST_TYPES)); } Post post = ParaObjectUtils.setAnnotatedFields(entity); if (!StringUtils.isBlank(post.getCreatorid())) { Profile authUser = pc.read(Profile.id(post.getCreatorid())); if (authUser != null) { req.setAttribute(AUTH_USER_ATTRIBUTE, authUser); } } Model model = new ExtendedModelMap(); List<String> spaces = readSpaces(post.getSpace()); post.setSpace(spaces.iterator().hasNext() ? spaces.iterator().next() : null); if (post.isQuestion()) { questionsController.post(post.getLocation(), post.getLatlng(), post.getAddress(), post.getSpace(), req, res, model); } else if (post.isReply()) { questionController.reply(post.getParentid(), "", null, req, res, model); } else { badReq("Invalid post type - could be one of " + Arrays.toString(POST_TYPES)); } checkForErrorsAndThrow(model); Map<String, Object> newpost = (Map<String, Object>) model.getAttribute("newpost"); res.setStatus(HttpStatus.CREATED.value()); return newpost; } @GetMapping("/posts") public List<Map<String, Object>> listQuestions(HttpServletRequest req) { Model model = new ExtendedModelMap(); questionsController.getQuestions(req.getParameter("sortby"), req.getParameter("filter"), req, model); return ((List<Question>) model.getAttribute("questionslist")).stream().map(p -> { Map<String, Object> post = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(p, false)); post.put("author", p.getAuthor()); return post; }).collect(Collectors.toList()); } @GetMapping("/posts/{id}") public Map<String, Object> getPost(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); questionController.get(id, "", req.getParameter("sortby"), req, res, model); Post showPost = (Post) model.getAttribute("showPost"); List<Post> answers = (List<Post>) model.getAttribute("answerslist"); List<Post> similar = (List<Post>) model.getAttribute("similarquestions"); if (showPost == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } Map<String, Object> result = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(showPost, false)); List<Map<String, Object>> answerz = answers.stream().map(p -> { Map<String, Object> post = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(p, false)); post.put("author", p.getAuthor()); return post; }).collect(Collectors.toList()); result.put("comments", showPost.getComments()); result.put("author", showPost.getAuthor()); showPost.setItemcount(null); if (!showPost.isReply()) { result.put("children", answerz); result.put("similar", similar); } return result; } @PatchMapping("/posts/{id}") public Post updatePost(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String editorid = (String) entity.get("lasteditby"); if (!StringUtils.isBlank(editorid)) { Profile authUser = pc.read(Profile.id(editorid)); if (authUser != null) { req.setAttribute(AUTH_USER_ATTRIBUTE, authUser); } } String space = (String) entity.get("space"); String title = (String) entity.get("title"); String body = (String) entity.get("body"); String location = (String) entity.get("location"); String latlng = (String) entity.get("latlng"); List<String> spaces = readSpaces(space); space = spaces.iterator().hasNext() ? spaces.iterator().next() : null; Model model = new ExtendedModelMap(); questionController.edit(id, title, body, String.join(",", (List<String>) entity.get("tags")), location, latlng, space, req, res, model); Post post = (Post) model.getAttribute("post"); if (post == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); } else if (!utils.canEdit(post, utils.getAuthUser(req))) { badReq("Update failed - user " + editorid + " is not allowed to update post."); } return post; } @DeleteMapping("/posts/{id}") public void deletePost(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); questionController.delete(id, req, model); res.setStatus(model.containsAttribute("deleted") ? 200 : HttpStatus.NOT_FOUND.value()); } @PutMapping("/posts/{id}/approve") public void approvePost(@PathVariable String id, HttpServletRequest req) { questionController.modApprove(id, req); } @PutMapping("/posts/{id}/accept/{replyid}") public void acceptReply(@PathVariable String id, @PathVariable String replyid, HttpServletRequest req) { questionController.approve(id, replyid, req); } @PutMapping("/posts/{id}/close") public void closePost(@PathVariable String id, HttpServletRequest req) { questionController.close(id, req); } @PutMapping("/posts/{id}/pin") public void pinPost(@PathVariable String id, HttpServletRequest req) { badReq("Not supported"); } @PutMapping("/posts/{id}/restore/{revisionid}") public void restoreRevision(@PathVariable String id, @PathVariable String revisionid, HttpServletRequest req) { questionController.restore(id, revisionid, req); } @PutMapping("/posts/{id}/like") public void favPost(@PathVariable String id, HttpServletRequest req) { badReq("Not supported"); } @PutMapping("/posts/{id}/voteup") public void upvotePost(@PathVariable String id, @RequestParam(required = false) String userid, HttpServletRequest req, HttpServletResponse res) { if (!voteRequest(true, id, userid, req)) { badReq("Vote request failed."); } } @PutMapping("/posts/{id}/votedown") public void downvotePost(@PathVariable String id, @RequestParam(required = false) String userid, HttpServletRequest req) { if (!voteRequest(false, id, userid, req)) { badReq("Vote request failed."); } } @GetMapping("/posts/{id}/comments") public List<Comment> getPostComments(@PathVariable String id, @RequestParam(required = false, defaultValue = "5") String limit, @RequestParam(required = false, defaultValue = "1") String page, @RequestParam(required = false, defaultValue = Config._TIMESTAMP) String sortby, @RequestParam(required = false, defaultValue = "false") String desc, HttpServletRequest req, HttpServletResponse res) { Post post = pc.read(id); if (post == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } post.getItemcount().setLimit(NumberUtils.toInt(limit)); post.getItemcount().setPage(NumberUtils.toInt(page)); post.getItemcount().setSortby(sortby); post.getItemcount().setDesc(Boolean.parseBoolean(desc)); utils.reloadFirstPageOfComments(post); return post.getComments(); } @GetMapping("/posts/{id}/revisions") public List<Map<String, Object>> getPostRevisions(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); revisionsController.get(id, req, model); Post post = (Post) model.getAttribute("showPost"); if (post == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return ((List<Revision>) model.getAttribute("revisionslist")).stream().map(r -> { Map<String, Object> rev = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(r, false)); rev.put("author", r.getAuthor()); return rev; }).collect(Collectors.toList()); } @PostMapping("/users") public Map<String, Object> createUser(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } Map<String, Object> userEntity = new HashMap<>(); userEntity.put(Config._TYPE, Utils.type(User.class)); userEntity.put(Config._NAME, entity.get(Config._NAME)); userEntity.put(Config._EMAIL, entity.get(Config._EMAIL)); userEntity.put(Config._IDENTIFIER, entity.get(Config._IDENTIFIER)); userEntity.put(Config._GROUPS, entity.get(Config._GROUPS)); userEntity.put("active", entity.getOrDefault("active", true)); userEntity.put("picture", entity.get("picture")); User newUser = ParaObjectUtils.setAnnotatedFields(new User(), userEntity, null); newUser.setPassword((String) entity.get(Config._PASSWORD)); newUser.setIdentifier(StringUtils.isBlank(newUser.getIdentifier()) ? newUser.getEmail() : newUser.getIdentifier()); String[] errors = ValidationUtils.validateObject(newUser); if (errors.length == 0) { // generic and password providers are identical but this was fixed in Para 1.37.1 (backwards compatibility) String provider = "generic".equals(newUser.getIdentityProvider()) ? "password" : newUser.getIdentityProvider(); User createdUser = pc.signIn(provider, newUser.getIdentifier() + Config.SEPARATOR + newUser.getName() + Config.SEPARATOR + newUser.getPassword(), false); // user is probably active:false so activate them List<User> created = pc.findQuery(newUser.getType(), Config._EMAIL + ":" + newUser.getEmail()); if (createdUser == null && !created.isEmpty()) { createdUser = created.iterator().next(); if (Utils.timestamp() - createdUser.getTimestamp() > TimeUnit.SECONDS.toMillis(20)) { createdUser = null; // user existed previously } else if (newUser.getActive() && !createdUser.getActive()) { createdUser.setActive(true); pc.update(createdUser); } } if (createdUser == null) { badReq("Failed to create user. User may already exist."); } else { Profile profile = Profile.fromUser(createdUser); profile.getSpaces().addAll(readSpaces(((List<String>) entity.getOrDefault("spaces", Collections.emptyList())).toArray(new String[0]))); res.setStatus(HttpStatus.CREATED.value()); pc.create(profile); Map<String, Object> payload = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(profile, false)); payload.put("user", createdUser); utils.triggerHookEvent("user.signup", payload); logger.info("Created new user through API '{}' with id={}, groups={}, spaces={}.", createdUser.getName(), profile.getId(), profile.getGroups(), profile.getSpaces()); Map<String, Object> result = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(profile, false)); result.put("user", createdUser); return result; } } badReq("Failed to create user - " + String.join("; ", errors)); return null; } @GetMapping("/users") public List<Profile> listUsers(@RequestParam(required = false, defaultValue = Config._TIMESTAMP) String sortby, @RequestParam(required = false, defaultValue = "*") String q, HttpServletRequest req) { Model model = new ExtendedModelMap(); peopleController.get(sortby, q, req, model); return (List<Profile>) model.getAttribute("userlist"); } @GetMapping("/users/{id}") public Map<String, Object> getUser(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { List<?> usrProfile = pc.readAll(Arrays.asList(StringUtils.substringBefore(id, Config.SEPARATOR), Profile.id(id))); Iterator<?> it = usrProfile.iterator(); User u = it.hasNext() ? (User) it.next() : null; Profile p = it.hasNext() ? (Profile) it.next() : null; if (p == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } Map<String, Object> result = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(p, false)); result.put("user", u); return result; } @PatchMapping("/users/{id}") public Profile updateUser(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String name = (String) entity.get("name"); String location = (String) entity.get("location"); String latlng = (String) entity.get("latlng"); String website = (String) entity.get("website"); String aboutme = (String) entity.get("aboutme"); String picture = (String) entity.get("picture"); Model model = new ExtendedModelMap(); profileController.edit(id, name, location, latlng, website, aboutme, picture, req, model); Profile profile = (Profile) model.getAttribute("user"); if (profile == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } if (entity.containsKey("spaces")) { profile.setSpaces(new HashSet<>(readSpaces(((List<String>) entity.getOrDefault("spaces", Collections.emptyList())).toArray(new String[0])))); pc.update(profile); } return profile; } @DeleteMapping("/users/{id}") public void deleteUser(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Profile profile = pc.read(Profile.id(id)); if (profile == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return; } profile.delete(); } @GetMapping("/users/{id}/questions") public List<? extends Post> getUserQuestions(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Profile p = pc.read(Profile.id(id)); if (p == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return profileController.getQuestions(utils.getAuthUser(req), p, true, utils.pagerFromParams(req)); } @GetMapping("/users/{id}/replies") public List<? extends Post> getUserReplies(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Profile p = pc.read(Profile.id(id)); if (p == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return profileController.getAnswers(utils.getAuthUser(req), p, true, utils.pagerFromParams(req)); } @GetMapping("/users/{id}/favorites") public List<? extends Post> getUserFavorites(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { badReq("Not supported"); return null; } @PutMapping("/users/{id}/moderator") public void makeUserMod(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { profileController.makeMod(id, req, res); } @PutMapping("/users/{id}/ban") public void banUser(@PathVariable String id, @RequestParam(defaultValue = "0") String banuntil, HttpServletRequest req, HttpServletResponse res) { badReq("Not supported"); } @PutMapping("/users/spaces") public void bulkEditSpaces(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } Set<String> selectedUsers = ((List<String>) entity.getOrDefault("users", Collections.emptyList())).stream().map(id -> Profile.id(id)).collect(Collectors.toSet()); Set<String> selectedSpaces = ((List<String>) entity.getOrDefault("spaces", Collections.emptyList())).stream().distinct().collect(Collectors.toSet()); peopleController.bulkEdit(selectedUsers.toArray(new String[0]), readSpaces(selectedSpaces).toArray(new String[0]), req); } @PostMapping("/tags") public Tag createTag(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } if (pc.read(new Tag((String) entity.get("tag")).getId()) != null) { badReq("Tag already exists."); } Tag newTag = ParaObjectUtils.setAnnotatedFields(new Tag((String) entity.get("tag")), entity, null); String[] errors = ValidationUtils.validateObject(newTag); if (errors.length == 0) { res.setStatus(HttpStatus.CREATED.value()); return pc.create(newTag); } badReq("Failed to create tag - " + String.join("; ", errors)); return null; } @GetMapping("/tags") public List<Tag> listTags(@RequestParam(required = false, defaultValue = "count") String sortby, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); tagsController.get(sortby, req, model); return (List<Tag>) model.getAttribute("tagslist"); } @GetMapping("/tags/{id}") public Tag getTag(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Tag tag = pc.read(new Tag(id).getId()); if (tag == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return tag; } @PatchMapping("/tags/{id}") public Tag updateTag(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } Model model = new ExtendedModelMap(); tagsController.rename(id, (String) entity.get("tag"), req, res, model); if (!model.containsAttribute("tag")) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return (Tag) model.getAttribute("tag"); } @DeleteMapping("/tags/{id}") public void deleteTag(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { pc.delete(new Tag(id)); } @GetMapping("/tags/{id}/questions") public List<Map<String, Object>> listTaggedQuestions(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); questionsController.getTagged(new Tag(id).getTag(), req, model); return ((List<Post>) model.getAttribute("questionslist")).stream().map(p -> { Map<String, Object> post = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(p, false)); post.put("author", p.getAuthor()); return post; }).collect(Collectors.toList()); } @PostMapping("/comments") public Comment createComment(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String comment = (String) entity.get("comment"); String parentid = (String) entity.get(Config._PARENTID); String creatorid = (String) entity.get(Config._CREATORID); ParaObject parent = pc.read(parentid); if (parent == null) { badReq("Parent object not found. Provide a valid parentid."); return null; } if (!StringUtils.isBlank(creatorid)) { Profile authUser = pc.read(Profile.id(creatorid)); if (authUser != null) { req.setAttribute(AUTH_USER_ATTRIBUTE, authUser); } } Model model = new ExtendedModelMap(); commentController.createAjax(comment, parentid, req, model); Comment created = (Comment) model.getAttribute("showComment"); if (created == null || StringUtils.isBlank(comment)) { badReq("Failed to create comment."); return null; } res.setStatus(HttpStatus.CREATED.value()); return created; } @GetMapping("/comments/{id}") public Comment getComment(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Comment comment = pc.read(id); if (comment == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return comment; } @DeleteMapping("/comments/{id}") public void deleteComment(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { commentController.deleteAjax(id, req, res); } @PostMapping("/reports") public Report createReport(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String creatorid = (String) entity.get(Config._CREATORID); if (!StringUtils.isBlank(creatorid)) { Profile authUser = pc.read(Profile.id(creatorid)); if (authUser != null) { req.setAttribute(AUTH_USER_ATTRIBUTE, authUser); } } Model model = new ExtendedModelMap(); reportsController.create(req, res, model); checkForErrorsAndThrow(model); Report newreport = (Report) model.getAttribute("newreport"); res.setStatus(HttpStatus.CREATED.value()); return newreport; } @GetMapping("/reports") public List<Report> listReports(@RequestParam(required = false, defaultValue = "count") String sortby, HttpServletRequest req, HttpServletResponse res) { Model model = new ExtendedModelMap(); reportsController.get(sortby, req, model); return (List<Report>) model.getAttribute("reportslist"); } @GetMapping("/reports/{id}") public Report getReport(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Report report = pc.read(id); if (report == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return report; } @DeleteMapping("/reports/{id}") public void deleteReport(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { reportsController.delete(id, req, res); } @PutMapping("/reports/{id}/close") public void closeReport(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); String solution = (String) entity.getOrDefault("solution", "Closed via API."); reportsController.close(id, solution, req, res); } @PostMapping("/spaces") public Sysprop createSpace(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String name = (String) entity.get(Config._NAME); if (StringUtils.isBlank(name)) { badReq("Property 'name' cannot be blank."); return null; } if (pc.read(utils.getSpaceId(name)) != null) { badReq("Space already exists."); return null; } Sysprop s = utils.buildSpaceObject(name); res.setStatus(HttpStatus.CREATED.value()); return pc.create(s); } @GetMapping("/spaces") public List<Sysprop> listSpaces(HttpServletRequest req, HttpServletResponse res) { return pc.findQuery("scooldspace", "*", utils.pagerFromParams(req)); } @GetMapping("/spaces/{id}") public Sysprop getSpace(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Sysprop space = pc.read(utils.getSpaceId(id)); if (space == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return space; } @PostMapping("/webhooks") public Webhook createWebhook(HttpServletRequest req, HttpServletResponse res) { Map<String, Object> entity = readEntity(req); if (entity.isEmpty()) { badReq("Missing request body."); } String targetUrl = (String) entity.get("targetUrl"); if (!Utils.isValidURL(targetUrl)) { badReq("Property 'targetUrl' must be a valid URL."); return null; } Webhook webhook = pc.create(ParaObjectUtils.setAnnotatedFields(new Webhook(), entity, null)); if (webhook == null) { badReq("Failed to create webhook."); return null; } res.setStatus(HttpStatus.CREATED.value()); return webhook; } @GetMapping("/webhooks") public List<Webhook> listWebhooks(HttpServletRequest req, HttpServletResponse res) { return pc.findQuery(Utils.type(Webhook.class), "*", utils.pagerFromParams(req)); } @GetMapping("/webhooks/{id}") public Webhook getWebhook(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Webhook webhook = pc.read(Utils.type(Webhook.class), id); if (webhook == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } return webhook; } @PatchMapping("/webhooks/{id}") public Webhook updateWebhook(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Webhook webhook = pc.read(id); if (webhook == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return null; } Map<String, Object> entity = readEntity(req); return pc.update(ParaObjectUtils.setAnnotatedFields(webhook, entity, Locked.class)); } @DeleteMapping("/webhooks/{id}") public void deleteWebhook(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { Webhook webhook = pc.read(id); if (webhook == null) { res.setStatus(HttpStatus.NOT_FOUND.value()); return; } pc.delete(webhook); } @GetMapping("/events") public Set<String> listHookEvents(HttpServletRequest req, HttpServletResponse res) { return utils.getCustomHookEvents(); } @GetMapping("/types") public Set<String> listCoreTypes(HttpServletRequest req, HttpServletResponse res) { return utils.getCoreScooldTypes(); } @GetMapping("/search/{type}/{query}") public Map<String, Object> search(@PathVariable String type, @PathVariable String query, HttpServletRequest req) { if ("answer".equals(type)) { type = Utils.type(Reply.class); } Pager pager = utils.pagerFromParams(req); Map<String, Object> result = new HashMap<>(); result.put("items", pc.findQuery(type, query, pager)); result.put("page", pager.getPage()); result.put("totalHits", pager.getCount()); if (!StringUtils.isBlank(pager.getLastKey())) { result.put("lastKey", pager.getLastKey()); } return result; } @GetMapping("/stats") public Map<String, Object> stats(HttpServletRequest req) { Map<String, Object> stats = new LinkedHashMap<>(); long qcount = 0L; long acount = 0L; long scount = 0L; long ucount = 0L; long tcount = 0L; long rcount = 0L; long ccount = 0L; long recount = 0L; long uqcount = 0L; long uacount = 0L; String paraVer = null; try { qcount = pc.getCount(Utils.type(Question.class)); acount = pc.getCount(Utils.type(Reply.class)); scount = pc.getCount("scooldspace"); ucount = pc.getCount(Utils.type(Profile.class)); tcount = pc.getCount(Utils.type(Tag.class)); rcount = pc.getCount(Utils.type(Report.class)); ccount = pc.getCount(Utils.type(Comment.class)); recount = pc.getCount(Utils.type(Revision.class)); uqcount = pc.getCount(Utils.type(UnapprovedQuestion.class)); uacount = pc.getCount(Utils.type(UnapprovedReply.class)); paraVer = pc.getServerVersion(); } catch (Exception e) { } stats.put("questions", qcount); stats.put("replies", acount); stats.put("spaces", scount); stats.put("users", ucount); stats.put("tags", tcount); stats.put("reports", rcount); stats.put("comments", ccount); stats.put("revisions", recount); stats.put("unapproved_questions", uqcount); stats.put("unapproved_replies", uacount); stats.put("para_version", Optional.ofNullable(paraVer).orElse("unknown")); stats.put("scoold_version", Optional.ofNullable(getClass().getPackage().getImplementationVersion()). orElse("unknown")); return stats; } @DeleteMapping("/spaces/{id}") public void deleteSpace(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) { pc.delete(new Sysprop(utils.getSpaceId(id))); } private boolean voteRequest(boolean isUpvote, String id, String userid, HttpServletRequest req) { if (!StringUtils.isBlank(userid)) { Profile authUser = pc.read(Profile.id(userid)); if (authUser != null) { req.setAttribute(AUTH_USER_ATTRIBUTE, authUser); } } ParaObject votable = pc.read(id); return votable != null && (isUpvote ? voteController.voteup(id, votable.getType(), req) : voteController.votedown(id, votable.getType(), req)); } private List<String> readSpaces(Collection<String> spaces) { if (spaces == null || spaces.isEmpty()) { return Collections.emptyList(); } List<String> ids = spaces.stream().map(s -> utils.getSpaceId(s)). filter(s -> !s.isEmpty() && !utils.isDefaultSpace(s)).distinct().collect(Collectors.toList()); List<Sysprop> existing = pc.readAll(ids); return existing.stream().map(s -> s.getId() + Config.SEPARATOR + s.getName()).collect(Collectors.toList()); } private List<String> readSpaces(String... spaces) { return readSpaces(Arrays.asList(spaces)); } @ExceptionHandler({Exception.class}) public Map<String, Object> handleException(Exception ex, WebRequest request, HttpServletResponse res) { Map<String, Object> error = new HashMap<>(2); int code = 500; if (ex instanceof BadRequestException) { code = 400; } res.setStatus(code); error.put("code", code); error.put("message", ex.getMessage()); return error; } private Map<String, Object> readEntity(HttpServletRequest req) { try { Map<String, Object> entity = ParaObjectUtils.getJsonReader(Map.class).readValue(req.getInputStream()); req.setAttribute(REST_ENTITY_ATTRIBUTE, entity); return entity; } catch (IOException ex) { badReq("Missing request body."); } catch (Exception ex) { logger.error(null, ex); } return Collections.emptyMap(); } private void checkForErrorsAndThrow(Model model) { if (model != null && model.containsAttribute("error")) { Object err = model.getAttribute("error"); if (err instanceof String) { badReq((String) err); } else if (err instanceof Map) { Map<String, String> error = (Map<String, String>) err; badReq(error.entrySet().stream().map(e -> "'" + e.getKey() + "' " + e.getValue()).collect(Collectors.joining("; "))); } } } private void badReq(String error) { if (!StringUtils.isBlank(error)) { throw new BadRequestException(error); } } }