/* * 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.core; import com.erudika.para.core.Tag; import com.erudika.para.annotations.Stored; import com.erudika.para.client.ParaClient; import com.erudika.para.core.ParaObject; import com.erudika.para.core.Sysprop; import com.erudika.para.core.User; import com.erudika.para.utils.Config; import com.erudika.para.utils.Pager; import com.erudika.para.utils.Utils; import com.erudika.scoold.ScooldServer; import com.erudika.scoold.utils.ScooldUtils; import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.validation.constraints.Size; import org.apache.commons.lang3.StringUtils; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; /** * * @author Alex Bogdanovski [[email protected]] */ public abstract class Post extends Sysprop { private static final long serialVersionUID = 1L; public static final String DEFAULT_SPACE = "scooldspace:default"; public static final String ALL_MY_SPACES = "scooldspace:*"; @Stored private String body; @Stored @NotBlank @Size(min = 2, max = 255) private String title; @Stored @NotEmpty @Size(min = 1) private List<String> tags; @Stored private Long viewcount; @Stored private String answerid; @Stored private String revisionid; @Stored private String closerid; @Stored private Long answercount; @Stored private Long lastactivity; @Stored private Long lastedited; @Stored private String lasteditby; @Stored private String deletereportid; @Stored private String location; @Stored private String address; @Stored private String latlng; @Stored private List<String> commentIds; @Stored private String space; @Stored private Map<String, String> followers; private transient Profile author; private transient Profile lastEditor; private transient List<Comment> comments; private transient Pager itemcount; public Post() { this.answercount = 0L; this.viewcount = 0L; } private ParaClient client() { return ScooldUtils.getInstance().getParaClient(); } public Long getLastactivity() { if (lastactivity == null || lastactivity <= 0) { lastactivity = getUpdated(); } return lastactivity; } public void setLastactivity(Long lastactivity) { this.lastactivity = lastactivity; } public Long getLastedited() { if (lastedited == null || lastedited <= 0) { lastedited = getUpdated(); } return lastedited; } public void setLastedited(Long lastedited) { this.lastedited = lastedited; } @JsonIgnore public Pager getItemcount() { if (itemcount == null) { itemcount = new Pager(5); itemcount.setDesc(false); } return itemcount; } public void setItemcount(Pager itemcount) { this.itemcount = itemcount; } public Map<String, String> getFollowers() { return followers; } public void setFollowers(Map<String, String> followers) { this.followers = followers; } public String getDeletereportid() { return deletereportid; } public void setDeletereportid(String deletereportid) { this.deletereportid = deletereportid; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getLatlng() { return latlng; } public void setLatlng(String latlng) { this.latlng = latlng; } public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; } public String getLasteditby() { return lasteditby; } public void setLasteditby(String lasteditby) { this.lasteditby = lasteditby; } public Long getAnswercount() { return answercount; } public void setAnswercount(Long answercount) { this.answercount = answercount; } public String getCloserid() { return closerid; } public void setCloserid(String closed) { this.closerid = closed; } public String getRevisionid() { return revisionid; } public void setRevisionid(String revisionid) { this.revisionid = revisionid; } public String getAnswerid() { return answerid; } public void setAnswerid(String answerid) { this.answerid = answerid; } public Long getViewcount() { return viewcount; } public void setViewcount(Long viewcount) { this.viewcount = viewcount; } public String getTitle() { if (StringUtils.isBlank(title)) { return getId(); } return title; } public void setTitle(String title) { if (!StringUtils.isBlank(title)) { this.title = StringUtils.trimToEmpty(title); setName(title); } } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public boolean isClosed() { return !StringUtils.isBlank(this.closerid); } public String getTagsString() { if (getTags() == null || getTags().isEmpty()) { return ""; } Collections.sort(getTags()); return StringUtils.join(getTags(), ","); } public String create() { updateTags(null, getTags()); this.body = Utils.abbreviate(this.body, Config.getConfigInt("max_post_length", 20000)); Post p = client().create(this); if (p != null) { setRevisionid(Revision.createRevisionFromPost(p, true).create()); setId(p.getId()); setTimestamp(p.getTimestamp()); return p.getId(); } return null; } public void update() { setRevisionid(Revision.createRevisionFromPost(this, false).create()); client().update(this); } public void delete() { // delete post ArrayList<ParaObject> children = new ArrayList<ParaObject>(); ArrayList<String> ids = new ArrayList<String>(); // delete Comments children.addAll(client().getChildren(this, Utils.type(Comment.class))); // delete Revisions children.addAll(client().getChildren(this, Utils.type(Revision.class))); for (ParaObject reply : client().getChildren(this, Utils.type(Reply.class))) { // delete answer children.add(reply); // delete Comments children.addAll(client().getChildren(reply, Utils.type(Comment.class))); // delete Revisions children.addAll(client().getChildren(reply, Utils.type(Revision.class))); } for (ParaObject child : children) { ids.add(child.getId()); } updateTags(getTags(), null); client().deleteAll(ids); client().delete(this); } public static String getTagString(String tag) { if (StringUtils.isBlank(tag)) { return ""; } String s = tag.replaceAll("[\\p{S}\\p{P}\\p{C}&&[^+\\.]]", " ").replaceAll("\\p{Z}+", " ").trim(); return StringUtils.truncate(Utils.noSpaces(s, "-"), 35); } public void updateTags(List<String> oldTags, List<String> newTags) { List<String> deleteUs = new LinkedList<>(); List<Tag> updateUs = new LinkedList<>(); Map<String, Tag> oldTagz = Optional.ofNullable(oldTags).orElse(Collections.emptyList()).stream(). map(t -> new Tag(getTagString(t))).collect(Collectors.toMap(t -> t.getId(), t -> t)); Map<String, Tag> newTagz = Optional.ofNullable(newTags).orElse(Collections.emptyList()).stream(). map(t -> new Tag(getTagString(t))).collect(Collectors.toMap(t -> t.getId(), t -> t)); Map<String, Tag> existingTagz = client().readAll(Stream.concat(oldTagz.keySet().stream(), newTagz.keySet(). stream()).distinct().collect(Collectors.toList())). stream().collect(Collectors.toMap(t -> t.getId(), t -> (Tag) t)); // add newly created tags client().createAll(newTagz.values().stream().filter(t -> { t.setCount(1); return !existingTagz.containsKey(t.getId()); }).collect(Collectors.toList())); // increment or decrement the count of the rest existingTagz.values().forEach(t -> { if (!oldTagz.containsKey(t.getId()) && newTagz.containsKey(t.getId())) { t.setCount(t.getCount() + 1); updateUs.add(t); } else if (oldTagz.containsKey(t.getId()) && (newTags == null || !newTagz.containsKey(t.getId()))) { t.setCount(t.getCount() - 1); if (t.getCount() <= 0) { // check if actual count is different int c = client().getCount(Utils.type(Question.class), Collections.singletonMap(Config._TAGS, t.getTag())).intValue(); if (c <= 1) { deleteUs.add(t.getId()); } else { t.setCount(c); } } else { updateUs.add(t); } } // else: count remains unchanged }); client().updateAll(updateUs); client().deleteAll(deleteUs); int tagsLimit = Math.min(ScooldServer.MAX_TAGS_PER_POST, 100); setTags(newTagz.values().stream().limit(tagsLimit).map(t -> t.getTag()).collect(Collectors.toList())); } @JsonIgnore public Profile getAuthor() { return author; } public void setAuthor(Profile author) { this.author = author; } @JsonIgnore public Profile getLastEditor() { return lastEditor; } public void setLastEditor(Profile lastEditor) { this.lastEditor = lastEditor; } public void setComments(List<Comment> comments) { this.comments = comments; } @JsonIgnore // DO NOT REMOVE! clashes with User.getComments() field in index public List<Comment> getComments() { return this.comments; } public List<String> getCommentIds() { return commentIds; } public String getSpace() { if (StringUtils.isBlank(space)) { space = DEFAULT_SPACE; } return space; } public void setSpace(String space) { this.space = space; } public void setCommentIds(List<String> commentIds) { this.commentIds = commentIds; } public void addCommentId(String id) { if (getCommentIds() != null && getCommentIds().size() < getItemcount().getLimit()) { getCommentIds().add(id); } } @JsonIgnore public List<Reply> getAnswers(Pager pager) { return getAnswers(Reply.class, pager); } @JsonIgnore public List<Reply> getUnapprovedAnswers(Pager pager) { if (isReply()) { return Collections.emptyList(); } return client().getChildren(this, Utils.type(UnapprovedReply.class), pager); } private List<Reply> getAnswers(Class<? extends Reply> type, Pager pager) { if (isReply()) { return Collections.emptyList(); } List<Reply> answers = client().getChildren(this, Utils.type(type), pager); // we try to find the accepted answer inside the answers list, in not there, read it from db if (pager.getPage() < 2 && !StringUtils.isBlank(getAnswerid())) { Reply acceptedAnswer = null; for (Iterator<Reply> iterator = answers.iterator(); iterator.hasNext();) { Reply answer = iterator.next(); if (getAnswerid().equals(answer.getId())) { acceptedAnswer = answer; iterator.remove(); break; } } if (acceptedAnswer == null) { acceptedAnswer = client().read(getAnswerid()); } if (acceptedAnswer != null) { ArrayList<Reply> sortedAnswers = new ArrayList<Reply>(answers.size() + 1); if (pager.isDesc()) { sortedAnswers.add(acceptedAnswer); sortedAnswers.addAll(answers); } else { sortedAnswers.addAll(answers); sortedAnswers.add(acceptedAnswer); } return sortedAnswers; } } return answers; } @JsonIgnore public List<Revision> getRevisions(Pager pager) { return client().getChildren(this, Utils.type(Revision.class), pager); } @JsonIgnore public boolean isReply() { return this instanceof Reply; } @JsonIgnore public boolean isQuestion() { return this instanceof Question; } @JsonIgnore public boolean isFeedback() { return this instanceof Feedback; } public String getPostLink(boolean plural, boolean noid) { Post p = this; String ptitle = Utils.noSpaces(Utils.stripAndTrim(p.getTitle()), "-"); String pid = (noid ? "" : "/" + p.getId() + "/" + ptitle); String ctx = ScooldServer.CONTEXT_PATH; if (p.isQuestion()) { return ctx + (plural ? ScooldServer.QUESTIONSLINK : ScooldServer.QUESTIONLINK + pid); } else if (p.isFeedback()) { return ctx + ScooldServer.FEEDBACKLINK + (plural ? "" : pid); } else if (p.isReply()) { return ctx + ScooldServer.QUESTIONLINK + (noid ? "" : "/" + p.getParentid()); } return ""; } public void restoreRevisionAndUpdate(String revisionid) { Revision rev = client().read(revisionid); if (rev != null) { //copy rev data to post setTitle(rev.getTitle()); setBody(rev.getBody()); setTags(rev.getTags()); setRevisionid(rev.getId()); setLastactivity(System.currentTimeMillis()); //update post without creating a new revision client().update(this); ScooldUtils.getInstance().triggerHookEvent("revision.restore", rev); } } public void addFollower(User user) { if (followers == null) { followers = new LinkedHashMap<String, String>(); } if (user != null && !StringUtils.isBlank(user.getEmail())) { followers.put(user.getId(), user.getEmail()); } } public void removeFollower(User user) { if (followers != null && user != null) { followers.remove(user.getId()); } } public boolean hasFollowers() { return (followers != null && !followers.isEmpty()); } public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } return Objects.equals(getTitle(), ((Post) obj).getTitle()) && Objects.equals(getBody(), ((Post) obj).getBody()) && Objects.equals(getSpace(), ((Post) obj).getSpace()) && Objects.equals(getTags(), ((Post) obj).getTags()); } public int hashCode() { return Objects.hashCode(getTitle()) + Objects.hashCode(getBody()) + Objects.hashCode(getTags()); } }