package eu.scape_project.planning.efficiency;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.supercsv.cellprocessor.FmtDate;
import org.supercsv.cellprocessor.Optional;
import org.supercsv.cellprocessor.constraint.DMinMax;
import org.supercsv.cellprocessor.constraint.LMinMax;
import org.supercsv.cellprocessor.constraint.NotNull;
import eu.scape_project.planning.model.Alternative;
import eu.scape_project.planning.model.ChangeLog;
import eu.scape_project.planning.model.DigitalObject;
import eu.scape_project.planning.model.Experiment;
import eu.scape_project.planning.model.IChangesHandler;
import eu.scape_project.planning.model.ITouchable;
import eu.scape_project.planning.model.Plan;
import eu.scape_project.planning.model.PlanProperties;
import eu.scape_project.planning.model.PlanState;
import eu.scape_project.planning.model.ProjectBasis;
import eu.scape_project.planning.model.SampleObject;
import eu.scape_project.planning.model.SampleRecordsDefinition;
import eu.scape_project.planning.model.Values;
import eu.scape_project.planning.model.scales.Scale;
import eu.scape_project.planning.model.tree.Leaf;
import eu.scape_project.planning.model.tree.TreeNode;
import eu.scape_project.planning.model.values.Value;
import eu.scape_project.planning.validation.ValidationError;
public class PlanStatisticsGenerator extends StatisticsGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(PlanStatisticsGenerator.class);
class ChangelogStatistics implements IChangesHandler {
Set<String> users = new HashSet<String>();
@Override
public void visit(ITouchable t) {
String user = t.getChangeLog().getChangedBy();
if (user != null) {
users.add(user);
}
user = t.getChangeLog().getCreatedBy();
if (user != null) {
users.add(user);
}
}
public int getNumberOfUsers() {
return users.size();
}
}
/**
* Creates a new plan statistic generator which loads the plans via the
* provided entity manager and outputs to the given writer.
*
* @param writer
* @param em
* @throws IOException
*/
public PlanStatisticsGenerator(Writer writer, EntityManager em) throws IOException {
super(writer, em);
setupColumns();
// write the header
listWriter.writeHeader(headers);
}
/**
* Writes the statistics of the given plan to the writer.
*
* @param plan
* @throws IOException
*/
public void writeStatistics(Plan plan) throws IOException {
PlanStatistics stats = generateStatistics(plan);
listWriter.write(stats, headers, processors);
listWriter.flush();
}
/**
* Generates the statistics for the given plan.
*
* @param plan
* @return
*/
private PlanStatistics generateStatistics(Plan plan) {
PlanProperties properties = plan.getPlanProperties();
ProjectBasis basis = plan.getProjectBasis();
SampleRecordsDefinition samples = plan.getSampleRecordsDefinition();
long numSamples = samples.getRecords().size();
List<Leaf> leaves = plan.getTree().getRoot().getAllLeaves();
long numLeaves = leaves.size();
long numMappedLeaves = 0;
long numMeasuresNeededTotal = 0;
long numEvaluated = 0;
long numAlternatives = plan.getAlternativesDefinition().getAlternatives().size();
long numConsideredAlternatives = plan.getAlternativesDefinition().getConsideredAlternatives().size();
// determine the number of users who have been working on the plan
ChangelogStatistics changelogStatistics = new ChangelogStatistics();
plan.handleChanges(changelogStatistics);
int numDistinctUsers = changelogStatistics.getNumberOfUsers();
double percentageDefinedTransformers = 0.0;
List<String> consideredAltNames = new ArrayList<String>();
for (Alternative alt : plan.getAlternativesDefinition().getConsideredAlternatives()) {
consideredAltNames.add(alt.getName());
}
List<ValidationError> errors = new ArrayList<ValidationError>();
for (Iterator<Leaf> iter = leaves.iterator(); iter.hasNext();) {
Leaf l = iter.next();
if (l.isMapped()) {
numMappedLeaves++;
}
int numMeasuresNeeded = 0;
if (l.isSingle()) {
numMeasuresNeeded += numConsideredAlternatives;
} else {
numMeasuresNeeded += (numConsideredAlternatives * numSamples);
}
numMeasuresNeededTotal += numMeasuresNeeded;
for (Iterator<String> alts = consideredAltNames.iterator(); alts.hasNext();) {
String alt = alts.next();
Values values = l.getValues(alt);
if (values != null) {
for (Iterator<Value> valueiter = values.getList().iterator(); valueiter.hasNext();) {
Value value = valueiter.next();
if ((value != null) && (value.getScale() != null) && value.isEvaluated()) {
numEvaluated++;
}
}
}
}
if (l.getTransformer() != null) {
if (l.isCompletelyTransformed(errors)) {
percentageDefinedTransformers += 1;
}
}
}
percentageDefinedTransformers = percentageDefinedTransformers / numLeaves;
double percentagePopulatedValues = (numMeasuresNeededTotal == 0) ? 0.0 : numEvaluated
/ (double) numMeasuresNeededTotal;
String creatorUsername = properties.getOwner();
String creatorEmail = null;
try {
if (em != null) {
creatorEmail = em.createQuery("select email from User where username = :username", String.class)
.setParameter("username", creatorUsername).getSingleResult();
}
} catch (Exception e) {
LOGGER.debug("Failed to retrieve creator email.", e);
}
int numPlansCreated = 0;
try {
if (em != null) {
numPlansCreated = (em.createQuery("select count(*) from PlanProperties p where p.owner = :username",
Long.class).setParameter("username", creatorUsername).getSingleResult()).intValue();
}
} catch (Exception e) {
LOGGER.debug("Failed to determine number of plans created", e);
}
Date decisionOn = null;
if (plan.getRecommendation().getAlternative() != null) {
decisionOn = new Date(plan.getRecommendation().getChangeLog().getChanged()); // decision
// on
}
PlanStatistics statistics = new PlanStatistics(
plan.getId(),
properties.getId(),
creatorUsername,
properties.getAuthor(),
creatorEmail,
properties.getName(),
properties.getState().getValue(), // "status"
new Date(plan.getChangeLog().getCreated()), // created on
decisionOn, // decision on
properties.getChangeLog().getChanged(), // "stage last accessed"
properties.getState().getValue(), // "highest stage achieved"
numSamples, // # samples
numLeaves, // # leaves
numMappedLeaves, // # mapped leaves
numMeasuresNeededTotal, // # measures needed
numAlternatives, // # alternatives
StringUtils.length(basis.getDocumentTypes()), StringUtils.length(properties.getDescription()),
StringUtils.length(basis.getMandate()), StringUtils.length(basis.getPlanningPurpose()),
StringUtils.length(basis.getDesignatedCommunity()), StringUtils.length(basis.getApplyingPolicies()),
StringUtils.length(basis.getOrganisationalProcedures()), StringUtils.length(basis.getPreservationRights()),
StringUtils.length(basis.getReferenceToAgreements()), StringUtils.length(basis.getPlanRelations()),
StringUtils.length(basis.getTriggers().getNewCollection().getDescription()), StringUtils.length(basis
.getTriggers().getPeriodicReview().getDescription()), StringUtils.length(basis.getTriggers()
.getChangedEnvironment().getDescription()), StringUtils.length(basis.getTriggers()
.getChangedObjective().getDescription()), StringUtils.length(basis.getTriggers()
.getChangedCollectionProfile().getDescription()), StringUtils.length(samples.getCollectionProfile()
.getDescription()), StringUtils.length(samples.getCollectionProfile().getTypeOfObjects()),
StringUtils.length(samples.getCollectionProfile().getExpectedGrowthRate()), StringUtils.length(samples
.getCollectionProfile().getRetentionPeriod()), StringUtils.length(samples.getSamplesDescription()),
StringUtils.length(plan.getRequirementsDefinition().getDescription()),
// mean of leaf-comment length ?
StringUtils.length(plan.getAlternativesDefinition().getDescription()), StringUtils.length(plan
.getDecision().getReason()), StringUtils.length(plan.getDecision().getActionNeeded()),
// mean of alternative description length ?
StringUtils.length(plan.getEvaluation().getComment()),
// mean of comment length of evaluation values?
StringUtils.length(plan.getImportanceWeighting().getComment()),
// and percentage of leaves with changed weight?
StringUtils.length(plan.getRecommendation().getReasoning()), StringUtils.length(plan.getRecommendation()
.getEffects()), StringUtils.length(plan.getPlanDefinition().getCostsRemarks()),
percentagePopulatedValues, percentageDefinedTransformers, numDistinctUsers, numPlansCreated);
// and add information on changelogs
analyseChangelogs(statistics, plan);
return statistics;
}
private void analyseChangelogs(PlanStatistics statistics, Plan plan) {
long[] minChangeLogs = new long[PlanStatistics.MAX_STATE + 1];
long[] maxChangeLogs = new long[PlanStatistics.MAX_STATE + 1];
PlanState maxState = plan.getPlanProperties().getState();
long createdAt = plan.getChangeLog().getCreated();
// 0 CREATED
minChangeLogs[0] = createdAt;
maxChangeLogs[0] = createdAt;
// 1 INITIALISED
minChangeLogs[1] = createdAt;
maxChangeLogs[1] = createdAt;
// 2 Define Basis - BASIS_DEFINED(2)
// - ProjectBasis
minChangeLogs[2] = plan.getProjectBasis().getChangeLog().getChanged();
maxChangeLogs[2] = minChangeLogs[2];
// 3. Define Sample Objects - RECORDS_CHOSEN(3)
// - SampleRecordsDefinition
// - samples (also created here)
long minCL = plan.getSampleRecordsDefinition().getChangeLog().getChanged();
long maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
for (SampleObject sample : plan.getSampleRecordsDefinition().getRecords()) {
ChangeLog sampleCL = sample.getChangeLog();
if (sampleCL.getChanged() > maxCL) {
maxCL = sampleCL.getChanged();
}
if (sampleCL.getCreated() < minCL) {
minCL = sampleCL.getCreated();
}
}
minChangeLogs[3] = minCL;
maxChangeLogs[3] = maxCL;
// 4. Identify requirements - TREE_DEFINED(4, "Tree Defined"),
// - nodes/leaves (also created!)
// - scales
// - RequirementsDefinition
minCL = plan.getRequirementsDefinition().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
List<TreeNode> nodes = plan.getTree().getRoot().getAllChildren();
for (TreeNode treeNode : nodes) {
ChangeLog nodeCL = treeNode.getChangeLog();
// we can only consider the creation time stamp, as changed is used
// to confirm values
if (minCL > nodeCL.getCreated()) {
minCL = nodeCL.getCreated();
}
if (maxCL < nodeCL.getCreated()) {
maxCL = nodeCL.getCreated();
}
// we can only use the scales of leaves, as
// - nodes can be changed when weighting is changed
// - leaves are "changed" when evaluated, aggregation mode is
// changed
if (treeNode instanceof Leaf) {
Scale scale = ((Leaf) treeNode).getScale();
if (scale != null) {
ChangeLog scaleCL = scale.getChangeLog();
if (maxCL < scaleCL.getChanged()) {
maxCL = scaleCL.getChanged();
}
}
}
}
minChangeLogs[4] = minCL;
maxChangeLogs[4] = maxCL;
// 5. Define alternatives - ALTERNATIVES_DEFINED(5,
// "Alternatives Defined")
// - AlternativesDefinition
// - Alternative - only created, "changed" can be overwritten in
// GoDecision)
minCL = plan.getAlternativesDefinition().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
for (Alternative alternative : plan.getAlternativesDefinition().getAlternatives()) {
ChangeLog altCL = alternative.getChangeLog();
if (minCL > altCL.getCreated()) {
minCL = altCL.getCreated();
}
if (maxCL < altCL.getCreated()) {
maxCL = altCL.getCreated();
}
// we cannot check for the changed(), as the alternative is also
// changed in GoDecision
}
minChangeLogs[5] = minCL;
maxChangeLogs[5] = maxCL;
// 6. Take go decision - GO_CHOSEN(6, "Go Decision Taken")
// - no: Alternative (! - if discarded)
// - GoDecision
minCL = plan.getDecision().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
// for (Alternative alternative :
// plan.getAlternativesDefinition().getAlternatives()) {
// ChangeLog altCL = alternative.getChangeLog();
// if (maxCL < altCL.getChanged()) {
// maxCL = altCL.getChanged();
// }
// }
minChangeLogs[6] = minCL;
maxChangeLogs[6] = maxCL;
// 7. Develop Experiments - EXPERIMENT_DEFINED(7, "Experiments Defined")
// Alternative.Experiment
minCL = Long.MAX_VALUE;
maxCL = Long.MIN_VALUE;
// as the experiment is created together with the alternative, this
// might result in the same timestamps
for (Alternative alternative : plan.getAlternativesDefinition().getAlternatives()) {
ChangeLog expCL = alternative.getExperiment().getChangeLog();
if (maxCL < expCL.getChanged()) {
maxCL = expCL.getChanged();
}
if (minCL > expCL.getChanged()) {
minCL = expCL.getChanged();
}
}
minChangeLogs[7] = minCL;
maxChangeLogs[7] = maxCL;
// 8. Run Experiments - EXPERIMENT_PERFORMED(8, "Experiments Performed")
// - detailedExperimentInfo
// - result files (alternative.experiment.results)
// - initValues on save
minCL = Long.MAX_VALUE;
maxCL = Long.MIN_VALUE;
// as the experiment is created together with the alternative, this
// might result in the same timestamps
for (Alternative alternative : plan.getAlternativesDefinition().getAlternatives()) {
Experiment experiment = alternative.getExperiment();
// - detailedExperimentInfo
// for (DetailedExperimentInfo detailedInfo :
// experiment.getDetailedInfo().values()) {
// ChangeLog diCL = detailedInfo.getChangeLog();
// if (maxCL < diCL.getChanged()) {
// maxCL = diCL.getChanged();
// }
// if (minCL > diCL.getChanged()) {
// minCL = diCL.getChanged();
// }
// }
// - result files (alternative.experiment.results)
for (DigitalObject result : experiment.getResults().values()) {
ChangeLog resultCL = result.getChangeLog();
if (maxCL < resultCL.getChanged()) {
maxCL = resultCL.getChanged();
}
if (minCL > resultCL.getChanged()) {
minCL = resultCL.getChanged();
}
}
// - initValues on save
// cannot be used, as automatic evaluators replace the value objects
// for (TreeNode treeNode : nodes) {
// ChangeLog nodeCL = treeNode.getChangeLog();
// if (treeNode instanceof Leaf) {
// Leaf leaf = (Leaf)treeNode;
// for (Values values : leaf.getValueMap().values()) {
// for (Value value : values.getList()) {
// ChangeLog valueCL = value.getChangeLog();
// // only the creation time of the value is relevant!
// if (maxCL.getChanged() < valueCL.getCreated()) {
// maxCL = valueCL;
// }
// }
// }
// }
// }
}
minChangeLogs[8] = minCL;
maxChangeLogs[8] = maxCL;
// 9. Evaluate Experiments - RESULTS_CAPTURED(9, "Results Captured")
// - Evaluation
// - ValueMap-Values
minCL = plan.getEvaluation().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
for (TreeNode treeNode : nodes) {
if (treeNode instanceof Leaf) {
Leaf leaf = (Leaf) treeNode;
// ValueMap-Values
for (Values values : leaf.getValueMap().values()) {
for (Value value : values.getList()) {
ChangeLog valueCL = value.getChangeLog();
if (maxCL < valueCL.getChanged()) {
maxCL = valueCL.getChanged();
}
}
}
// values are created in a previous step, therefore the min
// values cannot be set regarding the created timestamp
}
}
minChangeLogs[9] = minCL;
maxChangeLogs[9] = maxCL;
// 10. Transform measured values - TRANSFORMATION_DEFINED(10,
// "Transformations Defined")
// - leaf.transformer
// - Transformation
// - leaf (!! - leaf.aggregationMode )
minCL = plan.getTransformation().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
for (TreeNode treeNode : nodes) {
if (treeNode instanceof Leaf) {
Leaf leaf = (Leaf) treeNode;
if (leaf.getTransformer() != null) {
ChangeLog tCL = leaf.getTransformer().getChangeLog();
if (maxCL < tCL.getChanged()) {
maxCL = tCL.getChanged();
}
if (minCL > tCL.getChanged()) {
minCL = tCL.getChanged();
}
}
}
}
minChangeLogs[10] = minCL;
maxChangeLogs[10] = maxCL;
// 11. Set importance factors - WEIGHTS_SET(11, "Weights Set")
// - no: node (! - lock )
// - ImportanceWeighting
minCL = plan.getImportanceWeighting().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
minChangeLogs[11] = minCL;
maxChangeLogs[11] = maxCL;
// 12. Analyse results - ANALYSED(12, "Analyzed")
// - Recommendation
minCL = plan.getRecommendation().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
minChangeLogs[12] = minCL;
maxChangeLogs[12] = maxCL;
//
// 13. Create Executable plan - EXECUTEABLE_PLAN_CREATED(13,
// "Executable Plan Created")
// - ExecutablePlanDefinition
minCL = plan.getExecutablePlanDefinition().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
minChangeLogs[13] = minCL;
maxChangeLogs[13] = maxCL;
//
// 14. Define Plan - PLAN_DEFINED(14, "Plan Defined")
// - PlanDefinition
minCL = plan.getPlanDefinition().getChangeLog().getChanged();
maxCL = minCL;
if (minCL <= createdAt) {
minCL = Long.MAX_VALUE;
}
minChangeLogs[14] = minCL;
maxChangeLogs[14] = maxCL;
// 15. Validate Plan
if (plan.getPlanProperties().getState() == PlanState.PLAN_VALIDATED) {
minCL = plan.getPlanProperties().getChangeLog().getChanged();
} else {
minCL = Long.MAX_VALUE;
}
minChangeLogs[15] = minCL;
maxChangeLogs[15] = minCL;
// for (int i =0; i <= PlanStatistics.MAX_STATE; i ++) {
// if (minChangeLogs[i] == Long.MAX_VALUE) {
// minChangeLogs[i] = 0L;
// }
// if (maxChangeLogs[i] == Long.MIN_VALUE) {
// maxChangeLogs[i] = 0L;
// }
// }
// calculate durations of each step
for (int stage = 1; stage < PlanStatistics.MAX_STATE; stage++) {
long enter = minChangeLogs[stage];
long exit = minChangeLogs[stage + 1];
if (LUndef.isDefined(enter) && LUndef.isDefined(exit)) {
// in minutes, not milliseconds!
// long duration = exit - enter;
// from plan creation to exit - in hours
long duration = exit - createdAt;
statistics.getPhaseDurations()[stage] = duration / (60 * 60 * 1000);
} else {
statistics.getPhaseDurations()[stage] = Long.MAX_VALUE;
}
}
minCL = minChangeLogs[PlanState.GO_CHOSEN.getValue()];
if (LUndef.isDefined(minCL)) {
statistics.setToDecision((minCL - createdAt) / (60 * 60 * 1000));
} else {
statistics.setToDecision(Long.MIN_VALUE);
}
minCL = minChangeLogs[PlanState.PLAN_VALIDATED.getValue()];
if (LUndef.isDefined(minCL)) {
statistics.setToCompletion((minCL - createdAt) / (60 * 60 * 1000));
} else {
statistics.setToCompletion(Long.MIN_VALUE);
}
if (maxState != PlanState.PLAN_VALIDATED) {
for (int i = PlanStatistics.MAX_STATE - 1; i >= 1; i--) {
if (LUndef.isDefined(maxChangeLogs[i]) && (maxChangeLogs[i] > createdAt) && (i > maxState.getValue())) {
maxState = PlanState.valueOf(i);
}
}
}
statistics.setHighestStateAchieved(maxState.getValue());
}
protected void setupColumns() {
addColumn("id", new LUndef());
addColumn("propertyId", new LUndef());
addColumn("creatorUsername", new NotNull());
addColumn("creatorName", new NotNull());
addColumn("creatorEmail", new Optional());
addColumn("name", new NotNull());
addColumn("state", new LMinMax(0L, PlanState.PLAN_VALIDATED.getValue()));
addColumn("highestStateAchieved", new LMinMax(0L, PlanState.PLAN_VALIDATED.getValue()));
addColumn("createdOn", new FmtDate("yyyy.MM.dd HH:mm:ss"));
addColumn("decisionOn", new Optional(new FmtDate("yyyy.MM.dd HH:mm:ss")));
addColumn("toDecision", new LUndef());
addColumn("toCompletion", new LUndef());
addColumn("numOfSamples", new LUndef());
addColumn("numOfLeaves", new LUndef());
addColumn("numOfMappedLeaves", new LUndef());
addColumn("numOfMeasurementNeeded", new LUndef());
addColumn("numOfAlternatives", new LUndef());
addColumn("percentagePopulatedValues", new DMinMax(0.0, 10.0));
addColumn("percentageDefinedTransformers", new DMinMax(0.0, 10.0));
addColumn("numDistinctUsers", new LUndef());
addColumn("numPlansCreated", new LUndef());
addColumn("phase1", new LUndef());
addColumn("phase2", new LUndef());
addColumn("phase3", new LUndef());
addColumn("phase4", new LUndef());
addColumn("phase5", new LUndef());
addColumn("phase6", new LUndef());
addColumn("phase7", new LUndef());
addColumn("phase8", new LUndef());
addColumn("phase9", new LUndef());
addColumn("phase10", new LUndef());
addColumn("phase11", new LUndef());
addColumn("phase12", new LUndef());
addColumn("phase13", new LUndef());
addColumn("phase14", new LUndef());
addColumn("phase15", new LUndef());
addColumn("lengthDefineBasis", new LUndef());
addColumn("lengthDefineSamples", new LUndef());
addColumn("lengthRequirementsDefinitionDescription", new LUndef());
addColumn("lengthAlternativesDefinitionDescription", new LUndef());
addColumn("lengthDecision", new LUndef());
addColumn("lengthEvaluationComment", new LUndef());
addColumn("lengthImportanceWeightingComment", new LUndef());
addColumn("lengthRecommendationReasoning", new LUndef());
addColumn("lengthRecommendationEffects", new LUndef());
addColumn("lengthPlanDefinitionCostsRemarks", new LUndef());
addColumn("lengthDocumentTypes", new LUndef());
addColumn("lengthPropertiesDescription", new LUndef());
addColumn("lengthBasisMandate", new LUndef());
addColumn("lengthBasisPlanningPurpose", new LUndef());
addColumn("lengthBasisDesignatedCommunity", new LUndef());
addColumn("lengthBasisApplyingPolicies", new LUndef());
addColumn("lengthBasisOrganisationalProcedures", new LUndef());
addColumn("lengthBasisPreservationRights", new LUndef());
addColumn("lengthBasisReferenceToAgreements", new LUndef());
addColumn("lengthBasisPlanRelations", new LUndef());
addColumn("lengthTriggersNewCollection", new LUndef());
addColumn("lengthTriggersPeriodicReview", new LUndef());
addColumn("lengthTriggersChangedEnvironment", new LUndef());
addColumn("lengthTriggersChangedObjective", new LUndef());
addColumn("lengthTriggersChangedCollectionProfile", new LUndef());
addColumn("lengthCollectionProfileDescription", new LUndef());
addColumn("lengthCollectionProfileTypeOfObjects", new LUndef());
addColumn("lengthCollectionProfileExpectedGrowthRate", new LUndef());
addColumn("lengthCollectionProfileRetentionPeriod", new LUndef());
addColumn("lengthSamplesDescription", new LUndef());
addColumn("lengthDecisionReason", new LUndef());
addColumn("lengthDecisionActionNeeded", new LUndef());
finishColumns();
}
}