/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt * * 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. */ package se.streamsource.streamflow.web.domain.entity.caze; import org.qi4j.api.Qi4j; import org.qi4j.api.common.Optional; import org.qi4j.api.concern.ConcernOf; import org.qi4j.api.concern.Concerns; import org.qi4j.api.entity.Identity; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.util.Iterables; import org.qi4j.api.query.Query; import org.qi4j.api.query.QueryExpressions; import org.qi4j.api.sideeffect.SideEffectOf; import org.qi4j.api.sideeffect.SideEffects; import org.qi4j.api.specification.Specification; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.UnitOfWork; import se.streamsource.dci.api.RoleMap; import se.streamsource.streamflow.api.workspace.cases.caselog.CaseLogEntryTypes; import se.streamsource.streamflow.api.workspace.cases.contact.ContactDTO; import se.streamsource.streamflow.api.workspace.cases.conversation.MessageType; import se.streamsource.streamflow.util.Strings; import se.streamsource.streamflow.util.Translator; import se.streamsource.streamflow.web.domain.Describable; import se.streamsource.streamflow.web.domain.Notable; import se.streamsource.streamflow.web.domain.Removable; import se.streamsource.streamflow.web.domain.entity.DomainEntity; import se.streamsource.streamflow.web.domain.entity.form.SubmittedFormsQueries; import se.streamsource.streamflow.web.domain.entity.organization.OrganizationsEntity; import se.streamsource.streamflow.web.domain.entity.project.ProjectEntity; import se.streamsource.streamflow.web.domain.entity.project.ProjectOrganizationalUnitQueries; import se.streamsource.streamflow.web.domain.entity.user.EmailUserEntity; import se.streamsource.streamflow.web.domain.entity.user.ProjectQueries; import se.streamsource.streamflow.web.domain.entity.user.UserEntity; import se.streamsource.streamflow.web.domain.interaction.gtd.AssignIdSideEffect; import se.streamsource.streamflow.web.domain.interaction.gtd.Assignable; import se.streamsource.streamflow.web.domain.interaction.gtd.Assignee; import se.streamsource.streamflow.web.domain.interaction.gtd.CaseId; import se.streamsource.streamflow.web.domain.interaction.gtd.DueOn; import se.streamsource.streamflow.web.domain.interaction.gtd.Ownable; import se.streamsource.streamflow.web.domain.interaction.gtd.Owner; import se.streamsource.streamflow.web.domain.interaction.gtd.Status; import se.streamsource.streamflow.web.domain.interaction.gtd.Unread; import se.streamsource.streamflow.web.domain.interaction.profile.MessageRecipient; import se.streamsource.streamflow.web.domain.interaction.security.Authorization; import se.streamsource.streamflow.web.domain.interaction.security.CaseAccess; import se.streamsource.streamflow.web.domain.interaction.security.CaseAccessDefaults; import se.streamsource.streamflow.web.domain.interaction.security.CaseAccessRestriction; import se.streamsource.streamflow.web.domain.interaction.security.CaseAccessType; import se.streamsource.streamflow.web.domain.interaction.security.PermissionType; import se.streamsource.streamflow.web.domain.structure.attachment.AttachedFile; import se.streamsource.streamflow.web.domain.structure.attachment.Attachment; import se.streamsource.streamflow.web.domain.structure.attachment.Attachments; import se.streamsource.streamflow.web.domain.structure.attachment.FormAttachments; import se.streamsource.streamflow.web.domain.structure.caselog.CaseLoggable; import se.streamsource.streamflow.web.domain.structure.casetype.CaseType; import se.streamsource.streamflow.web.domain.structure.casetype.DefaultDaysToComplete; import se.streamsource.streamflow.web.domain.structure.casetype.PriorityOnCase; import se.streamsource.streamflow.web.domain.structure.casetype.Resolution; import se.streamsource.streamflow.web.domain.structure.casetype.Resolvable; import se.streamsource.streamflow.web.domain.structure.casetype.TypedCase; import se.streamsource.streamflow.web.domain.structure.caze.Case; import se.streamsource.streamflow.web.domain.structure.caze.CasePriority; import se.streamsource.streamflow.web.domain.structure.caze.Closed; import se.streamsource.streamflow.web.domain.structure.caze.Contacts; import se.streamsource.streamflow.web.domain.structure.caze.History; import se.streamsource.streamflow.web.domain.structure.caze.Location; import se.streamsource.streamflow.web.domain.structure.caze.Notes; import se.streamsource.streamflow.web.domain.structure.caze.NotificationTrace; import se.streamsource.streamflow.web.domain.structure.caze.Origin; import se.streamsource.streamflow.web.domain.structure.caze.SubCase; import se.streamsource.streamflow.web.domain.structure.caze.SubCases; import se.streamsource.streamflow.web.domain.structure.conversation.Conversation; import se.streamsource.streamflow.web.domain.structure.conversation.ConversationParticipant; import se.streamsource.streamflow.web.domain.structure.conversation.Conversations; import se.streamsource.streamflow.web.domain.structure.created.Creator; import se.streamsource.streamflow.web.domain.structure.form.FormDraft; import se.streamsource.streamflow.web.domain.structure.form.FormDrafts; import se.streamsource.streamflow.web.domain.structure.form.SearchableForms; import se.streamsource.streamflow.web.domain.structure.form.SubmittedFormValue; import se.streamsource.streamflow.web.domain.structure.form.SubmittedForms; import se.streamsource.streamflow.web.domain.structure.form.Submitter; import se.streamsource.streamflow.web.domain.structure.label.Labelable; import se.streamsource.streamflow.web.domain.structure.organization.*; import se.streamsource.streamflow.web.domain.structure.project.Project; import se.streamsource.streamflow.web.domain.structure.task.DoubleSignatureTasks; import se.streamsource.streamflow.web.domain.structure.user.User; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.Map; /** * This represents a single Case in the system */ @SideEffects( { AssignIdSideEffect.class, StatusClosedSideEffect.class, CaseEntity.CaseLogCaseEntitySideEffect.class, CaseEntity.UpdateSearchableFormsSideEffect.class, CaseEntity.EmailAccesspointSideEffect.class }) @Concerns( { CaseEntity.RemovableConcern.class, CaseEntity.TypedCaseAccessConcern.class, CaseEntity.TypedCaseDefaultDueOnConcern.class, CaseEntity.OwnableCaseAccessConcern.class, CaseEntity.CaseLogContactConcern.class, CaseEntity.CaseLogConversationConcern.class, CaseEntity.CaseLogAttachmentConcern.class, CaseEntity.CaseLogSubmittedFormsConcern.class, CaseEntity.AssignableConcern.class, CaseEntity.TypedCaseDefaultObligatoryPriorityConcern.class, CaseEntity.UnreadConcern.class, CaseEntity.OwnableUnreadConcern.class, CaseEntity.StatusConcern.class}) @Mixins(CaseEntity.AuthorizationMixin.class) public interface CaseEntity extends Case, // Interactions Assignable.Events, Assignable.Data, Describable.Data, DueOn.Data, Notable.Data, Notes.Data, Ownable.Data, CaseId.Data, Status.Events, Status.Data, Conversations.Data, CaseAccess.Data, CaseAccessRestriction.Data, Unread.Data, // Structure Closed, Attachments.Data, FormAttachments.Data, Contacts.Data, Labelable.Data, Removable.Data, Resolvable.Data, FormDrafts.Data, SubmittedForms.Data, SearchableForms.Data, SearchableForms.Events, TypedCase.Data, SubCases.Data, SubCase.Data, History.Data, CaseLoggable.Data, CasePriority.Data, Origin, DoubleSignatureTasks.Data, NotificationTrace.Data, Location.Data, // Queries SubmittedFormsQueries, CaseTypeQueries, DomainEntity { class AuthorizationMixin implements Authorization { @This CaseEntity aCase; @Structure Module module; public boolean hasPermission( String userId, String permission ) { User actor = module.unitOfWorkFactory().currentUnitOfWork().get( User.class, userId ); switch (aCase.status().get()) { case DRAFT: { // Creator has all permissions return aCase.createdBy().get().equals( actor ); } case OPEN: case CLOSED: case ON_HOLD: { CaseAccessType accessType = aCase.getAccessType( PermissionType.valueOf( permission ) ); switch (accessType) { case all: return true; case organization: { OwningOrganizationalUnit.Data owningOU = (OwningOrganizationalUnit.Data) aCase.owner().get(); OrganizationalUnit ou = owningOU.organizationalUnit().get(); return ou.isMemberOrParticipant( actor ); } case project: { Project project = (Project) aCase.owner().get(); return actor.isMember( project ); } case sameoubranch: { // Find all top ou for the projects that the user is member in ArrayList<OrganizationalUnit> topUnits = new ArrayList<OrganizationalUnit>(); for (Project project : ((ProjectQueries)actor).allProjects()) { topUnits.add(((ProjectOrganizationalUnitQueries)((ProjectEntity)project)).topOU()); } // Find the top Ou for the owner to the case OwningOrganizationalUnit.Data owningOU = (OwningOrganizationalUnit.Data) aCase.owner().get(); OrganizationalUnit ou = owningOU.organizationalUnit().get(); Organizations.Data organizations = module.unitOfWorkFactory().currentUnitOfWork().get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ); Organization organization = organizations.organization().get(); while (!ou.isOwnedBy( organization )) { ou = (OrganizationalUnit) ((Ownable.Data)ou).owner().get(); } return topUnits.contains( ou ); } } } } return false; // Can never get here, but just in case } } class TypedCaseAccessConcern extends ConcernOf<TypedCase> implements TypedCase { @This CaseAccess caseAccess; public void changeCaseType( @Optional CaseType newCaseType ) { next.changeCaseType( newCaseType ); if (newCaseType != null) { // Transfer settings for security from new casetype to case for (Map.Entry<PermissionType, CaseAccessType> entry : ((CaseAccessDefaults.Data) newCaseType).accessPermissionDefaults().get().entrySet()) { caseAccess.changeAccess( entry.getKey(), entry.getValue() ); } } } } class TypedCaseDefaultDueOnConcern extends ConcernOf<TypedCase> implements TypedCase { @This DueOn dueOn; public void changeCaseType( @Optional CaseType newCaseType ) { next.changeCaseType( newCaseType ); if (newCaseType != null) { // If no due on is set, then set it to "now" plus the given number of days DefaultDaysToComplete.Data defaultDaysToComplete = (DefaultDaysToComplete.Data) newCaseType; if (defaultDaysToComplete.defaultDaysToComplete().get() > 0) { Calendar now = Calendar.getInstance(); now.add(Calendar.DAY_OF_MONTH,defaultDaysToComplete.defaultDaysToComplete().get() ); dueOn.defaultDueOn(now.getTime()); } } } } class TypedCaseDefaultObligatoryPriorityConcern extends ConcernOf<TypedCase> implements TypedCase { @This CasePriority priority; @Structure Module module; public void changeCaseType( @Optional CaseType newCaseType ) { next.changeCaseType( newCaseType ); if (newCaseType == null) { priority.changePriority( null ); } else { Priority defaultPriority = ((PriorityOnCase.Data) newCaseType).defaultPriority().get(); if (((CasePriority.Data) priority).casepriority().get() == null) { if (defaultPriority != null) { // Set default casepriority if casepriority is missing and there is a // default setting priority.changePriority( ((PriorityOnCase.Data) newCaseType).defaultPriority().get() ); } else if (((PriorityOnCase.Data) newCaseType).mandatory().get()) { // If default priority is missing and priority is mandatory // then set the priority that is closest to middle... Organizations organizations = module.unitOfWorkFactory().currentUnitOfWork() .get( Organizations.class, OrganizationsEntity.ORGANIZATIONS_ID ); int priorityCount = ((Priorities.Data) ((Organizations.Data) organizations).organization().get()) .prioritys().count(); Query<Priority> query = module .queryBuilderFactory() .newQueryBuilder( Priority.class ) .where( QueryExpressions.eq( QueryExpressions.templateFor( (PrioritySettings.Data.class) ) .priority(), Math.round( priorityCount / 2 ) ) ) .newQuery( module.unitOfWorkFactory().currentUnitOfWork() ); priority.changePriority( query.find() ); } } } } } abstract class OwnableCaseAccessConcern extends ConcernOf<Ownable> implements Ownable { @This CaseAccess caseAccess; public void changeOwner( Owner owner ) { next.changeOwner( owner ); // Transfer settings for security from new owner to case for (Map.Entry<PermissionType, CaseAccessType> entry : ((CaseAccessDefaults.Data) owner).accessPermissionDefaults().get().entrySet()) { caseAccess.changeAccess( entry.getKey(), entry.getValue() ); } } } class RemovableConcern extends ConcernOf<Removable> implements Removable { @This Attachments.Data attachmentsData; @This Attachments attachments; @This FormAttachments formAttachments; @This Conversations conversations; @This FormAttachments.Data formAttachmentsData; @This SubCase.Data subCase; @This SubCases.Data subCases; @This Notes.Data notes; @This Conversations.Data conversationsData; @This Case caze; @This Unread unread; @Structure Qi4j api; public boolean removeEntity() { for (Attachment attachment : attachmentsData.attachments().toList()) { attachment.removeEntity(); } for (Attachment attachment : formAttachmentsData.formAttachments().toList()) { attachment.removeEntity(); } for( Conversation conversation : conversationsData.conversations().toList() ) { conversation.removeEntity(); } for (Case childCase : subCases.subCases().toList()) { childCase.removeEntity(); } if( notes.notes().get() != null ) { notes.notes().get().removeEntity(); } return next.removeEntity(); } public boolean reinstate() { for (Attachment attachment : attachmentsData.attachments().toList()) { attachment.reinstate(); } for (Attachment attachment : formAttachmentsData.formAttachments().toList()) { attachment.reinstate(); } for( Conversation conversation : conversationsData.conversations().toList() ) { conversation.reinstate(); } for (Case childCase : subCases.subCases().toList()) { childCase.reinstate(); } if( notes.notes().get() != null ) { notes.notes().get().reinstate(); } unread.setUnread( true ); return next.reinstate(); } public void deleteEntity() { for (Attachment attachment : attachmentsData.attachments().toList()) { attachments.removeAttachment( attachment ); } for (Attachment attachment : formAttachmentsData.formAttachments().toList()) { formAttachments.removeFormAttachment( attachment ); } for( Conversation conversation : conversationsData.conversations().toList()) { conversations.removeConversation( conversation ); } if (subCase.parent().get() != null) subCase.parent().get().removeSubCase( api.dereference( caze ) ); for (Case childCase : subCases.subCases().toList()) { caze.removeSubCase( childCase ); childCase.deleteEntity(); } if( notes.notes().get() != null ) { notes.notes().get().deleteEntity(); } next.deleteEntity(); } } abstract class CaseLogCaseEntitySideEffect extends SideEffectOf<CaseEntity> implements CaseEntity { @This CaseLoggable.Data caseLoggable; public void assignTo( Assignee assignee ) { caseLoggable.caselog().get().addTypedEntry( "{assigned,assignee=" + ((Describable) assignee).getDescription() + "}", CaseLogEntryTypes.system ); } public void unassign() { caseLoggable.caselog().get().addTypedEntry( "{unassigned}", CaseLogEntryTypes.system ); } public void open() { caseLoggable.caselog().get().addTypedEntry( "{opened}", CaseLogEntryTypes.system ); } public void close() { caseLoggable.caselog().get().addTypedEntry( "{closed}", CaseLogEntryTypes.system ); } public void onHold() { caseLoggable.caselog().get().addTypedEntry( "{paused}", CaseLogEntryTypes.system ); } public void reopen() { caseLoggable.caselog().get().addTypedEntry( "{reopened}", CaseLogEntryTypes.system ); } public void resume() { caseLoggable.caselog().get().addTypedEntry( "{resumed}", CaseLogEntryTypes.system ); } public void resolve( Resolution resolution ) { caseLoggable.caselog().get().addTypedEntry( "{resolved,resolution=" + resolution.getDescription() + "}", CaseLogEntryTypes.system ); } public void changeCaseType( @Optional CaseType newCaseType ) { caseLoggable.caselog().get().addTypedEntry( newCaseType != null ? "{changedCaseType,casetype=" + newCaseType.getDescription() + "}" : "{removedCaseType}", CaseLogEntryTypes.system ); } public void changeOwner( Owner owner ) { caseLoggable.caselog().get().addTypedEntry( "{changedOwner,owner=" + ((Project) owner).getDescription() + "}", CaseLogEntryTypes.system ); } public void restrict() { caseLoggable.caselog().get().addTypedEntry( "{restrict}", CaseLogEntryTypes.system ); } public void unrestrict() { caseLoggable.caselog().get().addTypedEntry( "{unrestrict}", CaseLogEntryTypes.system ); } } abstract class UpdateSearchableFormsSideEffect extends SideEffectOf<SubmittedForms> implements SubmittedForms { @This SearchableForms searchableForms; public SubmittedFormValue submitForm(FormDraft formSubmission, Submitter submitter) { SubmittedFormValue submittedForm = result.submitForm( formSubmission, submitter ); searchableForms.updateSearchableFormValues(); return submittedForm; } } abstract class CaseLogContactConcern extends ConcernOf<Contacts> implements Contacts { @This CaseLoggable.Data caseLoggable; @This Contacts.Data contacts; public void addContact( ContactDTO newContact ) { next.addContact( newContact ); if (caseLoggable.caselog().get() != null) { caseLoggable.caselog().get().addTypedEntry( "{addContact}", CaseLogEntryTypes.contact); } } public void updateContact( int index, ContactDTO contact ){ next.updateContact( index, contact ); caseLoggable.caselog().get().addTypedEntry( "{updateContact,name=" + contact.name().get()+"}" , CaseLogEntryTypes.contact); } public void deleteContact( int index ){ caseLoggable.caselog().get().addTypedEntry( "{deleteContact,name=" + contacts.contacts().get().get( index ).name().get()+"}" , CaseLogEntryTypes.contact); next.deleteContact( index ); } } abstract class CaseLogConversationConcern extends ConcernOf<Conversations> implements Conversations { @This CaseLoggable.Data caseLoggable; public Conversation createConversation(String topic, Creator creator) { Conversation conversation = next.createConversation( topic, creator ); caseLoggable.caselog().get().addTypedEntry( "{createConversation,topic=" + topic + "}" , CaseLogEntryTypes.conversation); return conversation; } } abstract class CaseLogAttachmentConcern extends ConcernOf<Attachments> implements Attachments { @This CaseLoggable.Data caseLoggable; public Attachment createAttachment(String uri) throws URISyntaxException { Attachment attachment = next.createAttachment( uri ); caseLoggable.caselog().get().addTypedEntry( "{createAttachment}" , CaseLogEntryTypes.attachment); return attachment; } public void addAttachment(Attachment attachment) { next.addAttachment( attachment ); caseLoggable.caselog().get().addTypedEntry( "{addAttachment,description=" + ((AttachedFile.Data)attachment).name().get() + "}" , CaseLogEntryTypes.attachment); } public void removeAttachment(Attachment attachment) { String fileName = ((AttachedFile.Data)attachment).name().get(); next.removeAttachment( attachment ); caseLoggable.caselog().get().addTypedEntry( "{removeAttachment,description=" + fileName + "}" , CaseLogEntryTypes.attachment); } } abstract class CaseLogSubmittedFormsConcern extends ConcernOf<SubmittedForms> implements SubmittedForms { @This CaseLoggable.Data caseLoggable; public SubmittedFormValue submitForm(FormDraft formSubmission, Submitter submitter) { SubmittedFormValue submittedForm = next.submitForm( formSubmission, submitter ); caseLoggable.caselog().get().addTypedEntry( "{submitForm,description=" + formSubmission.getFormDraftValue().description().get() + "}" , CaseLogEntryTypes.form); return submittedForm; } } abstract class EmailAccesspointSideEffect extends SideEffectOf<CaseEntity> implements CaseEntity { @Structure Module module; @This Case caze; @This Origin origin; public void open() { if (origin.accesspoint().get() != null && !Strings.empty( origin.accesspoint().get().emailTemplates().get().get( "received" ) ) ) { // Switch to administrator user and send confirmation message UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork(); UserEntity administrator = uow.get( UserEntity.class, UserEntity.ADMINISTRATOR_USERNAME ); RoleMap.current().set( administrator ); Conversations.Data conversationsData = (Conversations.Data) caze; for (Conversation conversation : conversationsData.conversations()) { if (conversation.isParticipant( (ConversationParticipant) caze.createdBy().get() )) { Map<String,String> variables = new HashMap<String, String>( ); variables.put( "caseid", ((CaseId.Data) caze).caseId().get() ); variables.put( "subject", caze.getDescription() ); if ( noMailRestrictionPresent( caze.createdBy().get() )) { conversation.createMessage( Translator.translate("{received,caseid=" + ((CaseId.Data) caze).caseId().get() + ",subject=" + caze.getDescription() + "}", origin.accesspoint().get().emailTemplates().get(), variables ), MessageType.SYSTEM, administrator, false ); } } } } } private boolean noMailRestrictionPresent(final Creator creator) { MailRestrictions mailRestrictions = RoleMap.role(MailRestrictions.class); MailRestriction mailRestriction = null; if( creator instanceof EmailUserEntity ) { mailRestriction = Iterables.first(Iterables.filter(new Specification<MailRestriction>() { @Override public boolean satisfiedBy(MailRestriction item) { return ((Identity)creator).identity().get().equals( "email:" + item.getDescription() ); } },mailRestrictions.getMailRestrictions())); } return mailRestriction == null; } public void close() { if (origin.accesspoint().get() != null&& !Strings.empty( origin.accesspoint().get().emailTemplates().get().get( "closed" ) ) ) { // Switch to administrator user and send close message UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork(); UserEntity administrator = uow.get( UserEntity.class, UserEntity.ADMINISTRATOR_USERNAME ); RoleMap.current().set( administrator ); Conversations.Data conversationsData = (Conversations.Data) caze; for (Conversation conversation : conversationsData.conversations()) { if (conversation.isParticipant( (ConversationParticipant) caze.createdBy().get() )) { Map<String,String> variables = new HashMap<String, String>( ); variables.put( "caseid", ((CaseId.Data) caze).caseId().get() ); variables.put( "subject", caze.getDescription() ); if ( noMailRestrictionPresent( caze.createdBy().get() )) { conversation.createMessage( Translator.translate( "{closed,caseid=" + ((CaseId.Data) caze).caseId().get() + ",subject=" + caze.getDescription() + "}", origin.accesspoint().get().emailTemplates().get(), variables ), MessageType.SYSTEM, administrator, false ); } } } } } } abstract class AssignableConcern extends ConcernOf<Assignable> implements Assignable { @This Conversations conversations; @This Conversations.Data conversationsData; @This Unread unread; public void assignTo( Assignee assignee ) { next.assignTo( assignee ); if( conversations.hasConversations() ) { Conversation conversation = conversationsData.conversations().get( 0 ); conversation.addParticipant( (ConversationParticipant)assignee ); } } public void unassign() { next.unassign(); unread.setUnread( true ); } } abstract class UnreadConcern extends ConcernOf<Unread> implements Unread { @This SubmittedForms submittedForms; @This Conversations conversations; public boolean isUnread() { return conversations.hasUnreadConversation() | submittedForms.hasUnreadForm() | next.isUnread() ; } } abstract class OwnableUnreadConcern extends ConcernOf<Ownable> implements Ownable { @This Unread unread; public void changeOwner( Owner owner ) { next.changeOwner( owner ); unread.setUnread( true ); } } abstract class StatusConcern extends ConcernOf<Status> implements Status { @This Unread unread; public void reopen() { next.reopen(); unread.setUnread( true ); } } }