From 02dbf3032c1ec00c7ef7d79dffb763dc5fd7fb3f Mon Sep 17 00:00:00 2001 From: samuelmale Date: Thu, 16 Jan 2020 11:28:54 +0300 Subject: [PATCH 1/2] AF-59: Extensions should support 'requiredProgram' property --- api/pom.xml | 19 + .../module/appframework/AppFrameworkUtil.java | 123 +++++ .../context/ProgramConfiguration.java | 415 ++++++++++++++++ .../module/appframework/domain/Extension.java | 15 +- .../service/AppFrameworkServiceImpl.java | 162 +++++- .../appframework/AppFrameworkUtilTest.java | 124 +++++ .../context/ProgramConfigurationTest.java | 463 ++++++++++++++++++ .../AppConfigurationLoaderFactoryTest.java | 4 +- .../service/AppFrameworkServiceImplTest.java | 199 +++++++- .../service/AppFrameworkServiceTest.java | 93 +++- ...rkServiceImplTest-createPatientProgram.xml | 76 +++ .../src/main/resources/apps/appset1_app.json | 11 + .../apps/extensionset1_extension.json | 15 + 13 files changed, 1670 insertions(+), 49 deletions(-) create mode 100644 api/src/main/java/org/openmrs/module/appframework/AppFrameworkUtil.java create mode 100644 api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java create mode 100644 api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java create mode 100644 api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java create mode 100644 api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml diff --git a/api/pom.xml b/api/pom.xml index 651fe16..87b2ef7 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -30,6 +30,12 @@ org.openmrs.api openmrs-api jar + + + javassist + javassist + + @@ -43,6 +49,12 @@ openmrs-api test-jar test + + + javassist + javassist + + @@ -80,6 +92,13 @@ handlebars + + org.javassist + javassist + 3.18.2-GA + test + + diff --git a/api/src/main/java/org/openmrs/module/appframework/AppFrameworkUtil.java b/api/src/main/java/org/openmrs/module/appframework/AppFrameworkUtil.java new file mode 100644 index 0000000..5c59ce6 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/appframework/AppFrameworkUtil.java @@ -0,0 +1,123 @@ +package org.openmrs.module.appframework; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.Concept; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.context.Context; + +public class AppFrameworkUtil { + + private final static Log log = LogFactory.getLog(AppFrameworkUtil.class); + + /** + * Get the concept by id where the id can either be: + * 1) an integer id like 5090 + * 2) a mapping type id like "XYZ:HT" + * 3) a uuid like "a3e12268-74bf-11df-9768-17cfc9833272" + * 4) a name + * + * @param id the concept identifier + * @return the concept if exist, else null + * @should find a concept by its conceptId + * @should find a concept by its mapping + * @should find a concept by its uuid + * @should find a concept by its name + * @should return null otherwise + */ + // NOTE: This method is a copy and paste from the htmlformentry module I guess it + // should be deprecated on fixing: https://issues.openmrs.org/browse/TRUNK-5655 + public static Concept getConcept(String id) { + if (StringUtils.isNotBlank(id)) { + id = id.trim(); + // see if this is parseable to an Integer; if so, try looking up concept by id + try { + // handle integer: id + int conceptId = Integer.parseInt(id); + return Context.getConceptService().getConcept(conceptId); + } catch (Exception ex) { + // pass + } + // handle mapping id: xyz:ht + if (id.indexOf(":") != -1) { + String [] sourceCodeSplit = id.split(":", 2); + String source = sourceCodeSplit[0].trim(); + String term = sourceCodeSplit[1].trim(); + return Context.getConceptService().getConceptByMapping(term, source, false); + } + // handle name + Concept ret = Context.getConceptService().getConceptByName(id); + if (ret != null) { + return ret; + } + // handle uuid + return Context.getConceptService().getConceptByUuid(id); + } + return null; + } + + /** + * Gets workflows associated with a given concept + */ + public static List getWorkflowsByConcept(Concept concept) { + List ret = new ArrayList(); + for (ProgramWorkflow candidate : getAllWorkflows()) { + if (candidate.getConcept().equals(concept)) { + ret.add(candidate); + } + } + return ret; + } + + /** + * Gets states associated with a given concept + */ + public static List getStatesByConcept(Concept concept) { + List ret = new ArrayList(); + for (ProgramWorkflow workflow : getAllWorkflows()) { + for (ProgramWorkflowState candidate : workflow.getStates()) { + if (candidate.getConcept().equals(concept)) { + ret.add(candidate); + } + } + } + return ret; + } + + /** + * Gets programs associated with a given concept + */ + public static List getProgramsByConcept(Concept concept) { + List ret = new ArrayList(); + List allPrograms = Context.getProgramWorkflowService().getAllPrograms(false); + if (CollectionUtils.isNotEmpty(allPrograms)) { + for (Program candidate : allPrograms) { + if (candidate.getConcept().equals(concept)) { + ret.add(candidate); + } + } + } + return ret; + } + + public static List getAllWorkflows() { + List ret = new ArrayList(); + List allPrograms = Context.getProgramWorkflowService().getAllPrograms(false); + for (Program program : allPrograms) { + Set workflows = program.getAllWorkflows(); + if (CollectionUtils.isNotEmpty(workflows)) { + ret.addAll(workflows); + } + + } + return ret; + } + +} \ No newline at end of file diff --git a/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java b/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java new file mode 100644 index 0000000..fd7d27d --- /dev/null +++ b/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java @@ -0,0 +1,415 @@ +package org.openmrs.module.appframework.context; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.annotate.JsonProperty; +import org.openmrs.Concept; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.APIException; +import org.openmrs.api.context.Context; +import org.openmrs.module.appframework.AppFrameworkUtil; + +public class ProgramConfiguration { + + @JsonProperty + private String programRef; + + @JsonProperty + private String workflowRef; + + @JsonProperty + private String stateRef; + + private ResolvedConfiguration resolvedConfig; + + private List allPossibleWorkflows; + + private List allPossibleStates; + + public ProgramConfiguration() { + + } + + public ProgramConfiguration(String programRef, String workflowRef, String stateRef) { + this.programRef = programRef; + this.workflowRef = workflowRef; + this.stateRef = stateRef; + } + + public Program getProgram() { + if (resolvedConfig == null) { + resolvedConfig = getResolvedConfig(); + } + return resolvedConfig.getProgram(); + } + + public ProgramWorkflow getWorkflow() { + if (resolvedConfig == null) { + resolvedConfig = getResolvedConfig(); + } + return resolvedConfig.getWorkflow(); + } + + public ProgramWorkflowState getState() { + if (resolvedConfig == null) { + resolvedConfig = getResolvedConfig(); + } + return resolvedConfig.getState(); + } + + /** + * Checks whether the underlying {@code program}, {@code workflow} or {@code state} are under the same program tree + * + *

+ * For instance if a {@link ProgramConfiguration} has a {@code resolvedConfig} with a {@code program}, {@code workflow} + * and {@code state}; this method determines whether the {@code state} is associated with the {@code workflow} and if + * the {@code workflow} is also associated with the {@code program}. + * + * @return {@code true} if a valid program tree was found + */ + public boolean hasValidProgramTree() { + Program program = getProgram(); + ProgramWorkflow workflow = getWorkflow(); + ProgramWorkflowState state = getState(); + // Only program was specified + if (program != null && workflow == null && state == null) { + return true; + } + // Only workflow was specified + if (program == null && workflow != null && state == null) { + return true; + } + // Only state was specified + if (program == null && workflow == null && state != null) { + return true; + } + // For cases where only workflow and state were specified, be sure the state belongs to the specified workflow + if (program == null && workflow != null && state != null) { + return workflow.getStates().contains(state); + } + // For cases where only program and state were specified, be sure the state belongs to a workflow associated with program + if (program != null && workflow == null && state != null) { + boolean stateInProgramTree = false; + for (ProgramWorkflow candidate : program.getAllWorkflows()) { + if (candidate.getStates().contains(state)) { + stateInProgramTree = true; + } + } + return stateInProgramTree; + } + // For cases where a workflow and program were specified, be sure the workflow belongs to the specified program + if (program != null && workflow != null) { + boolean programHasWorkflow = program.getAllWorkflows().contains(workflow); + if (state != null) { + // For cases where a workflow, program and state were specified + return programHasWorkflow && workflow.getStates().contains(state); + } + return programHasWorkflow; + } + return false; + } + + protected ResolvedConfiguration getResolvedConfig() { + if (resolvedConfig != null) { + return resolvedConfig; + } + ResolvedConfiguration ret = new ResolvedConfiguration(); + if (StringUtils.isNotBlank(programRef)) { + List programs = getAllPossiblePrograms(); + if (programs.size() == 1) { + ret.setProgram(programs.get(0)); + } else if (programs.size() > 1) { + ret.setProgram(programWithBestWorflowAndStateCombination(programs)); + } + } + if (StringUtils.isNotBlank(workflowRef)) { + List workflows = getAllPossibleWorkflows(); + if (workflows.size() == 1) { + ret.setWorkflow(workflows.get(0)); + } else if (workflows.size() > 1) { + ret.setWorkflow(workflowWithBestProgramAndStateCombination(workflows, ret.getProgram())); + } + } + if (StringUtils.isNotBlank(stateRef)) { + List states = getAllPossibleStates(); + if (states.size() == 1) { + ret.setState(states.get(0)); + } else if (states.size() > 1) { + ret.setState(stateWithBestProgramAndWorkflowCombination(states, ret.getProgram(), ret.getWorkflow())); + } + } + return ret; + } + + protected Program programWithBestWorflowAndStateCombination(List programsSharingConcept) { + Set candidates = new HashSet(); + for (Program program : programsSharingConcept) { + if (StringUtils.isNotBlank(workflowRef) && allPossibleWorkflows == null) { + allPossibleWorkflows = getAllPossibleWorkflows(); + } + if (CollectionUtils.isNotEmpty(allPossibleWorkflows)) { + for (ProgramWorkflow workflow : allPossibleWorkflows) { + if (program.getAllWorkflows().contains(workflow)) { + candidates.add(program); + } + } + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + if (StringUtils.isNotBlank(stateRef) && candidates.size() > 1) { + programsSharingConcept.clear(); + programsSharingConcept.addAll(candidates); + candidates.clear(); + for (Program candidate : programsSharingConcept) { + if (StringUtils.isNotBlank(stateRef) && allPossibleStates == null) { + allPossibleStates = getAllPossibleStates(); + } + for (ProgramWorkflowState state : allPossibleStates) { + for (ProgramWorkflow workflow : candidate.getAllWorkflows()) { + if (workflow.getSortedStates().contains(state)) { + candidates.add(candidate); + } + } + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + } + // If we didn't get any qualifying candidate or got more than one + throw new APIException("Could not choose the intended program out of the many programs identified by concept: " + programRef); + } + + @SuppressWarnings("unchecked") + protected ProgramWorkflow workflowWithBestProgramAndStateCombination(List workflowsSharingConcept, + Program underlyingProgram) { + Collection candidates = new HashSet(); + // Look at the states and try to find out the best combination(s) + if (StringUtils.isNotBlank(stateRef)) { + for (ProgramWorkflow candidate : workflowsSharingConcept) { + if (allPossibleStates == null) { + allPossibleStates = getAllPossibleStates(); + } + for (ProgramWorkflowState state : allPossibleStates) { + if (candidate.getSortedStates().contains(state)) { + candidates.add(candidate); + } + } + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } else if (candidates.size() > 1) { + // Move up to the underlying program and see whether we can have an outstanding combination + if (underlyingProgram != null) { + candidates = CollectionUtils.intersection(candidates, underlyingProgram.getAllWorkflows()); + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + } + } + // If we didn't get any qualifying candidate or got more than one + throw new APIException("Could not choose the intended workflow out of the many workflows identified by concept: " + workflowRef); + } + + @SuppressWarnings("unchecked") + protected ProgramWorkflowState stateWithBestProgramAndWorkflowCombination(List statesSharingConcept, + Program underlyingProgram, ProgramWorkflow underlyingWorkflow) { + Collection candidates = new HashSet(); + // Try combining with the underlying workflow + if (underlyingWorkflow != null) { + candidates = CollectionUtils.intersection(statesSharingConcept, underlyingWorkflow.getSortedStates()); + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + } + // Try combining with the underlying program + if (underlyingProgram != null) { + Set allProgramStates = new HashSet(); + for (ProgramWorkflow workflow : underlyingProgram.getAllWorkflows()) { + allProgramStates.addAll(workflow.getSortedStates()); + } + if (CollectionUtils.isNotEmpty(candidates)) { + candidates = CollectionUtils.intersection(candidates, allProgramStates); + } else { + candidates = CollectionUtils.intersection(statesSharingConcept, allProgramStates); + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + } + // If we didn't get any qualifying candidate or got more than one + throw new APIException("Could not choose the intended state out of the many states identified by concept: " + stateRef); + } + + /** + * Gets program(s) identified by the {@link #programRef} + * + * @should get program by it's {@code id} + * @should get program by it's {@code uuid} + * @should get program(s) by the associated {@code concept} + */ + private List getAllPossiblePrograms() { + try { + Integer programId = Integer.parseInt(programRef); + Program program = Context.getProgramWorkflowService().getProgram(programId); + if (program != null) { + return Arrays.asList(program); + } + } catch (NumberFormatException e) { + // try with the uuid + Program program = Context.getProgramWorkflowService().getProgramByUuid(programRef); + if (program != null) { + return Arrays.asList(program); + } + } + // try using the concept + Concept concept = AppFrameworkUtil.getConcept(programRef); + if (concept == null) { + throw new APIException("Could not find concept identified by: " + programRef); + } + List programs = AppFrameworkUtil.getProgramsByConcept(concept); + if (programs.isEmpty()) { + throw new APIException("Could not find program(s) identified by concept: " + concept); + } + return programs; + } + + /** + * Gets workflow(s) identified by the {@link #workflowRef} + * + * @should get workflow by it's {@code id} + * @should get workflow by it's {@code uuid} + * @should get workflow(s) by the associated {@code concept} + */ + private List getAllPossibleWorkflows() { + try { + Integer workflowId = Integer.parseInt(workflowRef); + ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflow(workflowId); + if (workflow != null) { + return Arrays.asList(workflow); + } + } catch (NumberFormatException e) { + // try with the uuid + ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflowByUuid(workflowRef); + if (workflow != null) { + return Arrays.asList(workflow); + } + } + Concept workflowConcept = AppFrameworkUtil.getConcept(workflowRef); + if (workflowConcept == null) { + throw new APIException("Could not find concept identified by: " + workflowRef); + } + List workflows = AppFrameworkUtil.getWorkflowsByConcept(workflowConcept); + if (CollectionUtils.isEmpty(workflows)) { + throw new APIException("Could not find workflow(s) identified by concept: " + workflowConcept); + } + return workflows; + } + + /** + * Gets state(s) identified by the {@link #stateRef} + * + * @should get state by it's {@code id} + * @should get state by it's {@code uuid} + * @should get state(s) by the associated {@code concept} + */ + private List getAllPossibleStates() { + try { + Integer stateId = Integer.parseInt(stateRef); + ProgramWorkflowState state = Context.getProgramWorkflowService().getState(stateId); + if (state != null) { + return Arrays.asList(state); + } + } catch (NumberFormatException e) { + // try with the uuid + ProgramWorkflowState state = Context.getProgramWorkflowService().getStateByUuid(stateRef); + if (state != null) { + return Arrays.asList(state); + } + } + Concept stateConcept = AppFrameworkUtil.getConcept(stateRef); + if (stateConcept == null) { + throw new APIException("Could not find concept identified by: " + stateRef); + } + List states = AppFrameworkUtil.getStatesByConcept(stateConcept); + if (CollectionUtils.isEmpty(states)) { + throw new APIException("Could not find state(s) identified by concept: " + stateConcept); + } + return states; + } + + public void setResolvedConfig(ResolvedConfiguration resolvedConfig) { + this.resolvedConfig = resolvedConfig; + } + + public boolean hasProgram() { + return getResolvedConfig().getProgram() != null; + } + + public boolean hasWorkflow() { + return getResolvedConfig().getWorkflow() != null; + } + + public boolean hasState() { + return getResolvedConfig().getState() != null; + } + + public boolean hasAll() { + return hasProgram() && hasWorkflow() && hasState(); + } + + public static class ResolvedConfiguration { + + private Program program; + + private ProgramWorkflow workflow; + + private ProgramWorkflowState state; + + public ResolvedConfiguration() { + + } + + public ResolvedConfiguration(Program program, ProgramWorkflow workflow, ProgramWorkflowState state) { + this.program = program; + this.workflow = workflow; + this.state = state; + } + + public Program getProgram() { + return program; + } + + public void setProgram(Program program) { + this.program = program; + } + + public ProgramWorkflow getWorkflow() { + return workflow; + } + + public void setWorkflow(ProgramWorkflow workflow) { + this.workflow = workflow; + } + + public ProgramWorkflowState getState() { + return state; + } + + public void setState(ProgramWorkflowState state) { + this.state = state; + } + } + +} \ No newline at end of file diff --git a/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java b/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java index f35afa3..c9c926f 100644 --- a/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java +++ b/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java @@ -5,9 +5,11 @@ import org.codehaus.jackson.annotate.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import org.openmrs.api.context.Context; +import org.openmrs.module.appframework.context.ProgramConfiguration; import org.openmrs.module.appframework.domain.validators.ValidationErrorMessages; import org.openmrs.module.appframework.template.TemplateFactory; +import java.util.List; import java.util.Map; public class Extension implements Comparable { @@ -52,6 +54,9 @@ public class Extension implements Comparable { @JsonProperty protected Map extensionParams; + @JsonProperty + protected List requiredPrograms; + /** * Will be set by {@link org.openmrs.module.appframework.AppFrameworkActivator} if this extension is defined within * an app @@ -280,4 +285,12 @@ TemplateFactory getTemplateFactory() { return Context.getRegisteredComponent("appframeworkTemplateFactory", TemplateFactory.class); } -} + public List getRequiredPrograms() { + return requiredPrograms; + } + + public void setRequiredPrograms(List requiredPrograms) { + this.requiredPrograms = requiredPrograms; + } + +} \ No newline at end of file diff --git a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java index 21e609c..fed4008 100644 --- a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.script.Bindings; import javax.script.ScriptContext; @@ -33,6 +34,13 @@ import org.codehaus.jackson.map.ObjectMapper; import org.openmrs.Location; import org.openmrs.LocationTag; +import org.openmrs.Patient; +import org.openmrs.PatientProgram; +import org.openmrs.PatientState; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.APIException; import org.openmrs.api.LocationService; import org.openmrs.api.context.Context; import org.openmrs.api.context.UserContext; @@ -42,6 +50,7 @@ import org.openmrs.module.appframework.LoginLocationFilter; import org.openmrs.module.appframework.config.AppFrameworkConfig; import org.openmrs.module.appframework.context.AppContextModel; +import org.openmrs.module.appframework.context.ProgramConfiguration; import org.openmrs.module.appframework.domain.AppDescriptor; import org.openmrs.module.appframework.domain.AppTemplate; import org.openmrs.module.appframework.domain.ComponentState; @@ -294,20 +303,22 @@ public List getExtensionsForCurrentUser(String extensionPointId) { @Override public List getExtensionsForCurrentUser(String extensionPointId, AppContextModel contextModel) { - List extensions = new ArrayList(); - UserContext userContext = Context.getUserContext(); - - for (Extension candidate : getAllEnabledExtensions(extensionPointId)) { - if ((candidate.getBelongsTo() == null - || hasPrivilege(userContext, candidate.getBelongsTo().getRequiredPrivilege())) - && hasPrivilege(userContext, candidate.getRequiredPrivilege())) { - if (contextModel == null || checkRequireExpression(candidate, contextModel)) { - extensions.add(candidate); - } - } - } - - return extensions; + List extensions = new ArrayList(); + UserContext userContext = Context.getUserContext(); + + for (Extension candidate : getAllEnabledExtensions(extensionPointId)) { + if ((candidate.getBelongsTo() == null || hasPrivilege(userContext, candidate.getBelongsTo().getRequiredPrivilege())) + && hasPrivilege(userContext, candidate.getRequiredPrivilege()) ) { + if (contextModel == null) { + extensions.add(candidate); + + } else if (checkRequireExpression(candidate, contextModel) && checkRequiredProgramsOnCurrentPatient(candidate, contextModel)) { + extensions.add(candidate); + } + } + } + + return extensions; } // making it public is a hack so we can test this directly in the appui module @@ -364,8 +375,129 @@ public boolean checkRequireExpression(Extension candidate, AppContextModel conte return false; } } + + /** + * Gets the patient's uuid from a given contextModel. + * + * @param contextModel + */ + protected String getPatientUuid(AppContextModel contextModel) { + Bindings bindings = new SimpleBindings(); + for (Map.Entry e : contextModel.entrySet()) { + bindings.put(e.getKey(), e.getValue()); + } + javascriptEngine.setBindings(bindings, ScriptContext.ENGINE_SCOPE); + try { + String uuid = (String) javascriptEngine.eval("patient.uuid;"); + if (StringUtils.isBlank(uuid)) { + throw new APIException("Patient uuid cannot be empty"); + } + return uuid; + } catch (ScriptException e) { + throw new APIException("Failed to get patient uuid", e); + } + } + + /** + * Determines whether a given extension requires a program that's associated with + * the patient in the current visit. + * + *

+ * For instance if an extension had such a program configuration: + *

+     * 	  {
+     * 		"programRef" : "CIEL:123",
+     * 		"workflowRef" : "CIEL:124",
+     * 		"stateRef" : "CIEL:125"
+     * 	  }
+     *  
+ * This method would return true if patient is enrolled to program("CIEL:123") with + * workflow("CIEL:124") and in state("CIEL:125") + *

+ * + */ + protected boolean checkRequiredProgramsOnCurrentPatient(Extension extension, AppContextModel contextModel) { + List requiredPrograms = extension.getRequiredPrograms(); + if (CollectionUtils.isEmpty(requiredPrograms)) { + return true; + } + Patient patient = Context.getPatientService().getPatientByUuid(getPatientUuid(contextModel)); + + List patientPrograms = Context.getProgramWorkflowService().getPatientPrograms(patient, null, null, + null, null, null, false); + if (CollectionUtils.isEmpty(patientPrograms)) { + // This patient isn't enrolled to any program + return false; + } + + for (ProgramConfiguration configuration : requiredPrograms) { + boolean configAssignableToPatient = hasProgramAssignableToConfiguration(patientPrograms, configuration); + if (configAssignableToPatient) { + // Return early + return configAssignableToPatient; + } + } + return false; + } + + /** + * Determines whether a given {@link ProgramConfiguration} is assignable to any of the + * patientPrograms + * + * @param patientPrograms + * @param config + * @return true if config is assignable to any of the patientPrograms + */ + protected boolean hasProgramAssignableToConfiguration(List patientPrograms, + ProgramConfiguration config) { + if (!config.hasValidProgramTree()) { + throw new APIException("ProgramConfiguration has an invalid program tree"); + } + Program program = config.getProgram(); + ProgramWorkflow workflow = config.getWorkflow(); + ProgramWorkflowState state = config.getState(); + + List programs = new ArrayList(); + List allWorkflows = new ArrayList(); + List patientCurrentStates = new ArrayList(); + for (PatientProgram patientProgram : patientPrograms) { + Set patientStates = patientProgram.getCurrentStates(); + for (PatientState patientState : patientStates) { + patientCurrentStates.add(patientState.getState()); + } + programs.add(patientProgram.getProgram()); + allWorkflows.addAll(patientProgram.getProgram().getAllWorkflows()); + } + if (config.hasProgram() && !config.hasWorkflow() && !config.hasState()) { + return programs.contains(config.getProgram()); + } + if (!config.hasProgram() && config.hasWorkflow() && !config.hasState()) { + return allWorkflows.contains(workflow); + } + if (!config.hasProgram() && !config.hasWorkflow() && config.hasState()) { + return patientCurrentStates.contains(state); + } + if (config.hasAll()) { + return programs.contains(config.getProgram()) && + allWorkflows.contains(workflow) && + patientCurrentStates.contains(state); + } + if (config.hasWorkflow() && config.hasState()) { + return allWorkflows.contains(workflow) && + patientCurrentStates.contains(state); + } + if (config.hasProgram() && config.hasWorkflow()) { + return programs.contains(program) && + allWorkflows.contains(workflow); + } + if (config.hasProgram() && config.hasState()) { + return programs.contains(program) && + patientCurrentStates.contains(state); + } + return false; + } - @Override + @Override public List getAppsForCurrentUser() { List userApps = new ArrayList(); UserContext userContext = Context.getUserContext(); diff --git a/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java b/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java new file mode 100644 index 0000000..15f68e9 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java @@ -0,0 +1,124 @@ +package org.openmrs.module.appframework; + +import static org.hamcrest.CoreMatchers.is; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.openmrs.Concept; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.ConceptService; +import org.openmrs.api.ProgramWorkflowService; +import org.openmrs.api.context.Context; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(Context.class) +public class AppFrameworkUtilTest { + + @Mock + ConceptService conceptService; + + @Mock + ProgramWorkflowService programWorkflowService; + + Concept concept; + + @Before + public void setup() { + concept = new Concept(1); + + mockStatic(Context.class); + when(Context.getConceptService()).thenReturn(conceptService); + when(Context.getProgramWorkflowService()).thenReturn(programWorkflowService); + when(programWorkflowService.getAllPrograms(false)).thenReturn(setupPrograms()); + + } + + @Test + public void getProgramsByConcept_shouldGetAllProgramsIdentifiedByGivenConcept() { + // Replay + List programsIdentifiedByConcept = AppFrameworkUtil.getProgramsByConcept(concept); + + // Verify + Assert.assertThat(programsIdentifiedByConcept.size(), is(2)); + + } + + @Test + public void getAllWorkflows_shouldReturnAllWorkflows() { + // Replay + List workflows = AppFrameworkUtil.getAllWorkflows(); + + // Verify + Assert.assertThat(workflows.size(), is(4)); + } + + @Test + public void getWorkflowsByConcept_shouldGetAllWorkflowsIdentifiedByConcept() { + // Replay + List workflows = AppFrameworkUtil.getWorkflowsByConcept(concept); + + // Verify + Assert.assertThat(workflows.size(), is(2)); + } + + @Test + public void getStatesByConcept_shouldGetAllStatesIdentifiedByConcept() { + // Replay + List states = AppFrameworkUtil.getStatesByConcept(concept); + + // Verify + Assert.assertThat(states.size(), is(1)); + } + + private List setupPrograms() { + Program program1 = new Program(); + program1.setConcept(new Concept()); + Program program2 = new Program(); + program2.setConcept(concept); + Program program3 = new Program(); + program3.setConcept(concept); + + ProgramWorkflow workflow1 = new ProgramWorkflow(); + ProgramWorkflowState state1 = new ProgramWorkflowState(); + state1.setConcept(new Concept(8)); + workflow1.addState(state1); + workflow1.setConcept(concept); + program1.addWorkflow(workflow1); + + ProgramWorkflow workflow2 = new ProgramWorkflow(); + ProgramWorkflowState state2 = new ProgramWorkflowState(); + state2.setConcept(concept); + workflow2.addState(state2); + workflow2.setConcept(new Concept(3)); + program1.addWorkflow(workflow2); + + ProgramWorkflow workflow3 = new ProgramWorkflow(); + ProgramWorkflowState state3 = new ProgramWorkflowState(); + state3.setConcept(new Concept(9)); + workflow3.addState(state3); + workflow3.setConcept(new Concept(7)); + program3.addWorkflow(workflow3); + + ProgramWorkflow workflow4 = new ProgramWorkflow(); + ProgramWorkflowState state4 = new ProgramWorkflowState(); + state4.setConcept(new Concept(10)); + workflow4.addState(state4); + workflow4.setConcept(concept); + program3.addWorkflow(workflow4); + + return Arrays.asList(program1, program2, program3); + } + +} diff --git a/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java new file mode 100644 index 0000000..5b23b97 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java @@ -0,0 +1,463 @@ +package org.openmrs.module.appframework.context; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.openmrs.Concept; +import org.openmrs.ConceptName; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.APIException; +import org.openmrs.api.ProgramWorkflowService; +import org.openmrs.api.context.Context; +import org.openmrs.module.appframework.AppFrameworkUtil; +import org.openmrs.module.appframework.context.ProgramConfiguration.ResolvedConfiguration; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({Context.class, AppFrameworkUtil.class}) +public class ProgramConfigurationTest { + + ProgramConfiguration configuration; + + // Concept associated with programs + Concept concept1; + + // Concept associated with workflows + Concept concept2; + + // Concepts associated with states + @Mock + Concept concept3; + + @Mock + Concept concept4; + + @Mock + ProgramWorkflowService service; + + Program program1; + + Program program2; + + ProgramWorkflow workflow1; + + ProgramWorkflow workflow2; + + ProgramWorkflowState state1; + + ProgramWorkflowState state2; + + List programsSharingConcept; + + List workflowsSharingConcept; + + List statesSharingConcept; + + private static final String PROGRAM_CONCEPT = "CIEL:123"; + + private static final String WORKFLOW_CONCEPT = "CIEL:124"; + + private static final String STATE_CONCEPT = "CIEL:125"; + + @Before + public void setup() { + setupPrograms(); + configuration = new ProgramConfiguration(PROGRAM_CONCEPT, WORKFLOW_CONCEPT, STATE_CONCEPT); + mockStatic(AppFrameworkUtil.class); + mockStatic(Context.class); + + when(AppFrameworkUtil.getConcept(PROGRAM_CONCEPT)).thenReturn(concept1); + when(AppFrameworkUtil.getConcept(WORKFLOW_CONCEPT)).thenReturn(concept2); + when(AppFrameworkUtil.getConcept(STATE_CONCEPT)).thenReturn(concept3); + when(Context.getProgramWorkflowService()).thenReturn(service); + when(service.getProgram(any(Integer.class))).thenReturn(null); + when(service.getProgramByUuid(any(String.class))).thenReturn(null); + when(service.getWorkflow(any(Integer.class))).thenReturn(null); + when(service.getWorkflowByUuid(any(String.class))).thenReturn(null); + when(service.getState(any(Integer.class))).thenReturn(null); + when(service.getStateByUuid(any(String.class))).thenReturn(null); + + programsSharingConcept = new ArrayList(Arrays.asList(program1, program2)); + workflowsSharingConcept = new ArrayList(Arrays.asList(workflow1, workflow2)); + statesSharingConcept = new ArrayList(Arrays.asList(state1, state2)); + + when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(programsSharingConcept); + when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(workflowsSharingConcept); + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(statesSharingConcept); + + } + + @Test + public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsObvious() { + // Setup + when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1)); + + // Replay + Program program = configuration.getProgram(); + + // Verify + Assert.assertEquals(program1, program); + } + + @Test + public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsNotObvious() { + // Setup + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + + // Replay + Program program = configuration.getProgram(); + + // Verify + Assert.assertEquals(program1, program); + } + + @Test + public void getProgram_shouldFailIfProgramCanNotBeChose() { + try { + // Replay + configuration.getProgram(); + fail("Should have failed since we have two programs, states and workflows identified by the smae concept"); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + + PROGRAM_CONCEPT, e.getMessage()); + } + } + + @Test + public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsObvious() { + // Setup + when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); + + // Replay + ProgramWorkflow worklow = configuration.getWorkflow(); + + // Verify + Assert.assertEquals(workflow1, worklow); + } + + @Test + public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsNotObvious() { + // Setup + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + + // Replay + ProgramWorkflow worklow = configuration.getWorkflow(); + + // Verify + Assert.assertEquals(workflow1, worklow); + } + + @Test + public void getWorkflow_shouldFailIfWorkflowCanNotBeChose() { + try { + // Replay + configuration.getWorkflow(); + fail("Should have failed at the program level since we have more than one program, workflow and states identified by the" + + " same concept."); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + + PROGRAM_CONCEPT, e.getMessage()); + } + + // Re-setup + configuration = new ProgramConfiguration(null, WORKFLOW_CONCEPT, null); + try { + // Replay + configuration.getWorkflow(); + fail("Should have failed at the workflow level or base level since we have more than one workflow identified by the same" + + " concept yet this config has no program or state specified."); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: " + + WORKFLOW_CONCEPT, e.getMessage()); + } + } + + @Test + public void getState_shouldHandleCasesWhereUnderlyingStateIsObvious() { + // Setup + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + + // Replay + ProgramWorkflowState state = configuration.getState(); + + // Verify + Assert.assertEquals(state1, state); + } + + @Test + public void getState_shouldHandleCasesWhereUnderlyingStateIsNotObvious() { + // Setup + when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1)); + + // Replay + ProgramWorkflowState state = configuration.getState(); + + // Verify + Assert.assertEquals(state1, state); + } + + @Test + public void getState_shouldFailIfStateCanNotBeChose() { + try { + // Replay + configuration.getState(); + fail("Should have failed at the program level since we have more than one program, workflow and states " + + "identified by the same concept"); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + + PROGRAM_CONCEPT, e.getMessage()); + } + + // Re-setup + configuration = new ProgramConfiguration(null, null, STATE_CONCEPT); + + try { + // Replay + configuration.getState(); + fail("Should have faied at the state level or base level since we have more than one state identified " + + "by the same concept yet this config has no program or workflow specified."); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: " + + STATE_CONCEPT, e.getMessage()); + } + } + + @Test + public void programWithBestWorflowAndStateCombination_shouldUseUniqueStateToDetermineTargetProgram() { + // setup + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + + // replay + Program program = configuration.programWithBestWorflowAndStateCombination( + programsSharingConcept); + + // verify + Assert.assertEquals(program1, program); + } + + @Test + public void programWithBestWorflowAndStateCombination_shouldUseUniqueWorkflowToDetermineTargetProgram() { + // setup + when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); + + // replay + Program program = configuration.programWithBestWorflowAndStateCombination( + programsSharingConcept); + + // verify + Assert.assertEquals(program1, program); + } + + @Test + public void programWithBestWorflowAndStateCombination_shouldFailInAmbigiousConditions() { + try { + // replay + configuration.programWithBestWorflowAndStateCombination(programsSharingConcept); + fail("Should have failed since we have more than one program, workflow and states " + + "identified by the same concept"); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + + PROGRAM_CONCEPT, e.getMessage()); + } + } + + @Test + public void workflowWithBestProgramAndStateCombination_shouldUseUniqueStateToDetermineTargetWorkflow() { + // setup + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + + // replay + ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null); + + // verify + Assert.assertEquals(workflow1, workflow); + } + + @Test + public void workflowWithBestProgramAndStateCombination_shouldUseUniqueProgramToDetermineTargetWorkflow() { + // replay + ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, program1); + + // verify + Assert.assertEquals(workflow1, workflow); + } + + @Test + public void workflowWithBestProgramAndStateCombination_shouldFailInAmbigiousConditions() { + try { + // replay + configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null); + fail("Should have failed at the program level since we have more than one program, workflow and states " + + "identified by the same concept"); + } catch (APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: " + + WORKFLOW_CONCEPT, e.getMessage()); + } + } + + @Test + public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueWorkflowToDetermineTargetState() { + // setup + when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); + + // replay + ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, workflow1); + + // verify + Assert.assertEquals(state1, state); + } + + @Test + public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueProgramToDetermineTargetState() { + // replay + ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, program1, null); + + // verify + Assert.assertEquals(state1, state); + } + + @Test + public void stateWithBestProgramAndWorkflowCombination_shouldFailInAmbigiousConditions() { + try { + // replay + configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, null); + fail("Should have failed at the program level since we have more than one program, workflow and states " + + "identified by the same concept"); + } catch(APIException e) { + // Verify + Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: " + + STATE_CONCEPT, e.getMessage()); + } + } + + @Test + public void hasValidProgramTree_shouldReturnTrueIfTreeIsValid() { + // setup + // program + workflow + state + ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, workflow1, state1); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + + // re-setup + // program + state + resolvedConfig = new ResolvedConfiguration(program1, null, state1); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + + // re-setup + // program + workflow + resolvedConfig = new ResolvedConfiguration(program1, workflow1, null); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + } + + @Test + public void hasValidProgramTree_shouldReturnTrueIfWeHaveNoTree() { + // setup + // program only + ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, null, null); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + + // setup + // workflow only + resolvedConfig = new ResolvedConfiguration(null, workflow1, null); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + + // setup + // state only + resolvedConfig = new ResolvedConfiguration(null, null, state1); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertTrue(configuration.hasValidProgramTree()); + } + + @Test + public void hasValidProgramTree_shouldReturnFalseIfTreeIsInvalid() { + // setup + // program + workflow of different program + state + ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, workflow2, state1); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertFalse(configuration.hasValidProgramTree()); + + // re-setup + // program + workflow + state of different program/workflow + resolvedConfig = new ResolvedConfiguration(program1, workflow1, state2); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertFalse(configuration.hasValidProgramTree()); + + // re-setup + // program + state of different program + resolvedConfig = new ResolvedConfiguration(program1, null, state2); + configuration.setResolvedConfig(resolvedConfig); + + // replay and verify + Assert.assertFalse(configuration.hasValidProgramTree()); + } + + private void setupPrograms() { + setupConcepts(); + + state1 = new ProgramWorkflowState(1); + state2 = new ProgramWorkflowState(2); + + state1.setConcept(concept3); + state2.setConcept(concept4); + + workflow1 = new ProgramWorkflow(1); + workflow2 = new ProgramWorkflow(2); + + program1 = new Program(1); + program2 = new Program(2); + + program1.addWorkflow(workflow1); + program2.addWorkflow(workflow2); + + workflow1.addState(state1); + workflow2.addState(state2); + } + + private void setupConcepts() { + concept1 = new Concept(1); + concept2 = new Concept(2); + when(concept3.getName()).thenReturn(new ConceptName("State One", null)); + when(concept4.getName()).thenReturn(new ConceptName("State Two", null)); + + } +} diff --git a/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java b/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java index ad0438d..063f71e 100644 --- a/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java +++ b/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java @@ -32,8 +32,8 @@ public void testConfigurationLoad() throws IOException { List appDescriptors = appConfigurationLoaderFactory.getAppDescriptors(); List extensions = appConfigurationLoaderFactory.getExtensions(); - assertEquals(5, appDescriptors.size()); - assertEquals(4, extensions.size()); + assertEquals(6, appDescriptors.size()); + assertEquals(5, extensions.size()); } } diff --git a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java index 95ec71a..f6af2ce 100644 --- a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java @@ -3,18 +3,22 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import javax.script.ScriptException; import javax.validation.Validator; import org.hibernate.Criteria; @@ -22,19 +26,31 @@ import org.hibernate.classic.Session; import org.junit.After; import org.junit.Before; -import org.junit.runner.RunWith; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.openmrs.Location; +import org.openmrs.Patient; +import org.openmrs.PatientProgram; +import org.openmrs.PatientState; +import org.openmrs.Program; +import org.openmrs.ProgramWorkflow; +import org.openmrs.ProgramWorkflowState; +import org.openmrs.api.APIException; +import org.openmrs.api.PatientService; +import org.openmrs.api.ProgramWorkflowService; import org.openmrs.api.context.Context; import org.openmrs.api.db.hibernate.DbSessionFactory; -import org.openmrs.Location; +import org.openmrs.module.appframework.LoginLocationFilter; import org.openmrs.module.appframework.config.AppFrameworkConfig; import org.openmrs.module.appframework.context.AppContextModel; +import org.openmrs.module.appframework.context.ProgramConfiguration; +import org.openmrs.module.appframework.context.ProgramConfiguration.ResolvedConfiguration; import org.openmrs.module.appframework.domain.AppDescriptor; import org.openmrs.module.appframework.domain.Extension; import org.openmrs.module.appframework.domain.ExtensionPoint; import org.openmrs.module.appframework.feature.FeatureToggleProperties; import org.openmrs.module.appframework.feature.TestFeatureTogglePropertiesFactory; -import org.openmrs.module.appframework.LoginLocationFilter; import org.openmrs.module.appframework.repository.AllAppDescriptors; import org.openmrs.module.appframework.repository.AllComponentsState; import org.openmrs.module.appframework.repository.AllFreeStandingExtensions; @@ -47,11 +63,12 @@ @PrepareForTest(Context.class) public class AppFrameworkServiceImplTest { - private Validator validator = mock(Validator.class); + @Mock() + private Validator validator; - private AllAppDescriptors allAppDescriptors = new AllAppDescriptors(validator); + private AllAppDescriptors allAppDescriptors; - private AllFreeStandingExtensions allFreeStandingExtensions = new AllFreeStandingExtensions(validator); + private AllFreeStandingExtensions allFreeStandingExtensions; private AllComponentsState allComponentsState = new AllComponentsState(); @@ -60,7 +77,7 @@ public class AppFrameworkServiceImplTest { private FeatureToggleProperties featureToggles = TestFeatureTogglePropertiesFactory.get(); private AppFrameworkConfig appFrameworkConfig = new AppFrameworkConfig(); - + private AppDescriptor app1; private AppDescriptor app2; @@ -82,6 +99,17 @@ public class AppFrameworkServiceImplTest { private AppFrameworkServiceImpl service; private LoginLocationFilter loginLocationFilter; + + private PatientProgram patientProgram; + + private Program program; + + private ProgramWorkflowState state; + + private ProgramWorkflow workflow; + + private static final String PATIENT_UUID = "0cbe2ed3-cd5f-4f46-9459-26127c9265ab"; + @Before public void setUp() throws Exception { @@ -95,6 +123,11 @@ public boolean accept(Location location) { PowerMockito.mockStatic(Context.class); when(Context.getRegisteredComponents(eq(LoginLocationFilter.class))) .thenReturn(Arrays.asList(loginLocationFilter)); + + setUpPatientProgram(); + + allAppDescriptors = new AllAppDescriptors(validator); + allFreeStandingExtensions = new AllFreeStandingExtensions(validator); featureToggles.setPropertiesFile(new File(this.getClass().getResource("/" + FeatureToggleProperties.FEATURE_TOGGLE_PROPERTIES_FILE_NAME).getFile())); @@ -279,12 +312,134 @@ public void testGetLoginLocationsShouldReturnAllLoginLocations() throws Exceptio assertEquals(loginLocations.get(1).getUuid(), actualLoginLocations.get(1).getUuid()); } + public void testGetPatientUuid() throws ScriptException { + // Setup + AppContextModel contextModel = new AppContextModel(); + contextModel.put("patient", new PatientContextModel(PATIENT_UUID)); + + // Replay + String uuid = service.getPatientUuid(contextModel); + + // Verify + assertEquals(PATIENT_UUID, uuid); + } + + @Test + public void testCheckRequiredProgramsOnCurrentPatient() { + // Setup + mockStatic(Context.class); + Patient patient = new Patient(); + PatientService patientService = mock(PatientService.class); + ProgramWorkflowService workflowService = mock(ProgramWorkflowService.class); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(workflowService.getPatientPrograms(patient, null, null, + null, null, null, false)).thenReturn(Arrays.asList(patientProgram)); + when(Context.getPatientService()).thenReturn(patientService); + when(Context.getProgramWorkflowService()).thenReturn(workflowService); + + AppContextModel contextModel = new AppContextModel(); + contextModel.put("patient", new PatientContextModel(PATIENT_UUID)); + ProgramConfiguration config = new ProgramConfiguration(); + config.setResolvedConfig(new ResolvedConfiguration(program, workflow, state)); + + // Replay + boolean hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel); + + // Verify + assertTrue(hasRequiredPrograms); + + // Re-setup + config.setResolvedConfig(new ResolvedConfiguration(null, workflow, state)); + + // Replay + hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel); + + // Verify + assertTrue(hasRequiredPrograms); + + // Re-setup + // Configuration with a state that's not a member of the patient's current states + ProgramWorkflowState pastState = new ProgramWorkflowState(); + workflow.addState(pastState); + config.setResolvedConfig(new ResolvedConfiguration(null, null, pastState)); + + // Replay + hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel); + + // Verify + assertFalse(hasRequiredPrograms); + } + + @Test + public void hasProgramAssignableToConfiguration_shouldReturnTrueIfConfigIsAssignableToAnyOfThePatientPrograms() throws ScriptException { + // Setup + PatientProgram emptyPatientProgram = new PatientProgram(); + emptyPatientProgram.setProgram(new Program()); + List patientPrograms = Arrays.asList(emptyPatientProgram, patientProgram); + + ProgramConfiguration config = new ProgramConfiguration(); + config.setResolvedConfig(new ResolvedConfiguration(program, workflow, state)); + + // Replay + boolean isAssignable = service.hasProgramAssignableToConfiguration(patientPrograms, config); + + // Verify + assertTrue(isAssignable); + } + + @Test + public void hasProgramAssignableToConfiguration_shouldReturnFalseIfConfigIsNotAssignableToAnyOfThePatientPrograms() throws ScriptException { + // Setup + PatientProgram emptyPatientProgram = new PatientProgram(); + emptyPatientProgram.setProgram(new Program()); + List patientPrograms = Arrays.asList(emptyPatientProgram, patientProgram); + + ProgramWorkflowState pastState = new ProgramWorkflowState(); + workflow.addState(pastState); + + // Configuration with a state that's not a member of the patient's current states + ProgramConfiguration config = new ProgramConfiguration(); + config.setResolvedConfig(new ResolvedConfiguration(program, workflow, pastState)); + + // Replay + boolean isAssignable = service.hasProgramAssignableToConfiguration(patientPrograms, config); + + // Verify + assertFalse(isAssignable); + } + + @Test + public void hasProgramAssignableToConfiguration_shouldFailWithAnExceptionIfConfigurationHasAnInvalidProgramTree() { + // Setup + + // Configuration with invalid program tree + ProgramConfiguration config = new ProgramConfiguration(); + config.setResolvedConfig(new ResolvedConfiguration(program, workflow, new ProgramWorkflowState())); + + try { + // Replay + service.hasProgramAssignableToConfiguration(Arrays.asList(patientProgram), config); + fail("Should throw exception if configuration has an invalid program tree"); + } catch(APIException e) { + // Verify + assertEquals("ProgramConfiguration has an invalid program tree", e.getMessage()); + } + } + private Extension extensionRequiring(String requires) { Extension extension = new Extension(); extension.setRequire(requires); return extension; } - + + private Extension extensionRequiringProgramConfigurations(List programConfigurations) { + Extension extension = new Extension(); + extension.setRequiredPrograms(programConfigurations); + return extension; + } + + public class VisitStatus { public boolean active; public boolean admitted; @@ -293,4 +448,32 @@ public VisitStatus(boolean active, boolean admitted) { this.admitted = admitted; } } + + public static class PatientContextModel { + public String uuid; + public Patient patient; + public PatientContextModel(String uuid) { + this.uuid = uuid; + } + } + + private void setUpPatientProgram() { + program = new Program(); + program.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1343"); + + state = new ProgramWorkflowState(); + state.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1344"); + + workflow = new ProgramWorkflow(); + workflow.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1345"); + + workflow.addState(state); + program.addWorkflow(workflow); + + patientProgram = mock(PatientProgram.class); + when(patientProgram.getProgram()).thenReturn(program); + PatientState currentState = new PatientState(); + currentState.setState(state); + when(patientProgram.getCurrentStates()).thenReturn(new HashSet(Arrays.asList(currentState))); + } } diff --git a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java index 3125aa2..64ae839 100644 --- a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java +++ b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java @@ -68,10 +68,14 @@ public class AppFrameworkServiceTest extends BaseModuleContextSensitiveTest { @Autowired private AppFrameworkService appFrameworkService; - - @Autowired - private AppFrameworkConfig appFrameworkConfig; - + + @Autowired + private AppFrameworkConfig appFrameworkConfig; + + private static String UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM = "0cbe2ed3-cd5x-4f46-9459-26127c226b9i"; + + private static String UUID_OF_PATIENT_ENROLLED_TO_PROGRAMS = "0cbe2ed3-cd5f-4f46-9459-26127c9265ab"; + @Before public void setup() throws IOException { //trigger loading of the apps @@ -113,17 +117,38 @@ else if (p4.getPrivilege().equals(privilegeToAssign)) return us.createUser(u, "Openmr5xy"); } + private Role viewPatientProgramRole() { + UserService us = Context.getUserService(); + Role role = new Role("View patient program", "Set of privileges required to view a Patient and their programs"); + Privilege p1 = new Privilege("View Patients", "description"); + us.savePrivilege(p1); + Privilege p2 = new Privilege("View Patient Programs", "description"); + us.savePrivilege(p2); + Privilege p3 = new Privilege("View Concepts", "description"); + us.savePrivilege(p3); + Privilege p4 = new Privilege("View Programs", "description"); + us.savePrivilege(p4); + + role.addPrivilege(p1); + role.addPrivilege(p2); + role.addPrivilege(p3); + role.addPrivilege(p4); + return us.saveRole(role); + } + /** * @see {@link AppFrameworkService#getAllEnabledExtensions()} */ @Test public void getAllEnabledExtensions_shouldGetAllEnabledExtensions() throws Exception { List visitExts = appFrameworkService.getAllEnabledExtensions(); - assertEquals(4, visitExts.size()); - assertThat(visitExts, - containsInAnyOrder(hasProperty("id", is("registerOutpatientHomepageLink")), - hasProperty("id", is("orderXrayExtension")), hasProperty("id", is("gotoPatientExtension")), - hasProperty("id", is("gotoArchives")))); + assertEquals(5, visitExts.size()); + assertThat(visitExts, containsInAnyOrder( + hasProperty("id", is("registerOutpatientHomepageLink")), + hasProperty("id", is("orderXrayExtension")), + hasProperty("id", is("gotoPatientExtension")), + hasProperty("id", is("gotoArchives")), + hasProperty("id", is("gotoProgramSection")))); } /** @@ -172,7 +197,7 @@ public void getExtensionsForCurrentUser_shouldGetAllEnabledExtensionsForTheCurre assertEquals(user, Context.getAuthenticatedUser()); List userExts = appFrameworkService.getExtensionsForCurrentUser(); - assertEquals(2, userExts.size()); + assertEquals(3, userExts.size()); List userAppIds = new ArrayList(); for (Extension ext : userExts) { userAppIds.add(ext.getId()); @@ -218,8 +243,9 @@ public void getExtensionsForCurrentUser_shouldReturnExtensionsWithNoRequiredPriv if (Context.getAuthenticatedUser() != null) Context.logout(); assertNull(Context.getAuthenticatedUser()); - assertThat(appFrameworkService.getExtensionsForCurrentUser(), hasSize(1)); - assertThat(appFrameworkService.getExtensionsForCurrentUser().get(0).getId(), is("gotoArchives")); + + assertThat(appFrameworkService.getExtensionsForCurrentUser(), hasSize(2)); + assertThat(appFrameworkService.getExtensionsForCurrentUser().get(0).getId(), is("gotoArchives")); } /** @@ -300,29 +326,24 @@ public void getExtensionsForCurrentUser_shouldGetAllEnabledExtensionsForTheSuper */ @Test public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentUserByRequireProperty() throws Exception { + executeDataSet("AppFrameworkServiceImplTest-createPatientProgram.xml"); User user = setupPrivilegesRolesAndUser("Some Random Privilege"); + user.addRole(viewPatientProgramRole()); Context.authenticate(user.getUsername(), "Openmr5xy"); assertEquals(user, Context.getAuthenticatedUser()); - AppContextModel contextModel = setupContextModel(true); + AppContextModel contextModel = setupContextModel(true, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM); List extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel); assertEquals(1, extensions.size()); assertEquals("orderXrayExtension", extensions.get(0).getId()); - contextModel = setupContextModel(false); + contextModel = setupContextModel(false, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM); extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel); assertEquals(1, extensions.size()); assertEquals("gotoArchives", extensions.get(0).getId()); } - - private AppContextModel setupContextModel(boolean isVisitActive) { - AppContextModel bindings = new AppContextModel(); - bindings.put("patientId", 7); - bindings.put("visit", new VisitStatus(isVisitActive)); - return bindings; - } - + @Test public void getAllAppTemplates_shouldGetAppTemplates() throws Exception { List actual = appFrameworkService.getAllAppTemplates(); @@ -354,7 +375,7 @@ public void testInheritingConfigurationFromAppTemplate() { @Verifies(value = "should get all enabled apps", method = "getAllEnabledApps()") public void getAllEnabledApps_shouldGetAllEnabledApps() throws Exception { List apps = appFrameworkService.getAllEnabledApps(); - assertEquals(4, apps.size());//should include the app with that requires no privilege + assertEquals(5, apps.size());//should include the app with that requires no privilege List appIds = new ArrayList(); for (AppDescriptor app : apps) { appIds.add(app.getId()); @@ -475,4 +496,30 @@ public VisitStatus(boolean visitActive) { this.active = visitActive; } } + + @Test + public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentUserByProgramConfiguration() throws Exception { + executeDataSet("AppFrameworkServiceImplTest-createPatientProgram.xml"); + User user = setupPrivilegesRolesAndUser(null); + user.addRole(viewPatientProgramRole()); + Context.authenticate(user.getUsername(), "Openmr5xy"); + + AppContextModel contextModel = setupContextModel(true, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM); + List extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel); + + assertEquals(0, extensions.size()); + + contextModel = setupContextModel(true, UUID_OF_PATIENT_ENROLLED_TO_PROGRAMS); + extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel); + + assertEquals(1, extensions.size()); + assertEquals("gotoProgramSection", extensions.get(0).getId()); + } + + private AppContextModel setupContextModel(boolean isVisitActive, String patientUuid) { + AppContextModel bindings = new AppContextModel(); + bindings.put("patient", new AppFrameworkServiceImplTest.PatientContextModel(patientUuid)); + bindings.put("visit", new VisitStatus(isVisitActive)); + return bindings; + } } diff --git a/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml new file mode 100644 index 0000000..e4f0dba --- /dev/null +++ b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appsfortesting/src/main/resources/apps/appset1_app.json b/appsfortesting/src/main/resources/apps/appset1_app.json index 1242ffa..8271175 100644 --- a/appsfortesting/src/main/resources/apps/appset1_app.json +++ b/appsfortesting/src/main/resources/apps/appset1_app.json @@ -24,5 +24,16 @@ } ], "contextModel": [ "xrayConceptId", "patientId" ] + }, + { + "id": "programApp", + "description": "Program Application", + "requiredPrivilege": "Manage Programs", + "extensionPoints": [ + { + "id": "programActions" + } + ], + "contextModel": null } ] \ No newline at end of file diff --git a/appsfortesting/src/main/resources/apps/extensionset1_extension.json b/appsfortesting/src/main/resources/apps/extensionset1_extension.json index ff5e4d3..3fefed8 100644 --- a/appsfortesting/src/main/resources/apps/extensionset1_extension.json +++ b/appsfortesting/src/main/resources/apps/extensionset1_extension.json @@ -29,5 +29,20 @@ "label": "Go to Archives", "url": "/archives.page?patientId={{patientId}}", "require": "!visit.active" + }, + { + "id": "gotoProgramSection", + "appId": "programApp", + "extensionPointId": "programActions", + "type": "link", + "label": "Go to Program", + "url": "/program.page?patientId={{patientId}}", + "requiredPrograms": [ + { + "programRef": "CIEL:123", + "workflowRef": "1743", + "stateRef": "CIEL:124" + } + ] } ] \ No newline at end of file From 7e2e40d9ad037ad786db485e7351da8b3665c017 Mon Sep 17 00:00:00 2001 From: samuelmale Date: Tue, 3 Mar 2020 01:59:57 +0300 Subject: [PATCH 2/2] AF-59: Misc code refactoring --- .../context/ProgramConfiguration.java | 244 +++++++----------- .../service/AppFrameworkServiceImpl.java | 36 +-- .../context/ProgramConfigurationTest.java | 134 +--------- .../service/AppFrameworkServiceTest.java | 41 ++- ...rkServiceImplTest-createPatientProgram.xml | 2 +- 5 files changed, 141 insertions(+), 316 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java b/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java index fd7d27d..217a373 100644 --- a/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java +++ b/api/src/main/java/org/openmrs/module/appframework/context/ProgramConfiguration.java @@ -1,15 +1,16 @@ package org.openmrs.module.appframework.context; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; import java.util.List; +import java.util.Map.Entry; import java.util.Set; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.codehaus.jackson.annotate.JsonProperty; import org.openmrs.Concept; +import org.openmrs.OpenmrsObject; import org.openmrs.Program; import org.openmrs.ProgramWorkflow; import org.openmrs.ProgramWorkflowState; @@ -29,11 +30,7 @@ public class ProgramConfiguration { private String stateRef; private ResolvedConfiguration resolvedConfig; - - private List allPossibleWorkflows; - - private List allPossibleStates; - + public ProgramConfiguration() { } @@ -113,7 +110,7 @@ public boolean hasValidProgramTree() { return programHasWorkflow && workflow.getStates().contains(state); } return programHasWorkflow; - } + } return false; } @@ -121,137 +118,103 @@ protected ResolvedConfiguration getResolvedConfig() { if (resolvedConfig != null) { return resolvedConfig; } - ResolvedConfiguration ret = new ResolvedConfiguration(); - if (StringUtils.isNotBlank(programRef)) { - List programs = getAllPossiblePrograms(); - if (programs.size() == 1) { - ret.setProgram(programs.get(0)); - } else if (programs.size() > 1) { - ret.setProgram(programWithBestWorflowAndStateCombination(programs)); - } - } - if (StringUtils.isNotBlank(workflowRef)) { - List workflows = getAllPossibleWorkflows(); - if (workflows.size() == 1) { - ret.setWorkflow(workflows.get(0)); - } else if (workflows.size() > 1) { - ret.setWorkflow(workflowWithBestProgramAndStateCombination(workflows, ret.getProgram())); - } - } - if (StringUtils.isNotBlank(stateRef)) { - List states = getAllPossibleStates(); - if (states.size() == 1) { - ret.setState(states.get(0)); - } else if (states.size() > 1) { - ret.setState(stateWithBestProgramAndWorkflowCombination(states, ret.getProgram(), ret.getWorkflow())); - } - } - return ret; - } - - protected Program programWithBestWorflowAndStateCombination(List programsSharingConcept) { - Set candidates = new HashSet(); - for (Program program : programsSharingConcept) { - if (StringUtils.isNotBlank(workflowRef) && allPossibleWorkflows == null) { - allPossibleWorkflows = getAllPossibleWorkflows(); - } - if (CollectionUtils.isNotEmpty(allPossibleWorkflows)) { - for (ProgramWorkflow workflow : allPossibleWorkflows) { - if (program.getAllWorkflows().contains(workflow)) { - candidates.add(program); - } - } - } + List programs = this.getAllPossiblePrograms(); + List workflows = this.getAllPossibleWorkflows(); + List states = this.getAllPossibleStates(); + + if (CollectionUtils.isNotEmpty(programs) && CollectionUtils.isNotEmpty(workflows)) { + filter(programs, workflows); } - if (candidates.size() == 1) { - return candidates.iterator().next(); - } - if (StringUtils.isNotBlank(stateRef) && candidates.size() > 1) { - programsSharingConcept.clear(); - programsSharingConcept.addAll(candidates); - candidates.clear(); - for (Program candidate : programsSharingConcept) { - if (StringUtils.isNotBlank(stateRef) && allPossibleStates == null) { - allPossibleStates = getAllPossibleStates(); - } - for (ProgramWorkflowState state : allPossibleStates) { - for (ProgramWorkflow workflow : candidate.getAllWorkflows()) { - if (workflow.getSortedStates().contains(state)) { - candidates.add(candidate); - } - } - } - } - if (candidates.size() == 1) { - return candidates.iterator().next(); - } + if (CollectionUtils.isNotEmpty(workflows) && CollectionUtils.isNotEmpty(states)) { + filter(workflows, states); } - // If we didn't get any qualifying candidate or got more than one - throw new APIException("Could not choose the intended program out of the many programs identified by concept: " + programRef); - } - - @SuppressWarnings("unchecked") - protected ProgramWorkflow workflowWithBestProgramAndStateCombination(List workflowsSharingConcept, - Program underlyingProgram) { - Collection candidates = new HashSet(); - // Look at the states and try to find out the best combination(s) - if (StringUtils.isNotBlank(stateRef)) { - for (ProgramWorkflow candidate : workflowsSharingConcept) { - if (allPossibleStates == null) { - allPossibleStates = getAllPossibleStates(); - } - for (ProgramWorkflowState state : allPossibleStates) { - if (candidate.getSortedStates().contains(state)) { - candidates.add(candidate); - } - } + if (CollectionUtils.isNotEmpty(programs) && CollectionUtils.isNotEmpty(states)) { + if (!(programs.size() == 1 && states.size() == 1)) { + filter(programs, states); } } - if (candidates.size() == 1) { - return candidates.iterator().next(); - } else if (candidates.size() > 1) { - // Move up to the underlying program and see whether we can have an outstanding combination - if (underlyingProgram != null) { - candidates = CollectionUtils.intersection(candidates, underlyingProgram.getAllWorkflows()); - if (candidates.size() == 1) { - return candidates.iterator().next(); - } + // Collect short listed items + ResolvedConfiguration ret = new ResolvedConfiguration(); + if (programs != null) { + if (programs.size() == 1) { + ret.setProgram(programs.get(0)); + } else { + throw new APIException("Failed to figure out the intended program identified by: " + programRef); } } - // If we didn't get any qualifying candidate or got more than one - throw new APIException("Could not choose the intended workflow out of the many workflows identified by concept: " + workflowRef); - } - - @SuppressWarnings("unchecked") - protected ProgramWorkflowState stateWithBestProgramAndWorkflowCombination(List statesSharingConcept, - Program underlyingProgram, ProgramWorkflow underlyingWorkflow) { - Collection candidates = new HashSet(); - // Try combining with the underlying workflow - if (underlyingWorkflow != null) { - candidates = CollectionUtils.intersection(statesSharingConcept, underlyingWorkflow.getSortedStates()); - if (candidates.size() == 1) { - return candidates.iterator().next(); + if (workflows != null) { + if (workflows.size() == 1) { + ret.setWorkflow(workflows.get(0)); + } else { + throw new APIException("Failed to figure out the intended workflow identified by: " + workflowRef); } } - // Try combining with the underlying program - if (underlyingProgram != null) { - Set allProgramStates = new HashSet(); - for (ProgramWorkflow workflow : underlyingProgram.getAllWorkflows()) { - allProgramStates.addAll(workflow.getSortedStates()); - } - if (CollectionUtils.isNotEmpty(candidates)) { - candidates = CollectionUtils.intersection(candidates, allProgramStates); + if (states != null) { + if (states.size() == 1) { + ret.setState(states.get(0)); } else { - candidates = CollectionUtils.intersection(statesSharingConcept, allProgramStates); - } - if (candidates.size() == 1) { - return candidates.iterator().next(); + throw new APIException("Failed to figure out the intended state identified by: " + stateRef); } } - // If we didn't get any qualifying candidate or got more than one - throw new APIException("Could not choose the intended state out of the many states identified by concept: " + stateRef); + return ret; } + /** + * Combines member(s) from the owners and owned list and retains only those that make a valid tree. + * + *

+ * A valid tree is deduced when an {@code Owner} has(owns) the {@code Owned} member it's combined with. + * Eg. If a {@code Program}(owner) is combined with a {@code ProgramWorkflow}(owned) and the workflow is a member + * of the program's workflows. + * + * @param In the ProgramConfiguration context, an owner is an {@link OpenmrsObject} that has a workflow or + * state eg. {@code Program} + * @param In the ProgramConfiguration context, an owned item is an {@link OpenmrsObject} that is part of a + * program or workflow eg. {@code ProgramWorkflowState} + * @param owners the owners + * @param theOwned the owned + */ + private void filter(List owners, List theOwned) { + List ownersFiltrate = new ArrayList(); + List ownedMembersFiltrate = new ArrayList(); + List> ownedToOwnersMapEntries = new ArrayList>(); + + for (Owner owner : owners) { + for (Owned owned : theOwned) { + AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry(owned, owner); + ownedToOwnersMapEntries.add(entry); + } + } + for (Entry entry : ownedToOwnersMapEntries) { + Owner owner = entry.getValue(); + Owned owned = entry.getKey(); + if (owner instanceof Program) { + Set workflows = ((Program)owner).getAllWorkflows(); + if (owned instanceof ProgramWorkflow) { + if (workflows.contains(owned)) { + ownersFiltrate.add(owner); + ownedMembersFiltrate.add(owned); + } + } else if (owned instanceof ProgramWorkflowState) { + for (ProgramWorkflow workflow : workflows) { + if (workflow.getStates(false).contains(owned)) { + ownersFiltrate.add(owner); + ownedMembersFiltrate.add(owned); + } + } + } + } else if (owner instanceof ProgramWorkflow){ + if (((ProgramWorkflow)owner).getStates(false).contains(owned)) { + ownersFiltrate.add(owner); + ownedMembersFiltrate.add(owned); + } + } + } + // update the short lists + owners.retainAll(ownersFiltrate); + theOwned.retainAll(ownedMembersFiltrate); + } + /** * Gets program(s) identified by the {@link #programRef} * @@ -260,6 +223,9 @@ protected ProgramWorkflowState stateWithBestProgramAndWorkflowCombination(List

getAllPossiblePrograms() { + if (programRef == null) { + return null; + } try { Integer programId = Integer.parseInt(programRef); Program program = Context.getProgramWorkflowService().getProgram(programId); @@ -293,6 +259,9 @@ private List getAllPossiblePrograms() { * @should get workflow(s) by the associated {@code concept} */ private List getAllPossibleWorkflows() { + if (workflowRef == null) { + return null; + } try { Integer workflowId = Integer.parseInt(workflowRef); ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflow(workflowId); @@ -325,6 +294,9 @@ private List getAllPossibleWorkflows() { * @should get state(s) by the associated {@code concept} */ private List getAllPossibleStates() { + if (stateRef == null) { + return null; + } try { Integer stateId = Integer.parseInt(stateRef); ProgramWorkflowState state = Context.getProgramWorkflowService().getState(stateId); @@ -352,23 +324,7 @@ private List getAllPossibleStates() { public void setResolvedConfig(ResolvedConfiguration resolvedConfig) { this.resolvedConfig = resolvedConfig; } - - public boolean hasProgram() { - return getResolvedConfig().getProgram() != null; - } - - public boolean hasWorkflow() { - return getResolvedConfig().getWorkflow() != null; - } - - public boolean hasState() { - return getResolvedConfig().getState() != null; - } - - public boolean hasAll() { - return hasProgram() && hasWorkflow() && hasState(); - } - + public static class ResolvedConfiguration { private Program program; diff --git a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java index fed4008..020dc84 100644 --- a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java @@ -452,7 +452,8 @@ protected boolean hasProgramAssignableToConfiguration(List patie ProgramConfiguration config) { if (!config.hasValidProgramTree()) { throw new APIException("ProgramConfiguration has an invalid program tree"); - } + } + boolean passForProgram = true, passForWorkflow = true, passForState = true; Program program = config.getProgram(); ProgramWorkflow workflow = config.getWorkflow(); ProgramWorkflowState state = config.getState(); @@ -468,33 +469,16 @@ protected boolean hasProgramAssignableToConfiguration(List patie programs.add(patientProgram.getProgram()); allWorkflows.addAll(patientProgram.getProgram().getAllWorkflows()); } - if (config.hasProgram() && !config.hasWorkflow() && !config.hasState()) { - return programs.contains(config.getProgram()); - } - if (!config.hasProgram() && config.hasWorkflow() && !config.hasState()) { - return allWorkflows.contains(workflow); - } - if (!config.hasProgram() && !config.hasWorkflow() && config.hasState()) { - return patientCurrentStates.contains(state); - } - if (config.hasAll()) { - return programs.contains(config.getProgram()) && - allWorkflows.contains(workflow) && - patientCurrentStates.contains(state); - } - if (config.hasWorkflow() && config.hasState()) { - return allWorkflows.contains(workflow) && - patientCurrentStates.contains(state); - } - if (config.hasProgram() && config.hasWorkflow()) { - return programs.contains(program) && - allWorkflows.contains(workflow); + if (program != null) { + passForProgram = programs.contains(config.getProgram()); } - if (config.hasProgram() && config.hasState()) { - return programs.contains(program) && - patientCurrentStates.contains(state); + if (workflow != null) { + passForWorkflow = allWorkflows.contains(workflow); } - return false; + if (state != null) { + passForState = patientCurrentStates.contains(state); + } + return passForProgram && passForWorkflow && passForState; } @Override diff --git a/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java index 5b23b97..47427e2 100644 --- a/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java +++ b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java @@ -99,13 +99,12 @@ public void setup() { when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(programsSharingConcept); when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(workflowsSharingConcept); when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(statesSharingConcept); - } @Test public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsObvious() { // Setup - when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1)); + when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(new ArrayList(Arrays.asList(program1))); // Replay Program program = configuration.getProgram(); @@ -117,7 +116,7 @@ public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsObvious() { @Test public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsNotObvious() { // Setup - when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(new ArrayList(Arrays.asList(state1))); // Replay Program program = configuration.getProgram(); @@ -134,7 +133,7 @@ public void getProgram_shouldFailIfProgramCanNotBeChose() { fail("Should have failed since we have two programs, states and workflows identified by the smae concept"); } catch(APIException e) { // Verify - Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + Assert.assertEquals("Failed to figure out the intended program identified by: " + PROGRAM_CONCEPT, e.getMessage()); } } @@ -142,7 +141,7 @@ public void getProgram_shouldFailIfProgramCanNotBeChose() { @Test public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsObvious() { // Setup - when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); + when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(new ArrayList(Arrays.asList(workflow1))); // Replay ProgramWorkflow worklow = configuration.getWorkflow(); @@ -154,7 +153,7 @@ public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsObvious() { @Test public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsNotObvious() { // Setup - when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(new ArrayList(Arrays.asList(state1))); // Replay ProgramWorkflow worklow = configuration.getWorkflow(); @@ -172,7 +171,7 @@ public void getWorkflow_shouldFailIfWorkflowCanNotBeChose() { + " same concept."); } catch(APIException e) { // Verify - Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + Assert.assertEquals("Failed to figure out the intended program identified by: " + PROGRAM_CONCEPT, e.getMessage()); } @@ -185,7 +184,7 @@ public void getWorkflow_shouldFailIfWorkflowCanNotBeChose() { + " concept yet this config has no program or state specified."); } catch(APIException e) { // Verify - Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: " + Assert.assertEquals("Failed to figure out the intended workflow identified by: " + WORKFLOW_CONCEPT, e.getMessage()); } } @@ -193,7 +192,7 @@ public void getWorkflow_shouldFailIfWorkflowCanNotBeChose() { @Test public void getState_shouldHandleCasesWhereUnderlyingStateIsObvious() { // Setup - when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); + when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(new ArrayList(Arrays.asList(state1))); // Replay ProgramWorkflowState state = configuration.getState(); @@ -205,7 +204,7 @@ public void getState_shouldHandleCasesWhereUnderlyingStateIsObvious() { @Test public void getState_shouldHandleCasesWhereUnderlyingStateIsNotObvious() { // Setup - when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1)); + when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(new ArrayList(Arrays.asList(program1))); // Replay ProgramWorkflowState state = configuration.getState(); @@ -223,7 +222,7 @@ public void getState_shouldFailIfStateCanNotBeChose() { + "identified by the same concept"); } catch(APIException e) { // Verify - Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " + Assert.assertEquals("Failed to figure out the intended program identified by: " + PROGRAM_CONCEPT, e.getMessage()); } @@ -237,121 +236,11 @@ public void getState_shouldFailIfStateCanNotBeChose() { + "by the same concept yet this config has no program or workflow specified."); } catch(APIException e) { // Verify - Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: " + Assert.assertEquals("Failed to figure out the intended state identified by: " + STATE_CONCEPT, e.getMessage()); } } - @Test - public void programWithBestWorflowAndStateCombination_shouldUseUniqueStateToDetermineTargetProgram() { - // setup - when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); - - // replay - Program program = configuration.programWithBestWorflowAndStateCombination( - programsSharingConcept); - - // verify - Assert.assertEquals(program1, program); - } - - @Test - public void programWithBestWorflowAndStateCombination_shouldUseUniqueWorkflowToDetermineTargetProgram() { - // setup - when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); - - // replay - Program program = configuration.programWithBestWorflowAndStateCombination( - programsSharingConcept); - - // verify - Assert.assertEquals(program1, program); - } - - @Test - public void programWithBestWorflowAndStateCombination_shouldFailInAmbigiousConditions() { - try { - // replay - configuration.programWithBestWorflowAndStateCombination(programsSharingConcept); - fail("Should have failed since we have more than one program, workflow and states " - + "identified by the same concept"); - } catch(APIException e) { - // Verify - Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: " - + PROGRAM_CONCEPT, e.getMessage()); - } - } - - @Test - public void workflowWithBestProgramAndStateCombination_shouldUseUniqueStateToDetermineTargetWorkflow() { - // setup - when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1)); - - // replay - ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null); - - // verify - Assert.assertEquals(workflow1, workflow); - } - - @Test - public void workflowWithBestProgramAndStateCombination_shouldUseUniqueProgramToDetermineTargetWorkflow() { - // replay - ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, program1); - - // verify - Assert.assertEquals(workflow1, workflow); - } - - @Test - public void workflowWithBestProgramAndStateCombination_shouldFailInAmbigiousConditions() { - try { - // replay - configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null); - fail("Should have failed at the program level since we have more than one program, workflow and states " - + "identified by the same concept"); - } catch (APIException e) { - // Verify - Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: " - + WORKFLOW_CONCEPT, e.getMessage()); - } - } - - @Test - public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueWorkflowToDetermineTargetState() { - // setup - when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1)); - - // replay - ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, workflow1); - - // verify - Assert.assertEquals(state1, state); - } - - @Test - public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueProgramToDetermineTargetState() { - // replay - ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, program1, null); - - // verify - Assert.assertEquals(state1, state); - } - - @Test - public void stateWithBestProgramAndWorkflowCombination_shouldFailInAmbigiousConditions() { - try { - // replay - configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, null); - fail("Should have failed at the program level since we have more than one program, workflow and states " - + "identified by the same concept"); - } catch(APIException e) { - // Verify - Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: " - + STATE_CONCEPT, e.getMessage()); - } - } - @Test public void hasValidProgramTree_shouldReturnTrueIfTreeIsValid() { // setup @@ -458,6 +347,5 @@ private void setupConcepts() { concept2 = new Concept(2); when(concept3.getName()).thenReturn(new ConceptName("State One", null)); when(concept4.getName()).thenReturn(new ConceptName("State Two", null)); - } } diff --git a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java index 64ae839..ceae52c 100644 --- a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java +++ b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java @@ -69,9 +69,6 @@ public class AppFrameworkServiceTest extends BaseModuleContextSensitiveTest { @Autowired private AppFrameworkService appFrameworkService; - @Autowired - private AppFrameworkConfig appFrameworkConfig; - private static String UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM = "0cbe2ed3-cd5x-4f46-9459-26127c226b9i"; private static String UUID_OF_PATIENT_ENROLLED_TO_PROGRAMS = "0cbe2ed3-cd5f-4f46-9459-26127c9265ab"; @@ -478,25 +475,6 @@ public void getLoginLocations_shouldReturnLoginLocationsConfiguredForTheUser() { assertTrue(locations.contains(location2)); } - private Location createLocation(String uuid) { - Location location = new Location(); - location.setUuid(uuid); - location.setName("Some name " + uuid); - location.addTag(locationService.getLocationTagByName(AppFrameworkConstants.LOCATION_TAG_SUPPORTS_LOGIN)); - return locationService.saveLocation(location); - } - - public class VisitStatus { - - public int id = 17; - - public boolean active; - - public VisitStatus(boolean visitActive) { - this.active = visitActive; - } - } - @Test public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentUserByProgramConfiguration() throws Exception { executeDataSet("AppFrameworkServiceImplTest-createPatientProgram.xml"); @@ -515,6 +493,25 @@ public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentU assertEquals(1, extensions.size()); assertEquals("gotoProgramSection", extensions.get(0).getId()); } + + private Location createLocation(String uuid) { + Location location = new Location(); + location.setUuid(uuid); + location.setName("Some name " + uuid); + location.addTag(locationService.getLocationTagByName(AppFrameworkConstants.LOCATION_TAG_SUPPORTS_LOGIN)); + return locationService.saveLocation(location); + } + + public class VisitStatus { + + public int id = 17; + + public boolean active; + + public VisitStatus(boolean visitActive) { + this.active = visitActive; + } + } private AppContextModel setupContextModel(boolean isVisitActive, String patientUuid) { AppContextModel bindings = new AppContextModel(); diff --git a/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml index e4f0dba..fbc616e 100644 --- a/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml +++ b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml @@ -64,7 +64,7 @@ - +