/**
*
* 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.application.statistics;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Period;
import org.qi4j.api.configuration.Configuration;
import org.qi4j.api.entity.EntityComposite;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.entity.Identity;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.service.Activatable;
import org.qi4j.api.service.ServiceComposite;
import org.qi4j.api.unitofwork.NoSuchEntityException;
import org.qi4j.api.unitofwork.UnitOfWork;
import org.qi4j.api.usecase.UsecaseBuilder;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.spi.structure.ModuleSPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.streamsource.streamflow.api.workspace.cases.CaseStates;
import se.streamsource.streamflow.infrastructure.event.domain.DomainEvent;
import se.streamsource.streamflow.infrastructure.event.domain.TransactionDomainEvents;
import se.streamsource.streamflow.infrastructure.event.domain.source.EventSource;
import se.streamsource.streamflow.infrastructure.event.domain.source.EventStream;
import se.streamsource.streamflow.infrastructure.event.domain.source.EventVisitor;
import se.streamsource.streamflow.infrastructure.event.domain.source.TransactionVisitor;
import se.streamsource.streamflow.infrastructure.event.domain.source.helper.EventRouter;
import se.streamsource.streamflow.infrastructure.event.domain.source.helper.Events;
import se.streamsource.streamflow.infrastructure.event.domain.source.helper.TransactionTracker;
import se.streamsource.streamflow.util.HierarchicalVisitor;
import se.streamsource.streamflow.web.domain.Describable;
import se.streamsource.streamflow.web.domain.entity.DomainEntity;
import se.streamsource.streamflow.web.domain.entity.casetype.CaseTypeEntity;
import se.streamsource.streamflow.web.domain.entity.casetype.ResolutionEntity;
import se.streamsource.streamflow.web.domain.entity.caze.CaseEntity;
import se.streamsource.streamflow.web.domain.entity.form.FieldEntity;
import se.streamsource.streamflow.web.domain.entity.form.FormEntity;
import se.streamsource.streamflow.web.domain.entity.label.LabelEntity;
import se.streamsource.streamflow.web.domain.entity.organization.GroupEntity;
import se.streamsource.streamflow.web.domain.entity.organization.OrganizationEntity;
import se.streamsource.streamflow.web.domain.entity.organization.OrganizationalUnitEntity;
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.user.UserEntity;
import se.streamsource.streamflow.web.domain.interaction.gtd.Assignee;
import se.streamsource.streamflow.web.domain.interaction.gtd.Ownable;
import se.streamsource.streamflow.web.domain.interaction.gtd.Owner;
import se.streamsource.streamflow.web.domain.structure.SubmittedFieldValue;
import se.streamsource.streamflow.web.domain.structure.casetype.CaseType;
import se.streamsource.streamflow.web.domain.structure.casetype.Resolution;
import se.streamsource.streamflow.web.domain.structure.form.Field;
import se.streamsource.streamflow.web.domain.structure.form.FieldId;
import se.streamsource.streamflow.web.domain.structure.form.Form;
import se.streamsource.streamflow.web.domain.structure.form.FormId;
import se.streamsource.streamflow.web.domain.structure.form.SubmittedFormValue;
import se.streamsource.streamflow.web.domain.structure.form.SubmittedPageValue;
import se.streamsource.streamflow.web.domain.structure.group.Group;
import se.streamsource.streamflow.web.domain.structure.group.Participation;
import se.streamsource.streamflow.web.domain.structure.label.Label;
import se.streamsource.streamflow.web.domain.structure.organization.Organization;
import se.streamsource.streamflow.web.domain.structure.organization.OrganizationalUnit;
import se.streamsource.streamflow.web.domain.structure.organization.OwningOrganizationalUnit;
import se.streamsource.streamflow.web.domain.structure.project.Members;
import se.streamsource.streamflow.web.domain.structure.project.Project;
import se.streamsource.streamflow.web.domain.structure.user.User;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Stack;
import static org.qi4j.api.specification.Specifications.*;
import static se.streamsource.streamflow.infrastructure.event.domain.source.helper.Events.*;
/**
* Consumes domain events and creates application events for statistics.
*/
@Mixins(CaseStatisticsService.Mixin.class)
public interface CaseStatisticsService
extends ServiceComposite, Activatable, CaseStatistics, Configuration
{
class Mixin
implements TransactionVisitor, Activatable, CaseStatistics
{
@Service
EventSource eventSource;
@Service
EventStream stream;
@Service
Iterable<StatisticsStore> statisticsStores;
@Structure
ModuleSPI module;
@This
Configuration<StatisticsConfiguration> config;
TransactionTracker tracker;
EventRouter router;
Logger log;
public TransactionVisitor transactionAdapter;
public void activate() throws Exception
{
log = LoggerFactory.getLogger(CaseStatisticsService.class);
router = new EventRouter().route(and(withNames("changedStatus"), paramIs("param1", CaseStates.CLOSED.name())),
new EventVisitor()
{
public boolean visit(DomainEvent event)
{
// Case was closed
UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork(UsecaseBuilder.newUsecase("Create statistics"));
try
{
CaseEntity entity = null;
try
{
entity = uow.get(CaseEntity.class, event.entity().get());
} catch (NoSuchEntityException e)
{
// Entity has been deleted. Ignore it
return true;
}
// case has been reopend and is still not closed again
// do nothing
if (!entity.isStatus(CaseStates.CLOSED) || !entity.isAssigned())
return true;
CaseStatisticsValue stats = createStatistics(entity);
try
{
notifyStores(stats);
return true;
} catch (StatisticsStoreException e)
{
log.warn(e.getMessage(), e.getCause());
return false;
}
} finally
{
uow.discard();
}
}
}).route(and(withNames("changedDescription", "changedFieldId", "changedFormId"), onEntityTypes(
LabelEntity.class.getName(),
UserEntity.class.getName(),
GroupEntity.class.getName(),
ProjectEntity.class.getName(),
OrganizationEntity.class.getName(),
OrganizationalUnitEntity.class.getName(),
ResolutionEntity.class.getName(),
FormEntity.class.getName(),
FieldEntity.class.getName(),
CaseTypeEntity.class.getName()
)),
new EventVisitor()
{
public boolean visit(DomainEvent event)
{
// Description of related entity was updated
UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork(UsecaseBuilder.newUsecase("Change related description"));
try
{
EntityComposite entity = uow.get(DomainEntity.class, event.entity().get());
RelatedEnum type;
if (entity instanceof Label)
type = RelatedEnum.label;
else if (entity instanceof User)
type = RelatedEnum.user;
else if (entity instanceof Group)
type = RelatedEnum.group;
else if (entity instanceof Project)
type = RelatedEnum.project;
else if (entity instanceof Organization)
type = RelatedEnum.organization;
else if (entity instanceof OrganizationalUnit)
type = RelatedEnum.organizationalUnit;
else if (entity instanceof Resolution)
type = RelatedEnum.resolution;
else if (entity instanceof Form)
type = RelatedEnum.form;
else if (entity instanceof Field)
type = RelatedEnum.field;
else if (entity instanceof CaseType)
type = RelatedEnum.caseType;
else
return true;
RelatedStatisticsValue related = createRelated(entity, type);
try
{
notifyStores(related);
return true;
} catch (StatisticsStoreException e)
{
log.warn(e.getMessage(), e.getCause());
return false;
}
} catch (NoSuchEntityException ex)
{
log.warn("Could not update database information due to missing entity", ex);
return true;
} finally
{
uow.discard();
}
}
}).route(and(withNames("deletedEntity"), onEntityTypes(CaseEntity.class.getName())),
new EventVisitor()
{
public boolean visit(DomainEvent event)
{
try
{
notifyStores(event.entity().get());
return true;
} catch (StatisticsStoreException e)
{
log.warn(e.getMessage(), e.getCause());
return false;
}
}
}).route(withNames("addedOrganizationalUnit", "removedOrganizationalUnit"), new EventVisitor()
{
public boolean visit(DomainEvent event)
{
// Create organizational structure
UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork(UsecaseBuilder.newUsecase("Changed structure"));
try
{
OrganizationalStructureValue structure = createStructure();
notifyStores(structure);
return true;
} catch (StatisticsStoreException e)
{
log.warn(e.getMessage(), e.getCause());
return false;
} finally
{
uow.discard();
}
}
});
transactionAdapter = Events.adapter(router);
tracker = new TransactionTracker(stream, eventSource, config, this);
tracker.start();
}
public void passivate() throws Exception
{
tracker.stop();
}
public void refreshStatistics() throws StatisticsStoreException
{
DateTime dateTimeStart = new DateTime( );
log.info("Refresh all statistics started at: " + dateTimeStart.toString( "YYYY-MM-dd HH:mm" ) );
try
{
// stop service
passivate();
// First clear the statistics stores of all their existing data
log.debug("Clear all statistics stores");
for (StatisticsStore statisticsStore : statisticsStores)
{
statisticsStore.clearAll();
}
// reset configuration
config.configuration().lastEventDate().set( 0L );
config.save();
// start service
activate();
DateTime dateTimeEnd = new DateTime( );
log.info( "Refresh all statistics stoped at: " + dateTimeEnd.toString( "YYYY-MM-dd HH:mm" ));
Period period = new Duration( dateTimeStart, dateTimeEnd ).toPeriod();
log.info( "Time elapsed: " + period.getDays() + " days " + period.getHours() + " hours " + period.getMinutes() + " minutes " + period.getSeconds() + " seconds." );
} catch (Exception e)
{
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
throw new StatisticsStoreException( "Refresh statistics failed.", e );
}
}
public boolean visit(TransactionDomainEvents transactionDomain)
{
return transactionAdapter.visit(transactionDomain);
}
private RelatedStatisticsValue createRelated(EntityComposite entity, RelatedEnum type)
{
ValueBuilder<RelatedStatisticsValue> builder = module.valueBuilderFactory().newValueBuilder(RelatedStatisticsValue.class);
builder.prototype().identity().set(entity.identity().get());
if (entity instanceof Form)
{
builder.prototype().description().set(((FormId.Data) entity).formId().get());
} else if (entity instanceof Field)
{
builder.prototype().description().set(((FieldId.Data) entity).fieldId().get());
} else
{
builder.prototype().description().set(((Describable) entity).getDescription());
}
builder.prototype().relatedType().set(type);
return builder.newInstance();
}
private OrganizationalStructureValue createStructure()
{
UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork();
OrganizationsEntity organizations = uow.get(OrganizationsEntity.class, OrganizationsEntity.ORGANIZATIONS_ID);
final List<OrganizationalUnitValue> ous = new ArrayList<OrganizationalUnitValue>();
OrganizationEntity org = (OrganizationEntity) organizations.organization().get();
org.accept(new HierarchicalVisitor<Object, Object, RuntimeException>()
{
int idx = 0;
Stack<ValueBuilder<OrganizationalUnitValue>> builders = new Stack<ValueBuilder<OrganizationalUnitValue>>();
@Override
public boolean visitEnter(Object visited) throws RuntimeException
{
if (visited instanceof OrganizationalUnit || visited instanceof Organization)
{
ValueBuilder<OrganizationalUnitValue> builder = module.valueBuilderFactory().newValueBuilder(OrganizationalUnitValue.class);
builder.prototype().name().set(((Describable)visited).getDescription());
builder.prototype().id().set(visited.toString());
builder.prototype().left().set(idx);
if (visited instanceof OrganizationalUnit) {
builder.prototype().parent().set( EntityReference.getEntityReference( ((Ownable.Data)visited).owner().get()).identity() );
}
builders.push(builder);
idx++;
}
return super.visitEnter(visited);
}
@Override
public boolean visitLeave(Object visited) throws RuntimeException
{
if (visited instanceof OrganizationalUnit || visited instanceof Organization)
{
ValueBuilder<OrganizationalUnitValue> builder = builders.pop();
builder.prototype().right().set(idx);
ous.add(builder.newInstance());
idx++;
}
return super.visitLeave(visited);
}
});
ValueBuilder<OrganizationalStructureValue> builder = module.valueBuilderFactory().newValueBuilder(OrganizationalStructureValue.class);
builder.prototype().structure().get().addAll(ous);
return builder.newInstance();
}
private CaseStatisticsValue createStatistics(CaseEntity aCase)
{
ValueBuilder<CaseStatisticsValue> builder = module.valueBuilderFactory().newValueBuilder(CaseStatisticsValue.class);
CaseStatisticsValue prototype = builder.prototype();
prototype.identity().set(aCase.identity().get());
prototype.description().set(aCase.getDescription() == null ? "" : aCase.getDescription() );
Assignee assignee = aCase.assignedTo().get();
prototype.assigneeId().set(((Identity) assignee).identity().get());
prototype.caseId().set(aCase.caseId().get());
prototype.createdOn().set(new Date(aCase.createdOn().get().getTime()));
Date closeDate = aCase.closedOn().get();
prototype.closedOn().set(new Date(closeDate.getTime()));
prototype.duration().set(closeDate.getTime() - aCase.createdOn().get().getTime());
prototype.dueOn().set(aCase.dueOn().get());
if (aCase.casepriority().get() != null)
{
prototype.priority().set(aCase.casepriority().get().getDescription());
}
CaseType caseType = aCase.caseType().get();
if (caseType != null)
{
prototype.caseTypeId().set(((Identity) caseType).identity().get());
Owner caseTypeOwner = ((Ownable.Data) caseType).owner().get();
if (caseTypeOwner != null)
prototype.caseTypeOwnerId().set(((Identity) caseTypeOwner).identity().get());
if (aCase.resolution().get() != null)
prototype.resolutionId().set(((Identity) aCase.resolution().get()).identity().get());
}
Owner owner = aCase.owner().get();
prototype.projectId().set(((Identity) owner).identity().get());
OwningOrganizationalUnit.Data po = (OwningOrganizationalUnit.Data) owner;
OrganizationalUnit organizationalUnit = po.organizationalUnit().get();
prototype.organizationalUnitId().set(((Identity) organizationalUnit).identity().get());
String groupId = null;
Participation.Data participant = (Participation.Data) assignee;
findgroup:
for (Group group : participant.groups())
{
Members.Data members = (Members.Data) owner;
if (members.members().contains(group))
{
groupId = ((Identity) group).identity().get();
break findgroup;
}
}
prototype.groupId().set(groupId);
for (Label label : aCase.labels())
{
prototype.labels().get().add(label.toString());
}
ValueBuilder<FormFieldStatisticsValue> formBuilder = module.valueBuilderFactory().newValueBuilder(FormFieldStatisticsValue.class);
for (SubmittedFormValue submittedFormValue : aCase.getLatestSubmittedForms())
{
for (SubmittedPageValue submittedPageValue : submittedFormValue.pages().get())
{
for (SubmittedFieldValue submittedFieldValue : submittedPageValue.fields().get())
{
FieldEntity fieldEntity = module.unitOfWorkFactory().currentUnitOfWork()
.get( FieldEntity.class, submittedFieldValue.field().get().identity() );
if (fieldEntity.isStatistical()) {
formBuilder.prototype().formId().set(submittedFormValue.form().get().identity());
formBuilder.prototype().fieldId().set(submittedFieldValue.field().get().identity());
if (fieldEntity.datatype().get() != null) {
formBuilder.prototype().datatype().set( fieldEntity.datatype().get().getUrl() );
} else {
formBuilder.prototype().datatype().set( "" );
}
// truncate field value if greater than 4500 chars.
// value in fields table is varchar(4500)
String fieldValue = submittedFieldValue.value().get();
fieldValue = fieldValue.length() > 4500 ? fieldValue.substring(0, 4500) : fieldValue;
formBuilder.prototype().value().set(fieldValue);
prototype.fields().get().add(formBuilder.newInstance());
}
}
}
}
return builder.newInstance();
}
private void notifyStores(RelatedStatisticsValue relatedStatisticsValue) throws StatisticsStoreException
{
for (StatisticsStore statisticsStore : statisticsStores)
{
statisticsStore.related(relatedStatisticsValue);
}
}
private void notifyStores(OrganizationalStructureValue structureValue) throws StatisticsStoreException
{
for (StatisticsStore statisticsStore : statisticsStores)
{
statisticsStore.structure(structureValue);
}
}
private void notifyStores(CaseStatisticsValue caseStatisticsValue) throws StatisticsStoreException
{
for (StatisticsStore statisticsStore : statisticsStores)
{
statisticsStore.caseStatistics(caseStatisticsValue);
}
}
private void notifyStores(String id) throws StatisticsStoreException
{
for (StatisticsStore statisticsStore : statisticsStores)
{
statisticsStore.removedCase(id);
}
}
}
}