package edu.ualberta.med.biobank.test.reports;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import edu.ualberta.med.biobank.common.reports.BiobankReport;
import edu.ualberta.med.biobank.common.util.AbstractRowPostProcess;
import edu.ualberta.med.biobank.common.util.DateCompare;
import edu.ualberta.med.biobank.common.util.Predicate;
import edu.ualberta.med.biobank.common.util.PredicateUtil;
import edu.ualberta.med.biobank.common.wrappers.AliquotedSpecimenWrapper;
import edu.ualberta.med.biobank.common.wrappers.ContainerWrapper;
import edu.ualberta.med.biobank.common.wrappers.PatientWrapper;
import edu.ualberta.med.biobank.common.wrappers.ProcessingEventWrapper;
import edu.ualberta.med.biobank.common.wrappers.SiteWrapper;
import edu.ualberta.med.biobank.common.wrappers.SpecimenTypeWrapper;
import edu.ualberta.med.biobank.common.wrappers.SpecimenWrapper;
import edu.ualberta.med.biobank.common.wrappers.StudyWrapper;
import edu.ualberta.med.biobank.server.reports.AbstractReport;
import edu.ualberta.med.biobank.server.reports.ReportFactory;
import gov.nih.nci.system.applicationservice.ApplicationException;
import gov.nih.nci.system.applicationservice.WritableApplicationService;
public abstract class AbstractReportTest {
public static enum CompareResult {
ORDER,
SIZE
};
public static final String[] DATE_FIELDS = { "Week", "Month", "Quarter",
"Year" };
public static final Predicate<ContainerWrapper> CONTAINER_IS_TOP_LEVEL =
new Predicate<ContainerWrapper>() {
@Override
public boolean evaluate(ContainerWrapper container) {
return container.getParentContainer() == null;
}
};
public static final Predicate<ContainerWrapper> CONTAINER_CAN_STORE_SAMPLES_PREDICATE =
new Predicate<ContainerWrapper>() {
@Override
public boolean evaluate(ContainerWrapper container) {
return (container.getContainerType().getSpecimenTypeCollection(
false) != null)
&& (container.getContainerType()
.getSpecimenTypeCollection(false).size() > 0);
}
};
public static final Predicate<SpecimenWrapper> ALIQUOT_NOT_IN_SENT_SAMPLE_CONTAINER =
new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
return (aliquot.getParentContainer() == null)
|| !aliquot.getParentContainer().getLabel()
.startsWith("SS");
}
};
public static final Predicate<SpecimenWrapper> ALIQUOT_HAS_POSITION =
new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
return aliquot.getParentContainer() != null;
}
};
public static final Comparator<SpecimenWrapper> ORDER_ALIQUOT_BY_PNUMBER =
new Comparator<SpecimenWrapper>() {
@Override
public int compare(SpecimenWrapper lhs, SpecimenWrapper rhs) {
return compareStrings(lhs.getCollectionEvent().getPatient()
.getPnumber(), rhs.getCollectionEvent().getPatient()
.getPnumber());
}
};
private BiobankReport report;
private static ReportDataSource dataSource;
protected static void setReportDataSource(ReportDataSource dataSource) {
AbstractReportTest.dataSource = dataSource;
}
public static Predicate<SpecimenWrapper> aliquotSite(final boolean isIn,
final Integer siteId) {
return new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
return isIn == aliquot.getProcessingEvent().getCenter().getId()
.equals(siteId);
}
};
}
public static Predicate<ContainerWrapper> containerSite(final boolean isIn,
final Integer siteId) {
return new Predicate<ContainerWrapper>() {
@Override
public boolean evaluate(ContainerWrapper container) {
return isIn == container.getSite().getId().equals(siteId);
}
};
}
public static Predicate<ProcessingEventWrapper> patientVisitSite(
final boolean isIn, final Integer siteId) {
return new Predicate<ProcessingEventWrapper>() {
@Override
public boolean evaluate(ProcessingEventWrapper patientVisit) {
return !isIn || patientVisit.getCenter().getId().equals(siteId);
}
};
}
public static Predicate<SpecimenWrapper> aliquotDrawnSameDay(final Date date) {
final Calendar wanted = Calendar.getInstance();
wanted.setTime(date);
return new Predicate<SpecimenWrapper>() {
private Calendar drawn = Calendar.getInstance();
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
drawn.setTime(aliquot.getParentSpecimen().getCreatedAt());
int drawnDayOfYear = drawn.get(Calendar.DAY_OF_YEAR);
int wantedDayOfYear = wanted.get(Calendar.DAY_OF_YEAR);
int drawnYear = drawn.get(Calendar.YEAR);
int wantedYear = wanted.get(Calendar.YEAR);
return (drawnDayOfYear == wantedDayOfYear)
&& (drawnYear == wantedYear);
}
};
}
public static Predicate<SpecimenWrapper> aliquotLinkedBetween(
final Date after, final Date before) {
return new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
Date linked = aliquot.getCreatedAt();
return (DateCompare.compare(linked, after) <= 0)
&& (DateCompare.compare(linked, before) >= 0);
}
};
}
public static Predicate<SpecimenWrapper> aliquotPvProcessedBetween(
final Date after, final Date before) {
return new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
Date processed = aliquot.getProcessingEvent().getCreatedAt();
return (DateCompare.compare(processed, after) <= 0)
&& (DateCompare.compare(processed, before) >= 0);
}
};
}
public static Predicate<ProcessingEventWrapper> patientVisitProcessedBetween(
final Date after, final Date before) {
return new Predicate<ProcessingEventWrapper>() {
@Override
public boolean evaluate(ProcessingEventWrapper pevent) {
Date processed = pevent.getCreatedAt();
return (DateCompare.compare(processed, after) <= 0)
&& (DateCompare.compare(processed, before) >= 0);
}
};
}
public static Collection<Integer> getTopContainerIds(
Collection<ContainerWrapper> containers) {
Set<Integer> topContainerIds = new HashSet<Integer>();
for (ContainerWrapper container : PredicateUtil.filter(containers,
CONTAINER_IS_TOP_LEVEL)) {
topContainerIds.add(container.getId());
}
return topContainerIds;
}
public static Collection<ContainerWrapper> getTopContainers(
Collection<ContainerWrapper> containers) {
Set<ContainerWrapper> topContainers = new HashSet<ContainerWrapper>();
for (ContainerWrapper container : PredicateUtil.filter(containers,
CONTAINER_IS_TOP_LEVEL)) {
topContainers.add(container);
}
return topContainers;
}
public static Predicate<SpecimenWrapper> aliquotTopContainerIdIn(String list) {
if ((list == null) || list.isEmpty()) {
return new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
return false;
}
};
}
final List<Integer> topContainerIds = new ArrayList<Integer>();
for (String id : list.split(",")) {
topContainerIds.add(Integer.valueOf(id));
}
return new Predicate<SpecimenWrapper>() {
@Override
public boolean evaluate(SpecimenWrapper aliquot) {
ContainerWrapper top = aliquot.getTop();
return (top != null) && topContainerIds.contains(top.getId());
}
};
}
public static int getDateFieldValue(Calendar calendar, String dateField) {
int dateGroupBy = 0;
if (dateField.equals("Year")) {
dateGroupBy = calendar.get(Calendar.YEAR);
} else if (dateField.equals("Quarter")) {
dateGroupBy = (calendar.get(Calendar.MONTH) / 3) + 1;
} else if (dateField.equals("Month")) {
dateGroupBy = calendar.get(Calendar.MONTH) + 1;
} else if (dateField.equals("Week")) {
dateGroupBy = calendar.get(Calendar.WEEK_OF_YEAR);
// java.util.GregorianCalendar.WEEK_OF_YEAR can only have 1 to 53
// weeks a year. However, it is seemingly possibly to have 54 weeks
// in a year (e.g. "Jan 01, 2000" - 366 days, with the first and
// last week in the year having 1 day). MySQL SELECT
// WEEK('2000-01-01', 0) returns 0 and SELECT WEEK('2000-12-31')
// returns 53. However, java.util.GregorianCalendar would say that
// the week of '2000-01-01' is 1 and the week of '2000-12-31' is 1
// as well. This is because java.util.GregorianCalendar considers
// the last week of the year 2000 the first week of the next year,
// NOT ITS OWN WEEK. So, we must check for this case.
//
// Note that MySQL's behaviour depends on the value of the
// "default_week_format" variable.
if ((dateGroupBy == 1)
&& (calendar.get(Calendar.DAY_OF_YEAR) > 3 * calendar
.getMaximum(Calendar.DAY_OF_WEEK))) {
// If there has clearly been more than one week in this year,
// but its week of year is only 1, then add one to the previous
// week number.
calendar.add(Calendar.WEEK_OF_YEAR, -1);
int previousWeek = calendar.get(Calendar.WEEK_OF_YEAR);
calendar.add(Calendar.WEEK_OF_YEAR, 1);
dateGroupBy = previousWeek + 1;
}
// change range from [1..54] to [0..53] to match MySQL
dateGroupBy -= 1;
}
return dateGroupBy;
}
protected final void setReport(BiobankReport report) {
this.report = report;
}
protected final BiobankReport getReport() {
if (report == null) {
String reportName = this.getClass().getSimpleName()
.replace("Test", "");
report = BiobankReport.getReportByName(reportName);
report.setParams(new ArrayList<Object>());
report.setContainerList("");
report.setGroupBy("");
}
return report;
}
protected final boolean isInSite() {
return getReport().getOp() == "=";
}
protected final Integer getSiteId() {
return getReport().getSiteId();
}
// use getReport() to get the parameters
protected abstract Collection<Object> getExpectedResults() throws Exception;
/**
* Override this method to return an implementation of PostProcessTester if
* the postProcess() method should be compared with the results of another
* implementation.
*
* @return
*/
protected PostProcessTester getPostProcessTester() {
return null;
}
protected void checkResults(EnumSet<CompareResult> cmpOptions)
throws Exception {
for (SiteWrapper site : getSites()) {
getReport().setSiteInfo("=", site.getId());
compareResults(cmpOptions);
}
// run report across all sites
getReport().setSiteInfo("!=", 0);
compareResults(cmpOptions);
}
protected final WritableApplicationService getAppService() {
return dataSource.getAppService();
}
protected final List<SiteWrapper> getSites() throws Exception {
return dataSource.getSites();
}
protected final List<SpecimenTypeWrapper> getSpecimenTypes()
throws Exception {
return dataSource.getSpecimenTypes();
}
protected final List<AliquotedSpecimenWrapper> getAliquotedSpecimens()
throws Exception {
return dataSource.getAliquotedSpecimens();
}
protected final List<SpecimenWrapper> getSpecimens() throws Exception {
return dataSource.getSpecimens();
}
protected final List<ContainerWrapper> getContainers() throws Exception {
return dataSource.getContainers();
}
protected final List<StudyWrapper> getStudies() throws Exception {
return dataSource.getStudies();
}
protected final List<ProcessingEventWrapper> getPatientVisits()
throws Exception {
return dataSource.getPatientVisits();
}
protected final List<PatientWrapper> getPatients() throws Exception {
return dataSource.getPatients();
}
private void testPostProcess(EnumSet<CompareResult> cmpOptions,
Collection<Object> rawResults, List<Object> expectedResults) {
PostProcessTester postProcessTester = getPostProcessTester();
if (postProcessTester != null) {
List<Object> postProcessedRawResults = postProcessTester
.postProcess(getAppService(), rawResults);
compareResults(cmpOptions, expectedResults, postProcessedRawResults);
}
}
private Collection<Object> compareResults(EnumSet<CompareResult> cmpOptions)
throws Exception {
// TODO: logging?
// System.out.print("compareResults(" + cmpOptions + ") for "
// + getReport().getClassName() + " w/ params "
// + Arrays.toString(getReport().getParams().toArray())
// + (isInSite() ? "" : " not") + " in site " + getSiteId());
//
// if ((getReport().getGroupBy() != null)
// && (getReport().getGroupBy().length() > 0)) {
// System.out.print(" grouped by " + getReport().getGroupBy());
// }
//
// if ((getReport().getContainerList() != null)
// && (getReport().getContainerList().length() > 0)) {
// System.out
// .print(" in containers " + getReport().getContainerList());
// }
//
// System.out.println();
Collection<Object> expectedResults = getExpectedResults();
List<Object> actualResults = ReportFactory.createReport(getReport())
.generate(getAppService());
List<Object> postProcessedExpectedResults =
postProcessExpectedResults(expectedResults);
testPostProcess(cmpOptions, expectedResults,
postProcessedExpectedResults);
compareResults(cmpOptions, actualResults, postProcessedExpectedResults);
return postProcessedExpectedResults;
}
private static void compareResults(EnumSet<CompareResult> cmpOptions,
List<Object> actualResults, List<Object> expectedResults) {
// we may only require the actual results to be a subset of
// the expected results, so the actual results must be iterated in an
// outer loop.
Iterator<Object> it = expectedResults.iterator();
int actualResultsSize = 0;
for (Object actualRow : actualResults) {
boolean isFound = false;
if (cmpOptions.contains(CompareResult.ORDER)) {
if (it.hasNext()) {
Object[] next = (Object[]) it.next();
if (datewiseArraysEquals(next, (Object[]) actualRow)) {
isFound = true;
}
}
} else {
for (Object expectedRow : expectedResults) {
if (datewiseArraysEquals((Object[]) expectedRow,
(Object[]) actualRow)) {
isFound = true;
break;
}
}
}
if (!isFound) {
Assert.fail("did not expect this row in actual results: "
+ Arrays.toString((Object[]) actualRow));
} else {
// TODO: logging?
// System.out.println("found: "
// + Arrays.toString((Object[]) actualRow));
}
actualResultsSize++;
}
it = null; // done with this iterator.
// cannot accurately know the size of actual results until they have all
// been run through once, so, do not compare actual size to expected
// size
if (cmpOptions.contains(CompareResult.SIZE)
&& (expectedResults.size() != actualResultsSize)) {
Assert.fail("expected " + expectedResults.size() + " results, got "
+ actualResultsSize);
}
}
/**
* Compare two Object[] references, paying special attention to Object-s
* that implement Date, comparing them using a special function.
*
* @param a1
* @param a2
* @return true if the two arrays are the same length and the Object
* referenced at each corresponding index is equal.
*/
private static boolean datewiseArraysEquals(Object[] a1, Object[] a2) {
if (a1.length != a2.length) {
return false;
}
for (int i = 0; i < a1.length; i++) {
if ((a1[i] instanceof Date) && (a2[i] instanceof Date)) {
if (DateCompare.compare((Date) a1[i], (Date) a2[i]) != 0) {
return false;
}
} else if (a1[i] != null) {
if (!a1[i].equals(a2[i])) {
return false;
}
} else if (a2[i] != null) {
return false;
}
}
return true;
}
private List<Object> postProcessExpectedResults(
Collection<Object> expectedResults) throws ApplicationException {
// post process individual rows BEFORE post processing the entire
// collection, if necessary
List<Object> postProcessedExpectedResults = new ArrayList<Object>(
expectedResults);
// some classes derived from AbstractReport modify the BiobankReport
// objects they are passed, so the BiobankReport params must be
// remembered and restored when a new AbstractReport is created (e.g.
// see InvoicingReportImpl).
List<Object> originalParams = new ArrayList<Object>(getReport()
.getParams());
AbstractReport abstractReport = ReportFactory.createReport(getReport());
getReport().setParams(originalParams); // restore original params
AbstractRowPostProcess rowPostProcessor = abstractReport
.getRowPostProcess();
if (rowPostProcessor != null) {
Object processedRow;
for (int i = 0, numRows = postProcessedExpectedResults.size(); i < numRows; i++) {
processedRow = rowPostProcessor
.rowPostProcess(postProcessedExpectedResults.get(i));
postProcessedExpectedResults.set(i, processedRow);
}
}
// post process the entire expected collection AFTER individual
// rows have been processed
postProcessedExpectedResults = abstractReport.postProcess(
getAppService(), postProcessedExpectedResults);
return postProcessedExpectedResults;
}
/**
* Database may or may not ignore case when comparing strings. All local
* Java String comparisons should use this method so we can easily change to
* match the db's behaviour.
*
* @param left
* @param right
* @return
*/
public static int compareStrings(String left, String right) {
return left.compareToIgnoreCase(right);
}
}