package scrum.server; import ilarkesto.auth.Auth; import ilarkesto.auth.WrongPasswordException; import ilarkesto.base.PermissionDeniedException; import ilarkesto.base.Reflect; import ilarkesto.base.Utl; import ilarkesto.base.time.Date; import ilarkesto.base.time.DateAndTime; import ilarkesto.core.logging.Log; import ilarkesto.gwt.server.AGwtConversation; import ilarkesto.persistence.ADao; import ilarkesto.persistence.AEntity; import ilarkesto.persistence.EntityDoesNotExistException; import ilarkesto.webapp.AWebApplication; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import scrum.client.DataTransferObject; import scrum.client.admin.SystemMessage; import scrum.server.admin.SystemConfig; import scrum.server.admin.User; import scrum.server.admin.UserDao; import scrum.server.collaboration.ChatMessage; import scrum.server.collaboration.Comment; import scrum.server.collaboration.CommentDao; import scrum.server.collaboration.EmoticonDao; import scrum.server.collaboration.Subject; import scrum.server.collaboration.Wikipage; import scrum.server.common.Numbered; import scrum.server.common.Transient; import scrum.server.files.File; import scrum.server.impediments.Impediment; import scrum.server.issues.Issue; import scrum.server.issues.IssueDao; import scrum.server.journal.Change; import scrum.server.journal.ChangeDao; import scrum.server.journal.ProjectEvent; import scrum.server.journal.ProjectEventDao; import scrum.server.pr.BlogEntry; import scrum.server.pr.BlogEntryDao; import scrum.server.project.Project; import scrum.server.project.ProjectDao; import scrum.server.project.Requirement; import scrum.server.project.RequirementDao; import scrum.server.release.Release; import scrum.server.release.ReleaseDao; import scrum.server.risks.Risk; import scrum.server.sprint.Sprint; import scrum.server.sprint.Task; public class ScrumServiceImpl extends GScrumServiceImpl { private static final Log LOG = Log.get(ScrumServiceImpl.class); private static final long serialVersionUID = 1; // --- dependencies --- private transient ProjectDao projectDao; private transient UserDao userDao; private transient RequirementDao requirementDao; private transient IssueDao issueDao; private transient ReleaseDao releaseDao; private transient CommentDao commentDao; private transient ScrumWebApplication webApplication; private transient ProjectEventDao projectEventDao; private transient EmoticonDao emoticonDao; private transient ChangeDao changeDao; private transient BlogEntryDao blogEntryDao; public void setReleaseDao(ReleaseDao releaseDao) { this.releaseDao = releaseDao; } public void setChangeDao(ChangeDao changeDao) { this.changeDao = changeDao; } public void setEmoticonDao(EmoticonDao emoticonDao) { this.emoticonDao = emoticonDao; } public void setProjectEventDao(ProjectEventDao projectEventDao) { this.projectEventDao = projectEventDao; } public void setWebApplication(ScrumWebApplication webApplication) { this.webApplication = webApplication; } public void setRequirementDao(RequirementDao requirementDao) { this.requirementDao = requirementDao; } public void setIssueDao(IssueDao issueDao) { this.issueDao = issueDao; } public void setProjectDao(ProjectDao projectDao) { this.projectDao = projectDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void setCommentDao(CommentDao commentDao) { this.commentDao = commentDao; } public void setBlogEntryDao(BlogEntryDao blogEntryDao) { this.blogEntryDao = blogEntryDao; } // --- --- private void onStartConversation(GwtConversation conversation) { User user = conversation.getSession().getUser(); if (user == null) throw new PermissionDeniedException("Login required."); conversation.clearRemoteEntities(); conversation.getNextData().applicationInfo = webApplication.getApplicationInfo(); conversation.sendToClient(webApplication.getSystemConfig()); conversation.getNextData().setUserId(user.getId()); conversation.sendUserScopeDataToClient(user); } @Override public void onSendIssueReplyEmail(GwtConversation conversation, String issueId, String from, String to, String subject, String text) { assertProjectSelected(conversation); Issue issue = issueDao.getById(issueId); webApplication.sendEmail(from, to, subject, text); User user = conversation.getSession().getUser(); postProjectEvent(conversation, user.getName() + " emailed a response to " + issue.getReferenceAndLabel(), issue); Change change = changeDao.postChange(issue, user, "@reply", null, text); conversation.sendToClient(change); } @Override public void onCreateExampleProject(GwtConversation conversation) { User user = conversation.getSession().getUser(); Project project = projectDao.postExampleProject(user, user, user); conversation.sendToClient(project); } @Override public void onSearch(GwtConversation conversation, String text) { Project project = conversation.getProject(); if (project == null) return; List<AEntity> foundEntities = project.search(text); LOG.debug("Found entities for search", "\"" + text + "\"", "->", foundEntities); conversation.sendToClient(foundEntities); } @Override public void onUpdateSystemMessage(GwtConversation conversation, SystemMessage systemMessage) { User user = conversation.getSession().getUser(); if (user == null || user.isAdmin() == false) throw new PermissionDeniedException(); webApplication.updateSystemMessage(systemMessage); } @Override public void onSetSelectedEntitysIds(GwtConversation conversation, Set ids) { Project project = conversation.getProject(); if (project == null) return; webApplication.setUsersSelectedEntities(project, conversation, ids); } @Override public void onLogout(GwtConversation conversation) { WebSession session = conversation.getSession(); webApplication.destroyWebSession(session, getThreadLocalRequest().getSession()); } @Override public void onResetPassword(GwtConversation conversation, String userId) { if (!conversation.getSession().getUser().isAdmin()) throw new PermissionDeniedException(); User user = userDao.getById(userId); if (user.isEmailSet()) { user.triggerEmailVerification(); user.triggerPasswordReset(); } else { user.setPassword(webApplication.getSystemConfig().getDefaultUserPassword()); } } @Override public void onChangePassword(GwtConversation conversation, String oldPassword, String newPassword) { User user = conversation.getSession().getUser(); if (user.matchesPassword(oldPassword) == false) throw new WrongPasswordException(); user.setPassword(newPassword); LOG.info("password changed by user"); } @Override public void onCreateEntity(GwtConversation conversation, String type, Map properties) { String id = (String) properties.get("id"); if (id == null) throw new NullPointerException("id == null"); ADao dao = getDaoService().getDaoByName(type); AEntity entity = dao.newEntityInstance(id); entity.updateProperties(properties); User currentUser = conversation.getSession().getUser(); if (entity instanceof Numbered) { ((Numbered) entity).updateNumber(); } if (entity instanceof Project) { Project project = (Project) entity; project.addParticipant(currentUser); project.addAdmin(currentUser); project.addProductOwner(currentUser); project.addScrumMaster(currentUser); project.addTeamMember(currentUser); } if (entity instanceof Comment) { Comment comment = (Comment) entity; comment.setDateAndTime(DateAndTime.now()); postProjectEvent(conversation, comment.getAuthor().getName() + " commented on " + comment.getParent(), comment.getParent()); conversation.getProject().updateHomepage(comment.getParent(), true); } if (entity instanceof ChatMessage) { ChatMessage chatMessage = (ChatMessage) entity; chatMessage.setDateAndTime(DateAndTime.now()); } if (entity instanceof Impediment) { Impediment impediment = (Impediment) entity; impediment.setDate(Date.today()); } if (entity instanceof Issue) { Issue issue = (Issue) entity; issue.setDate(DateAndTime.now()); issue.setCreator(currentUser); } if (entity instanceof Task) { Task task = (Task) entity; Requirement requirement = task.getRequirement(); requirement.setRejectDate(null); requirement.setClosed(false); sendToClients(conversation, requirement); } if (entity instanceof BlogEntry) { BlogEntry blogEntry = (BlogEntry) entity; blogEntry.setDateAndTime(DateAndTime.now()); blogEntry.addAuthor(currentUser); } if (!(entity instanceof Transient)) dao.saveEntity(entity); sendToClients(conversation, entity); if (entity instanceof Task || entity instanceof Requirement || entity instanceof Wikipage || entity instanceof Risk || entity instanceof Impediment || entity instanceof Issue || entity instanceof BlogEntry) { User user = currentUser; Change change = changeDao.postChange(entity, user, "@created", null, null); conversation.sendToClient(change); } } @Override public void onDeleteEntity(GwtConversation conversation, String entityId) { AEntity entity = getDaoService().getEntityById(entityId); if (!Auth.isDeletable(entity, conversation.getSession().getUser())) throw new PermissionDeniedException(); if (entity instanceof File) { File file = (File) entity; file.deleteFile(); } ADao dao = getDaoService().getDao(entity); dao.deleteEntity(entity); Project project = conversation.getProject(); if (project != null) { for (GwtConversation c : webApplication.getConversationsByProject(project, conversation)) { c.getNextData().addDeletedEntity(entityId); } } } @Override public void onChangeProperties(GwtConversation conversation, String entityId, Map properties) { AEntity entity = getDaoService().getEntityById(entityId); User currentUser = conversation.getSession().getUser(); if (!Auth.isEditable(entity, currentUser)) throw new PermissionDeniedException(); if (entity instanceof Task) { // update sprint day snapshot before change Task task = (Task) entity; task.getRequirement().getSprint().getDaySnapshot(Date.today()).updateWithCurrentSprint(); } Sprint previousRequirementSprint = null; if (entity instanceof Requirement) { Requirement requirement = (Requirement) entity; previousRequirementSprint = requirement.getSprint(); postChangeIfChanged(conversation, entity, properties, currentUser, "description"); postChangeIfChanged(conversation, entity, properties, currentUser, "testDescription"); postChangeIfChanged(conversation, entity, properties, currentUser, "sprintId"); postChangeIfChanged(conversation, entity, properties, currentUser, "closed"); postChangeIfChanged(conversation, entity, properties, currentUser, "issueId"); } if (entity instanceof Wikipage) { postChangeIfChanged(conversation, entity, properties, currentUser, "text"); } if (entity instanceof Risk) { postChangeIfChanged(conversation, entity, properties, currentUser, "description"); postChangeIfChanged(conversation, entity, properties, currentUser, "probability"); postChangeIfChanged(conversation, entity, properties, currentUser, "impact"); postChangeIfChanged(conversation, entity, properties, currentUser, "probabilityMitigation"); postChangeIfChanged(conversation, entity, properties, currentUser, "impactMitigation"); } if (entity instanceof Impediment) { postChangeIfChanged(conversation, entity, properties, currentUser, "description"); postChangeIfChanged(conversation, entity, properties, currentUser, "solution"); postChangeIfChanged(conversation, entity, properties, currentUser, "closed"); } if (entity instanceof Issue) { postChangeIfChanged(conversation, entity, properties, currentUser, "description"); postChangeIfChanged(conversation, entity, properties, currentUser, "statement"); postChangeIfChanged(conversation, entity, properties, currentUser, "closeDate"); postChangeIfChanged(conversation, entity, properties, currentUser, "storyId"); } if (entity instanceof BlogEntry) { postChangeIfChanged(conversation, entity, properties, currentUser, "text"); } entity.updateProperties(properties); if (entity instanceof Task) { // update sprint day snapshot after change Task task = (Task) entity; Requirement requirement = task.getRequirement(); requirement.getSprint().getDaySnapshot(Date.today()).updateWithCurrentSprint(); if (task.isClosed() && properties.containsKey("remainingWork")) { String event = currentUser.getName() + " closed " + task.getReferenceAndLabel(); if (requirement.isTasksClosed()) { event += ", all tasks closed in " + requirement.getReferenceAndLabel(); } postProjectEvent(conversation, event, task); } else if (task.isOwnerSet() && properties.containsKey("ownerId")) { postProjectEvent(conversation, currentUser.getName() + " claimed " + task.getReferenceAndLabel(), task); } if (!task.isOwnerSet() && properties.containsKey("ownerId")) { postProjectEvent(conversation, currentUser.getName() + " unclaimed " + task.getReferenceAndLabel(), task); } if (!task.isClosed() && requirement.isRejectDateSet()) { requirement.setRejectDate(null); sendToClients(conversation, requirement); } } if (entity instanceof Requirement) { Requirement requirement = (Requirement) entity; Sprint sprint = requirement.getSprint(); boolean inCurrentSprint = sprint != null && conversation.getProject().isCurrentSprint(sprint); if (properties.containsKey("description") || properties.containsKey("testDescription") || properties.containsKey("qualitysIds")) { requirement.setDirty(true); conversation.sendToClient(requirement); } if (properties.containsKey("rejectDate") && requirement.isRejectDateSet()) { postProjectEvent(conversation, currentUser.getName() + " rejected " + requirement.getReferenceAndLabel(), requirement); } if (properties.containsKey("accepted") && requirement.isRejectDateSet()) { postProjectEvent(conversation, currentUser.getName() + " accepted " + requirement.getReferenceAndLabel(), requirement); } if (sprint != previousRequirementSprint) { if (properties.containsKey("sprintId")) { if (inCurrentSprint) { postProjectEvent(conversation, currentUser.getName() + " pulled " + requirement.getReferenceAndLabel() + " to current sprint", requirement); } else { postProjectEvent(conversation, currentUser.getName() + " kicked " + requirement.getReferenceAndLabel() + " from current sprint", requirement); } } } if (properties.containsKey("estimatedWork")) { requirement.initializeEstimationVotes(); requirement.setDirty(false); requirement.setWorkEstimationVotingShowoff(false); requirement.setWorkEstimationVotingActive(false); conversation.sendToClient(requirement); } requirement.getProject().getCurrentSprintSnapshot().update(); } if (entity instanceof Project) { Project project = (Project) entity; } if (entity instanceof Impediment) { Impediment impediment = (Impediment) entity; if (impediment.isClosed() && properties.containsKey("closed")) { postProjectEvent(conversation, currentUser.getName() + " closed " + impediment.getReferenceAndLabel(), impediment); } } if (entity instanceof Issue) { Issue issue = (Issue) entity; if (properties.containsKey("closeDate")) { if (issue.isClosed()) { issue.setCloseDate(Date.today()); postProjectEvent(conversation, currentUser.getName() + " closed " + issue.getReferenceAndLabel(), issue); } else { postProjectEvent(conversation, currentUser.getName() + " reopened " + issue.getReferenceAndLabel(), issue); } } if (properties.containsKey("ownerId") && issue.isOwnerSet()) { postProjectEvent(conversation, currentUser.getName() + " claimed " + issue.getReferenceAndLabel(), issue); Release nextRelease = issue.getProject().getNextRelease(); if (issue.isFixReleasesEmpty() && nextRelease != null) { issue.setFixReleases(Collections.singleton(nextRelease)); sendToClients(conversation, issue); } } if (properties.containsKey("fixDate")) { if (issue.isFixed()) { postProjectEvent(conversation, currentUser.getName() + " fixed " + issue.getReferenceAndLabel(), issue); } else { postProjectEvent(conversation, currentUser.getName() + " rejected fix for " + issue.getReferenceAndLabel(), issue); } } if (properties.containsKey("urgent")) { if (issue.isBug()) { Release currentRelease = issue.getProject().getCurrentRelease(); if (issue.isAffectedReleasesEmpty() && currentRelease != null) { issue.setAffectedReleases(Collections.singleton(currentRelease)); sendToClients(conversation, issue); } } } issue.getProject().updateHomepage(issue, false); } if (entity instanceof BlogEntry) { BlogEntry blogEntry = (BlogEntry) entity; if (properties.containsKey("text")) { blogEntry.addAuthor(currentUser); } if (properties.containsKey("published")) { if (blogEntry.isPublished()) { postProjectEvent(conversation, currentUser.getName() + " published " + blogEntry.getReferenceAndLabel(), blogEntry); } blogEntry.getProject().updateHomepage(); } } if (entity instanceof Comment) { Comment comment = (Comment) entity; conversation.getProject().updateHomepage(comment.getParent(), false); } if (entity instanceof SystemConfig) { SystemConfig config = (SystemConfig) entity; if (properties.containsKey("url")) { webApplication.getConfig().setUrl(config.getUrl()); } } conversation.clearRemoteEntitiesByType(Change.class); sendToOtherConversationsByProject(conversation, entity); } @Override public void onSelectProject(GwtConversation conversation, String projectId) { Project project = projectDao.getById(projectId); User user = conversation.getSession().getUser(); if (!project.isVisibleFor(user)) throw new PermissionDeniedException("Project '" + project + "' is not visible for user '" + user + "'"); project.setLastOpenedDateAndTime(DateAndTime.now()); conversation.setProject(project); user.setCurrentProject(project); conversation.sendToClient(project); conversation.sendToClient(project.getSprints()); conversation.sendToClient(project.getParticipants()); Set<Requirement> requirements = project.getRequirements(); conversation.sendToClient(requirements); for (Requirement requirement : requirements) { conversation.sendToClient(requirement.getEstimationVotes()); } conversation.sendToClient(project.getQualitys()); conversation.sendToClient(project.getTasks()); conversation.sendToClient(project.getUserConfigs()); conversation.sendToClient(project.getWikipages()); conversation.sendToClient(project.getImpediments()); conversation.sendToClient(project.getRisks()); conversation.sendToClient(project.getLatestProjectEvents()); conversation.sendToClient(project.getCalendarEvents()); conversation.sendToClient(project.getFiles()); conversation.sendToClient(project.getOpenIssues()); conversation.sendToClient(project.getReleases()); conversation.sendToClient(project.getBlogEntrys()); webApplication.updateOnlineTeamMembers(project, null); } @Override public void onCloseProject(GwtConversation conversation) { Project project = conversation.getProject(); if (project != null) webApplication.setUsersSelectedEntities(project, conversation, new HashSet<String>(0)); conversation.clearRemoteEntities(); conversation.setProject(null); if (project != null) webApplication.updateOnlineTeamMembers(project, conversation); } @Override public void onRequestForum(GwtConversation conversation, boolean all) { Project project = conversation.getProject(); Set<AEntity> parents = new HashSet<AEntity>(); for (Subject subject : project.getSubjects()) { if (subject.getComments().isEmpty()) parents.add(subject); } for (Comment comment : project.getLatestComments()) { AEntity parent = comment.getParent(); if (!all && !conversation.isAvailableOnClient(parent) && comment.getDateAndTime().getPeriodToNow().abs().toDays() > 7) continue; conversation.sendToClient(comment); parents.add(parent); } conversation.sendToClient(parents); } @Override public void onRequestImpediments(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); conversation.sendToClient(project.getImpediments()); } @Override public void onRequestRisks(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); conversation.sendToClient(project.getRisks()); } @Override public void onRequestAcceptedIssues(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); conversation.sendToClient(project.getAcceptedIssues()); } @Override public void onRequestClosedIssues(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); conversation.sendToClient(project.getClosedIssues()); } @Override public void onRequestReleaseIssues(GwtConversation conversation, String releaseId) { assertProjectSelected(conversation); Project project = conversation.getProject(); Release release = releaseDao.getById(releaseId); if (!release.isProject(project)) throw new PermissionDeniedException(); conversation.sendToClient(release.getIssues()); } @Override public void onRequestEntity(GwtConversation conversation, String entityId) { assertProjectSelected(conversation); try { AEntity entity = getDaoService().getById(entityId); if (!Auth.isVisible(entity, conversation.getSession().getUser())) throw new PermissionDeniedException(); // TODO check if entity is from project conversation.sendToClient(entity); } catch (EntityDoesNotExistException ex) { // nop } } @Override public void onRequestEntityByReference(GwtConversation conversation, String reference) { assertProjectSelected(conversation); Project project = conversation.getProject(); AEntity entity = project.getEntityByReference(reference); if (entity == null) { LOG.info("Requested entity not found:", reference); } else { conversation.sendToClient(entity); } } @Override public void onRequestComments(GwtConversation conversation, String parentId) { conversation.sendToClient(commentDao.getCommentsByParentId(parentId)); } @Override public void onRequestChanges(GwtConversation conversation, String parentId) { conversation.sendToClient(changeDao.getChangesByParentId(parentId)); } @Override public void onRequestRequirementEstimationVotes(GwtConversation conversation, String requirementId) { assertProjectSelected(conversation); Project project = conversation.getProject(); Requirement requirement = requirementDao.getById(requirementId); if (!requirement.isProject(project)) throw new PermissionDeniedException(); conversation.sendToClient(requirement.getEstimationVotes()); } @Override public void onActivateRequirementEstimationVoting(GwtConversation conversation, String requirementId) { Requirement requirement = requirementDao.getById(requirementId); if (requirement == null || !requirement.isProject(conversation.getProject())) throw new PermissionDeniedException(); requirement.initializeEstimationVotes(); requirement.setWorkEstimationVotingActive(true); requirement.setWorkEstimationVotingShowoff(false); sendToClients(conversation, requirement); sendToClients(conversation, requirement.getEstimationVotes()); } @Override public void onSwitchToNextSprint(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); Sprint oldSprint = project.getCurrentSprint(); for (Requirement requirement : oldSprint.getRequirements()) { if (!requirement.isClosed()) { requirement.setDirty(true); sendToClients(conversation, requirement); } } project.switchToNextSprint(); sendToClients(conversation, project.getSprints()); sendToClients(conversation, project.getRequirements()); sendToClients(conversation, project.getTasks()); sendToClients(conversation, project); } @Override public void onUpdateProjectHomepage(GwtConversation conversation) { assertProjectSelected(conversation); Project project = conversation.getProject(); project.updateHomepage(); } @Override public void onSendTestEmail(GwtConversation conversation) { webApplication.sendEmail(null, null, "Kunagi email test", "Kunagi email test"); } @Override public void onPing(GwtConversation conversation) { // nop } @Override public void onSleep(GwtConversation conversation, long millis) { Utl.sleep(millis); } // --- helper --- @Override public DataTransferObject startConversation(int conversationNumber) { LOG.debug("startConversation"); WebSession session = (WebSession) getSession(); GwtConversation conversation = session.getGwtConversation(-1); ilarkesto.di.Context context = ilarkesto.di.Context.get(); context.setName("gwt-srv:startSession"); context.bindCurrentThread(); try { onStartConversation(conversation); } catch (Throwable t) { handleServiceMethodException(conversation.getNumber(), "startSession", t); throw new RuntimeException(t); } scrum.client.DataTransferObject ret = (scrum.client.DataTransferObject) conversation.popNextData(); onServiceMethodExecuted(context); return ret; } private void postChangeIfChanged(GwtConversation conversation, AEntity entity, Map properties, User user, String property) { if (properties.containsKey(property)) { boolean reference = property.endsWith("Id"); Object oldValue = Reflect.getProperty(entity, property); Object newValue = properties.get(property); Change change = changeDao.postChange(entity, user, property, oldValue, newValue); conversation.sendToClient(change); } } private void postProjectEvent(GwtConversation conversation, String message, AEntity subject) { assertProjectSelected(conversation); ProjectEvent event = projectEventDao.postEvent(conversation.getProject(), message, subject); sendToClients(conversation, event); sendToClients(conversation, event.createChatMessage()); } private void sendToClients(GwtConversation conversation, Collection<? extends AEntity> entities) { for (AEntity entity : entities) { sendToClients(conversation, entity); } } private void sendToClients(GwtConversation conversation, AEntity entity) { conversation.sendToClient(entity); sendToOtherConversationsByProject(conversation, entity); } private void sendToOtherConversationsByProject(GwtConversation conversation, AEntity entity) { for (AGwtConversation c : webApplication.getConversationsByProject(conversation.getProject(), conversation)) { c.sendToClient(entity); } } private void assertProjectSelected(GwtConversation conversation) { if (conversation.getProject() == null) throw new RuntimeException("No project selected."); } @Override protected Class<? extends AWebApplication> getWebApplicationClass() { return ScrumWebApplication.class; } @Override protected AWebApplication getWebApplication() { return webApplication; } @Override public void onConvertIssueToStory(GwtConversation conversation, String issueId) { Issue issue = issueDao.getById(issueId); Requirement story = requirementDao.postRequirement(issue); issue.setStatement(issue.getStatement() + "\n\n" + "Created Story " + story.getReference() + " in Product Backlog."); issue.setCloseDate(Date.today()); sendToClients(conversation, story); sendToClients(conversation, issue); User currentUser = conversation.getSession().getUser(); postProjectEvent(conversation, currentUser.getName() + " created " + story.getReference() + " from " + issue.getReferenceAndLabel(), issue); changeDao.postChange(issue, currentUser, "storyId", null, story.getId()); changeDao.postChange(story, currentUser, "issueId", null, issue.getId()); } }