/*******************************************************************************
* Copyright 2006 - 2012 Vienna University of Technology,
* Department of Software Technology and Interactive Systems, IFS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This work originates from the Planets project, co-funded by the European Union under the Sixth Framework Programme.
******************************************************************************/
package eu.scape_project.planning.model.tree;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.persistence.CascadeType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.validation.Valid;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.scape_project.planning.model.Alternative;
import eu.scape_project.planning.model.EvaluationStatus;
import eu.scape_project.planning.model.IChangesHandler;
import eu.scape_project.planning.model.ITouchable;
import eu.scape_project.planning.model.SampleAggregationMode;
import eu.scape_project.planning.model.TargetValueObject;
import eu.scape_project.planning.model.Values;
import eu.scape_project.planning.model.measurement.EvaluationScope;
import eu.scape_project.planning.model.measurement.Measure;
import eu.scape_project.planning.model.scales.FloatRangeScale;
import eu.scape_project.planning.model.scales.FreeStringScale;
import eu.scape_project.planning.model.scales.IntRangeScale;
import eu.scape_project.planning.model.scales.OrdinalScale;
import eu.scape_project.planning.model.scales.PositiveFloatScale;
import eu.scape_project.planning.model.scales.PositiveIntegerScale;
import eu.scape_project.planning.model.scales.Scale;
import eu.scape_project.planning.model.scales.ScaleType;
import eu.scape_project.planning.model.scales.YanScale;
import eu.scape_project.planning.model.transform.NumericTransformer;
import eu.scape_project.planning.model.transform.OrdinalTransformer;
import eu.scape_project.planning.model.transform.Transformer;
import eu.scape_project.planning.model.values.FreeStringValue;
import eu.scape_project.planning.model.values.INumericValue;
import eu.scape_project.planning.model.values.IOrdinalValue;
import eu.scape_project.planning.model.values.TargetValue;
import eu.scape_project.planning.model.values.TargetValues;
import eu.scape_project.planning.model.values.Value;
import eu.scape_project.planning.validation.ValidationError;
/**
* A leaf node in the objective tree does not contain any children, but instead
* defines the actual measurement scale to be used and points to conforming
* valueMap. Part of the implementation of the Composite design pattern, cf.
* TreeNode, Node - Leaf corresponds to the <code>Leaf</code>, surprise!
*
* @author Christoph Becker
*/
@Entity
@NamedQuery(name = "getLaevesById", query = "SELECT l from Leaf l WHERE id IN (:leafList)")
@DiscriminatorValue("L")
public class Leaf extends TreeNode {
private static final long serialVersionUID = -6561945098296876384L;
private static final Logger log = LoggerFactory.getLogger(Leaf.class);
/**
* The {@link Transformer} stores the user-set transformation rules. There
* are two types:
* <ul>
* <li>numeric transformation (thresholds)</li>
* <li>ordinal transformation: direct mapping from values to numeric values.
* This also applies to boolean scales.</li>
*/
@OneToOne(cascade = CascadeType.ALL)
private Transformer transformer;
/**
* determines the aggregation mode for the values of the sample records(!)
* WITHIN one alternative. The overall aggregation method over the tree is a
* different beer! Is initialised with {@link SampleAggregationMode#WORST},
* but later initialised according to the {@link Scale} in
* {@link #setDefaultAggregation()}
*/
@Enumerated
private SampleAggregationMode aggregationMode = SampleAggregationMode.WORST;
/**
* specifies the {@link Scale} to be used for evaluating experiment outcomes
*/
@Valid
@OneToOne(cascade = CascadeType.ALL)
private Scale scale;
/**
* We have values actually per
* <ul>
* <li>preservation strategy ({@link Alternative}),</li>
* <li>decision criteria (leaf node), AND</li>
* <li>sample record.</li>
* </ul>
* So we have another encapsulation: {@link Values}
*
* Note: For some databases it might be necessary to rename the key member
* of Map, as it might be a reserved keyword, e.g.: Derby
*/
// @IndexColumn(name = "key_name")
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Map<String, Values> valueMap = new HashMap<String, Values>();
/**
* The measure this decision criterion is mapped to.
*
* Note that orphanRemoval does not work on OneToOne relationships if the
* orphan is replaced by a new entity ({@link https
* ://hibernate.onjira.com/browse/HHH-6484} If you want to do so, you have
* to take care of deleting the orphan yourself
*
*/
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
private Measure measure;
public Map<String, Values> getValueMap() {
return valueMap;
}
public void setValueMap(Map<String, Values> v) {
this.valueMap = v;
}
/**
* @return the <b>unweighted</b> result value for an Alternative. This is
* the aggregation of all transformed evaluation values
* @see #aggregateValues(TargetValues)
* @see #transformValues(Alternative)
*/
public double getResult(Alternative a) {
return aggregateValues(transformValues(a));
}
/**
* Aggregates values of one Alternative, depending on the
* {@link #aggregationMode}
*
* @param values
* the TargetValue element over which aggregation shall be
* performed according to the {@link #aggregationMode}
* @return a single number denoting the aggregated, transformed, unweighted
* result value of this Leaf.
*/
private double aggregateValues(TargetValues values) {
if (aggregationMode == SampleAggregationMode.WORST) {
return values.worst();
} else {
return values.average();
}
}
/**
* Returns the {@link TargetValues evaluation values} for each SampleObject
* for one {@link Alternative} already transformed from the measurement
* scale to the final scale used for ranking.
*
* @see #getResult(Alternative)
* @param a
* the {@link Alternative} for which evaluation values shall be
* returned
* @return {@link TargetValues}
*/
public TargetValues transformValues(Alternative a) {
Values v = valueMap.get(a.getName());
if (transformer == null) {
log.error("transformer is null!");
}
return transformer.transformValues(v);
}
public Leaf() {
}
public Transformer getTransformer() {
return transformer;
}
public void setTransformer(Transformer transformer) {
this.transformer = transformer;
}
public void setValues(String alternative, Values values) {
valueMap.put(alternative, values);
}
public Values getValues(String alternative) {
return valueMap.get(alternative);
}
public Scale getScale() {
return scale;
}
/**
* The standard setter sets the scale of the leaf to the given instance
* <code>scale</code>, but leaves {@link #transformer} and
* {@link #aggregationMode} unchanged.
*
* <b>Important: If you want to change the type of the scale, e.g. from
* Boolean to Numeric, you have to take transformation settings and
* aggregation mode into account. Thus you need to use
* {@link #changeScale(Scale)} instead, which also takes care of the
* transformer and aggregationMode.</b>
*
* @param scale
*/
public void setScale(Scale scale) {
this.scale = scale;
}
/**
* When a scale is changed e.g. from Boolean to a number, all evaluation
* values that have already been associated become invalid and need to be
* removed.
*
* This function resets all evaluation {@link Values} associated with this
* Leaf, which depend on the {@link Scale} that is set. This means that if
* the scale is not set, all Values are removed. If the scale is set, we
* iterate into all values for all alternatives and samplerecords and check
* if the scale in there differs from the scale that has been set. If yes,
* we remove the values. Furthermore, if this Leaf has been changed from an
* Object criterion to an Action criterion, all excess values are removed.
*/
public void resetValues(List<Alternative> list) {
if (scale == null) {
/*
* there is no scaletype set, so we remove existing values
*/
valueMap.clear();
return;
}
// Get the Values for each Alternative
for (Alternative a : list) {
Values values = valueMap.get(a.getName());
if (values == null) {
log.debug("values is null for alternative " + a.getName() + " in Leaf " + name);
continue;
}
// Check value of each sample object for conformance with Scale -
// if we find a changed scale, we reset everything.
// It might be faster not to check ALL values, but this is safer.
for (Value value : values.getList()) {
// If the scale has changed, we reset all evaluation values of
// this Alternative:
// this may look strange, but it is OK that the scale of a value
// is null.
// If there have been values before, you change the scale and
// then save - the linkage is lost
// if (value.getScale() == null) {
// LogFactory.getLog(Leaf.class).error("WHAT THE...?? no scale for value"+getName());
// } else {
if ((value.getScale() == null) || (!value.getScale().getClass().equals(scale.getClass()))) {
if (!a.isDiscarded()) { // for discarded alternatives,
// that's ok.
log.debug("Leaf " + this.getName() + " Class: " + value.getClass() + " not like "
+ scale.getClass() + ". RESETTING the valuemap now!");
valueMap.clear(); // reset all values
return;
}
}
// }
// PLEASE NOTE- WRT ORDINAL RESTRICTIONS:
// we do NOT reset values when the restriction has changed, such
// as
// the ordinal values or the boundaries.
// Instead, those values that are still valid remain, the others
// will be checked
// and need to be corrected anyway in the evaluate step.
// Should be nicer for the user. If we find out this leads to
// validation problems
// (which shouldnt be the case because the data types are valid
// as long as the scale
// doesnt change) then we will reset the values even if just the
// restriction changes.
}
/*
* maybe this leaf was set to single, reset all values
*/
if (isSingle() && values.size() > 1) {
valueMap.clear();
return;
}
}
}
/**
* Sets a default transformer corresponding to the current scale of this
* leaf. The transformer is initialized with default-values.
*
* If no scale is set, the current transformer will be set to null!
*/
public void setDefaultTransformer() {
if (scale == null) {
log.warn("Can't set DefaultTransformer, no scale set!");
this.setTransformer(null);
return;
}
if (ScaleType.ordinal.equals(scale.getType())) {
OrdinalTransformer t = new OrdinalTransformer();
this.setTransformer(t);
if (!(scale instanceof FreeStringScale)) {
Map<String, TargetValueObject> map = t.getMapping();
OrdinalScale o = (OrdinalScale) scale;
for (String s : o.getList()) {
map.put(s, new TargetValueObject());
}
}
} else {
NumericTransformer t = new NumericTransformer();
this.setTransformer(t);
}
}
/**
* Returns the fully qualified class-name ("canonical name") of the current
* scale
*
* @return the canonical classname of the scale, or null if no scale is set
*/
public String getScaleByClassName() {
if (scale == null)
return null;
else
return scale.getClass().getCanonicalName();
}
/**
* Sets the Scale according to the provided name, IF the name differs from
* the classname of the currently set {@link #scale}
*
* resets property mappings, if present.
*
* @param className
* canonical class name of the new scale
*/
public void setScaleByClassName(String className) {
Scale scaleType = null;
try {
if (className != null && !"".equals(className)) {
scaleType = (Scale) Class.forName(className).newInstance();
}
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
} catch (ClassNotFoundException e) {
}
changeScale(scaleType);
}
/**
* Changes the {@link Scale} to the provided one. if the new scale differs
* from the type of the current scale, it also:
* <ul>
* <li>sets: default aggregators and transformers.</li>
* </ul>
* It does not set a reference to the provided scale, but clones it instead!
*
* @param newScale
* the new Scale to be set
*/
public void changeScale(Scale newScale) {
if (newScale == null) {
log.debug("CHECK THIS: setting scale to null.");
scale = null;
// remove mapping
setMeasure(null);
} else {
// If
if ((this.scale == null) // we don't have a scale yet
|| (!scale.getClass().getName().equals(newScale.getClass().getName())))
// the new scale is not the same as ours
{
// a new scale was chosen, remove mapping
setMeasure(null);// new Criterion());
setScale(newScale.clone());
setDefaultAggregation();
if (scale != null) {
setDefaultTransformer();
}
}
}
}
/**
* Applies the given measure to this leaf, and adjusts scale and single
* properly
*
* @param m
*/
public void applyMeasure(final Measure m) {
adjustScale(m.getScale());
setMeasure(new Measure(m));
setSingle(m.getAttribute().getCategory().getScope() == EvaluationScope.ALTERNATIVE_ACTION);
if (StringUtils.isEmpty(name)) {
setName(m.getName());
}
touchIncludingScale();
}
/**
* is used to adjust the scale of this leaf to its mapping - the type of the
* new scale has already been checked, mapping information is not discarded.
* - a new scale is created, even the types of the current and the new Scale
* match (to get clean aggregation and transformer values)
*
* @param newScale
*/
public void adjustScale(Scale newScale) {
if (newScale == null) {
log.debug("CHECK THIS: try to setg scale to null due to measurement info: this should NOT happen at all.");
} else {
if ((this.scale == null) // we don't have a scale yet
|| (!scale.getClass().getName().equals(newScale.getClass().getName())))
// the new scale is not the same as ours
{
setScale(newScale.clone());
setDefaultAggregation();
if (scale != null) {
setDefaultTransformer();
}
}
}
}
/**
* sets the {@link #aggregationMode} depending on {@link #scale}. For all
* ordinal scales we set it to using the worst result, and for numeric
* scales we use the average result
*
* @see SampleAggregationMode
*/
private void setDefaultAggregation() {
if (scale instanceof OrdinalScale) {
setAggregationMode(SampleAggregationMode.WORST);
} else { // numeric
setAggregationMode(SampleAggregationMode.AVERAGE);
}
}
@Override
/**
* This is a leaf, so: YES, I am.
* @return true
*/
public boolean isLeaf() {
return true;
}
public SampleAggregationMode getAggregationMode() {
return aggregationMode;
}
public void setAggregationMode(SampleAggregationMode aggregationMode) {
this.aggregationMode = aggregationMode;
}
/**
* unused at the moment. TODO checking the size of the valuemap is not
* enough.
*/
public EvaluationStatus getEvaluationStatus() {
return (valueMap.size() > 0) ? EvaluationStatus.COMPLETE : EvaluationStatus.NONE;
}
/**
* Unused at the moment.
*
* @return the transformation status. TODO checking transformer for null
* state is NOT enough
*/
public EvaluationStatus getTransformationStatus() {
return (transformer != null) ? EvaluationStatus.COMPLETE : EvaluationStatus.NONE;
}
/**
* removes associated evaluation {@link Values} for a given list of
* alternatives and a give record index.
*
* @param list
* list of Alternatives for which values shall be removed
* @param record
* index of the record for which values shall be removed
*/
public void removeValues(List<Alternative> list, int record) {
for (Alternative a : list) {
Values v = getValues(a.getName());
// maybe this alternative has no values at all - e.g. because it was
// just created
if ((v != null) // there is a Values object
&& (v.getList().size() > record) // there can be a value for
// this sample record
&& (v.getList().get(record) != null)) { // there is a value
log.debug("removing values:: " + getName() + " ," + record + ", " + a.getName());
v.getList().remove(record);
}
}
}
/**
* The value map is properly initialized if its size equals the number of
* alternatives and the number of values equals the number of records.
*
* @return true if value map is properly initialized
*/
@Override
public boolean isValueMapProperlyInitialized(List<Alternative> alternatives, int numberRecords) {
if (valueMap.size() != alternatives.size()) {
return false;
}
for (Alternative a : alternatives) {
if (!valueMap.keySet().contains(a.getName())) {
return false;
}
}
for (String a : valueMap.keySet()) {
if (!isSingle() && valueMap.get(a).size() != numberRecords) {
return false;
} else if (isSingle() && valueMap.get(a).size() != 1) {
return false;
}
}
return true;
}
/**
* Creates empty Values for all Alternatives and SampleRecords as provided
* in the parameters, PLUS ensures that values are linked to scales if the
* parameter addLinkage is true
*
* An assumption here is that other methods take care of removing values
* when removing records ({@link #removeValues(List, int)}), and of
* resetting values when changing scales and from object to action
* criterion. ({@link #resetValues()}) These methods need to be called when
* manipulating the object model.
*
* @param list
* of Alternatives
* @param records
* The number of records determines how many {@link Values} are
* created and associated for every {@link Alternative}
* @param addLinkage
* If true, ensure that values are linked to scales by calling
* {@link #initScaleValueLinkage(List, int)}
*/
public void initValues(List<Alternative> list, int records, boolean addLinkage) {
/**
* maybe we have not completed the step identify requirements yet - so
* there might be no scales!
**/
if (scale == null)
return;
for (Alternative a : list) {
// for every Alternative we get the container of the values of each
// sample object
// from the map
Values v = valueMap.get(a.getName());
// If it doesnt exist, we create it and link it in the map
if (v == null) {
v = new Values();
valueMap.put(a.getName(), v);
// it the valueMap has just been created and the leaf is single,
// we need to add one value.
if (isSingle()) {
v.add(scale.createValue());
}
}
// 20090217, hotfix CB: if a Leaf is set to SINGLE *after*
// initValues has been called,
// the Value object at position 0 of the ValueS object might not be
// properly initialised.
// Check and initialise if needed:
if (isSingle()) {
if (v.size() == 0) {
log.warn("adding value to a SINGLE LEAF WITH A VALUES OBJECT WITHOUT A PROPER VALUE:" + getName());
v.getList().add(scale.createValue());
} else {
if (v.getValue(0) == null) {
log.warn("adding value to a SINGLE LEAF WITH A VALUES OBJECT WITHOUT A PROPER VALUE:"
+ getName());
v.setValue(0, scale.createValue());
}
}
}
// end hotfix 20090217
// So we can be sure now that we have a value container and
// that it is linked and that for Action criteria, i.e. single
// values, we have the one value.
// For Object criteria we have to be sure that the number of values
// corresponds to the number of sample objects, so we fill the list
// up
if (!isSingle()) {
// this is to add MISSING values for records.
// it doesnt make a difference for this condition
// whether we just created a new valuemap or are
// refilling an existing one
// Note that the index here starts at the size of the values
// array
// and runs to the total number of records.
// so if we have enough - nothing happens; if some are missing,
// they are
// added at the end
for (int i = v.size(); i < records; i++) {
v.add(scale.createValue());
}
}
}
if (addLinkage) {
initScaleValueLinkage(list, records);
}
}
/**
* ensures that values are linked to scales by setting all of them
* explicitly. We need that especially for export/import
*
* @param list
* List of Alternatives over which to iterate
* @param records
* denotes the number of records for the iteration
*/
public void initScaleValueLinkage(List<Alternative> list, int records) {
for (Alternative a : list) {
Values v = valueMap.get(a.getName());
if (v == null) {
throw new IllegalStateException("initScaleLinkage called,"
+ " but the valueMap is still empty - that's a bug." + " Leaf:" + getName());
}
if (isSingle()) {
v.getValue(0).setScale(scale);
} else {
for (int i = 0; i < records; i++) {
v.getValue(i).setScale(scale);
}
}
}
}
/**
* Checks if the Scale of this Leaf is existent and correctly specified. To
* achieve this, it calls {@link Scale#isCorrectlySpecified(String, List)}
* if there is a scale, or returns false otherwise.
*
* @see TreeNode#isCompletelySpecified(List<ValidationError>)
* @see Scale#isCorrectlySpecified(String, List)
*/
@Override
public boolean isCompletelySpecified(List<ValidationError> errors) {
if (this.scale == null) {
errors.add(new ValidationError("Leaf " + this.getName() + " has no scale", this));
return false;
}
if (scale instanceof YanScale) {
errors
.add(new ValidationError(
"Criterion "
+ getName()
+ " is associated with a 'Yes/Acceptable/No' scale, which is discouraged. We recommend to refine the criterion to be as objective as possible.",
this));
}
return this.scale.isCorrectlySpecified(this.getName(), errors);
}
/**
* Checks if this Leaf is completely evaluated, i.e. we have correct values
* for all Alternatives and samples. For this means we need to iterate over
* all alternatives and check all values. This is done by calling
* {@link Scale#isEvaluated(Value)}
*
* @param alternatives
* the list of Alternatives over which to iterate when checking
* for evaluation values
* @param errorMessages
* This is the <b>list of messages</b> where we add a message
* about this Leaf in case validation fails, i.e. it is not
* completely evaluated.
* @see eu.scape_project.planning.model.tree.TreeNode#isCompletelyEvaluated(List,
* List)
* @see Scale#isEvaluated(Value)
*/
@Override
public boolean isCompletelyEvaluated(List<Alternative> alternatives, List<ValidationError> errors) {
boolean validates = true;
log.debug("checking complete evaluation for leaf " + getName());
for (Alternative a : alternatives) {
Values values = valueMap.get(a.getName());
log.debug("checking values for " + a.getName());
if (this.isSingle()) {
if (values.size() < 1) {
log.warn("Not Enough Value Objects in Values");
validates = false;
} else {
if (!scale.isEvaluated(values.getValue(0))) {
validates = false;
}
}
} else {
int i = 0;
for (Value value : values.getList()) {
log.debug("checking value for " + (i));
if (!scale.isEvaluated(value)) {
validates = false;
break;
}
i++;
}
}
}
if (!validates) {
// I add an error message to the list, and myself to the list of
// error nodes
errors.add(new ValidationError("Leaf " + this.getName() + " is not properly evaluated", this));
}
return validates;
}
/**
* Checks if the transformation settings for this Leaf are complete and
* correct.
*
* @see Transformer#isTransformable(List)
* @see TreeNode#isCompletelyTransformed(List)
*/
@Override
public boolean isCompletelyTransformed(List<ValidationError> errors) {
if (this.transformer == null) {
errors.add(new ValidationError("Leaf " + this.getName() + " is not properly transformed", this));
log.error("Transformer is NULL in Leaf " + getParent().getName() + " > " + getName());
return false;
}
if (!this.transformer.isTransformable(errors) || !this.transformer.isChanged()) {
errors.add(new ValidationError("Leaf " + this.getName() + " is not properly transformed", this));
return false;
}
return true;
}
@Override
/**
* Checks if the weight is in [0,1].
* @see Node#isCorrecltlyWeighted(List<String>)
*/
public boolean isCorrectlyWeighted(List<ValidationError> errors) {
// A leaf is always weighted correctly as long as its weight is in [0,1]
if (this.weight >= 0 && this.weight <= 1) {
return true;
}
errors
.add(new ValidationError("Leaf " + this.getName() + " has an illegal weight (" + this.weight + ")", this));
return false;
}
@Override
/**
* Returns a clone of this Leaf. Includes: <ul>
* <li>{@link Scale}</li>
* <li>{@link AggregationMode}</li>
* <li>{@link ValueMap} which is initialised, but not cloned</li>
* </ul>
* Excludes transformer! The transformer is set to <code>null</code>
*/
public TreeNode clone() {
Leaf clone = (Leaf) super.clone();
if (this.getScale() != null) {
clone.setScale(this.getScale().clone());
}
clone.setValueMap(new HashMap<String, Values>());
Transformer newTransformer = null;
if (transformer != null) {
newTransformer = transformer.clone();
}
clone.setTransformer(newTransformer);
clone.setAggregationMode(this.getAggregationMode());
if (measure != null) {
clone.setMeasure(new Measure(measure));
}
return clone;
}
/**
* @see ITouchable#handleChanges(IChangesHandler)
*/
public void handleChanges(IChangesHandler h) {
super.handleChanges(h);
// call handleChanges of all properties
if (scale != null) {
scale.handleChanges(h);
}
if (transformer != null) {
transformer.handleChanges(h);
}
if (measure != null) {
measure.handleChanges(h);
}
}
@Transient
public boolean isMapped() {
return (measure != null);
}
/**
* this method updates the value map, changing the name of the alternative
* to the new one.
*
* @param oldName
* old name to be updated
* @param newName
* new name to be used instead of oldName
*/
public void updateAlternativeName(String oldName, String newName) {
if (valueMap.containsKey(oldName))
valueMap.put(newName, valueMap.remove(oldName));
/*
* for (String name: valueMap.keySet()) { if (name.equals(oldName)) {
* valueMap.put(newName, valueMap.get(oldName));
* valueMap.remove(oldName); } }
*/
}
/**
* <ul>
* <li>
* removes all {@link Values} from the {@link #valueMap} which are not
* mapped by one of the names provided in the list</li>
* <li>
* removes all {@link Value} objects in the {@link Values} which are out of
* the index of the sample records (which should not happen, but apparently
* we have some projects where this is the case), or where a leaf is single
* and there is more than one {@link Value}</li>
* </ul>
*
* @param alternatives
* list of names of alternatives
* @return number of {@link Values} objects removed
*/
public int removeLooseValues(List<String> alternatives, int records) {
int number = 0;
Iterator<String> it = valueMap.keySet().iterator();
List<String> namesToRemove = new ArrayList<String>();
while (it.hasNext()) {
String altName = it.next();
if (!alternatives.contains(altName)) {
log.warn("removing Values for " + altName + " at leaf " + getName());
namesToRemove.add(altName);
number++;
} else {
Values v = valueMap.get(altName);
int removed = v.removeLooseValues(isSingle() ? 1 : records);
log.warn("removed " + removed + " Value objects " + "for " + altName + " at leaf " + getName());
number += removed;
}
}
for (String s : namesToRemove) {
valueMap.remove(s);
}
return number;
}
public void normalizeWeights(boolean recoursive) {
// this is a leaf which means there are no children
// and therefore there is nothing to do
}
public Measure getMeasure() {
return measure;
}
public void setMeasure(Measure measure) {
this.measure = measure;
}
/**
* initialises the ordinal transformer for free text scales AND has a side
* effect: textual values in free text scales with equalsIgnoreCase=true to
* an existing mapping are changed to the case of the mapping string!
*/
public void initTransformer() {
initTransformer(null);
}
/**
* initialises the ordinal transformer for free text scales, @see
* #initTransformer()
*
* @param defaultTarget
* if this is used (must be 0.0<=defaultTarget<=5.0, unchecked)
* then for each newly added mapping, the default target is set
* as provided.
*/
public void initTransformer(Double defaultTarget) {
if (scale instanceof FreeStringScale) {
FreeStringScale freeScale = (FreeStringScale) scale;
// We collect all distinct actually EXISTING values
OrdinalTransformer t = (OrdinalTransformer) transformer;
Map<String, TargetValueObject> map = t.getMapping();
HashSet<String> allValues = new HashSet<String>();
for (Values values : valueMap.values()) {
for (Value v : values.getList()) {
FreeStringValue text = (FreeStringValue) v;
if (!text.toString().equals("")) {
for (String s : map.keySet()) {
// if the value is NOT the same, but IS the same
// with other case,
// we replace the value with the cases predefined by
// the mapping
if (text.getValue().equalsIgnoreCase(s) && !text.getValue().equals(s)) {
text.setValue(s);
}
}
allValues.add(text.getValue());
}
}
}
// We remove all values from the transformer that do not actually
// occur (anymore)
// I am disabling this for now - why would we want to remove known
// mappings?
// They don't do harm because for the lookup, we use the actually
// encountered values
// (see below)
// HashSet<String> keysToRemove = new HashSet<String>();
// for (String s: map.keySet()) {
// if (!allValues.contains(s)) {
// keysToRemove.add(s);
// }
// }
// for (String s: keysToRemove) {
// map.remove(s);
// }
// We add all values that occur, but are not in the map yet:
for (String s : allValues) {
if (!map.containsKey(s)) {
if (defaultTarget == null) {
map.put(s, new TargetValueObject());
} else {
map.put(s, new TargetValueObject(defaultTarget.doubleValue()));
}
}
}
// We also have to publish the known values
// to the SCALE because it provides the reference lookup
// for iterating and defining the transformation
freeScale.setPossibleValues(allValues);
}
}
/**
* Method responsible for assessing the potential output range of this
* requirement. Calculation rule: if (minPossibleTransformedValue == 0)
* koFactor = 1; else koFactor = 0; potentialOutputRange = relativeWeight *
* (maxPossibleTransformedValue - minPossibleTransformedValue) + koFactor;
*
* @return potential output range. If the plan is not yet at a evaluation
* stage where potential output range can be calculated 0 is
* returned.
*/
public double getPotentialOutputRange() {
// If the plan is not yet at a evaluation stage where potential output
// range can be calculated - return 0.
if (transformer == null) {
return 0;
}
double outputLowerBound = 10;
double outputUpperBound = -10;
// Check OrdinalTransformer
if (transformer instanceof OrdinalTransformer) {
OrdinalTransformer ot = (OrdinalTransformer) transformer;
Map<String, TargetValueObject> otMapping = ot.getMapping();
// set upper- and lower-bound
for (TargetValueObject tv : otMapping.values()) {
if (tv.getValue() > outputUpperBound) {
outputUpperBound = tv.getValue();
}
if (tv.getValue() < outputLowerBound) {
outputLowerBound = tv.getValue();
}
}
}
// Check OrdinalTransformer
if (transformer instanceof NumericTransformer) {
// I have to identify the scale bounds before I can calculate the
// output bounds.
double scaleLowerBound = Double.MIN_VALUE;
double scaleUpperBound = Double.MAX_VALUE;
// At Positive Scales lowerBound is 0, upperBound has to be fetched
if (scale instanceof PositiveIntegerScale) {
PositiveIntegerScale s = (PositiveIntegerScale) scale;
scaleLowerBound = 0;
scaleUpperBound = s.getUpperBound();
}
if (scale instanceof PositiveFloatScale) {
PositiveFloatScale s = (PositiveFloatScale) scale;
scaleLowerBound = 0;
scaleUpperBound = s.getUpperBound();
}
// At Range Scales lowerBound and upperBound have to be fetched
if (scale instanceof IntRangeScale) {
IntRangeScale s = (IntRangeScale) scale;
scaleLowerBound = s.getLowerBound();
scaleUpperBound = s.getUpperBound();
}
if (scale instanceof FloatRangeScale) {
FloatRangeScale s = (FloatRangeScale) scale;
scaleLowerBound = s.getLowerBound();
scaleUpperBound = s.getUpperBound();
}
// get Transformer thresholds
NumericTransformer nt = (NumericTransformer) transformer;
double transformerT1 = nt.getThreshold1();
double transformerT2 = nt.getThreshold2();
double transformerT3 = nt.getThreshold3();
double transformerT4 = nt.getThreshold4();
double transformerT5 = nt.getThreshold5();
// calculate output bounds
if (nt.hasIncreasingOrder()) {
// increasing thresholds
// lower bound
if (scaleLowerBound < transformerT1) {
outputLowerBound = 0;
} else if (scaleLowerBound < transformerT2) {
outputLowerBound = 1;
} else if (scaleLowerBound < transformerT3) {
outputLowerBound = 2;
} else if (scaleLowerBound < transformerT4) {
outputLowerBound = 3;
} else if (scaleLowerBound < transformerT5) {
outputLowerBound = 4;
} else {
outputLowerBound = 5;
}
// upper bound
if (scaleUpperBound < transformerT1) {
outputUpperBound = 0;
} else if (scaleUpperBound < transformerT2) {
outputUpperBound = 1;
} else if (scaleUpperBound < transformerT3) {
outputUpperBound = 2;
} else if (scaleUpperBound < transformerT4) {
outputUpperBound = 3;
} else if (scaleUpperBound < transformerT5) {
outputUpperBound = 4;
} else {
outputUpperBound = 5;
}
} else {
// decreasing thresholds
// lower bound
if (scaleUpperBound > transformerT1) {
outputLowerBound = 0;
} else if (scaleUpperBound > transformerT2) {
outputLowerBound = 1;
} else if (scaleUpperBound > transformerT3) {
outputLowerBound = 2;
} else if (scaleUpperBound > transformerT4) {
outputLowerBound = 3;
} else if (scaleUpperBound > transformerT5) {
outputLowerBound = 4;
} else {
outputLowerBound = 5;
}
// upper bound
if (scaleLowerBound > transformerT1) {
outputUpperBound = 0;
} else if (scaleLowerBound > transformerT2) {
outputUpperBound = 1;
} else if (scaleLowerBound > transformerT3) {
outputUpperBound = 2;
} else if (scaleLowerBound > transformerT4) {
outputUpperBound = 3;
} else if (scaleLowerBound > transformerT5) {
outputUpperBound = 4;
} else {
outputUpperBound = 5;
}
}
}
double koFactor = 0;
if (outputLowerBound == 0) {
koFactor = 1;
}
double potentialOutputRange = getTotalWeight() * (outputUpperBound - outputLowerBound) + koFactor;
return potentialOutputRange;
}
public double getPotentialImpact() {
if (transformer == null) {
return 0.0;
}
double maxRating = 0.0;
if (transformer instanceof OrdinalTransformer) {
for (TargetValueObject tv : ((OrdinalTransformer) transformer).getMapping().values()) {
if (tv.getValue() > maxRating) {
maxRating = tv.getValue();
}
}
} else {
maxRating = 5.0;
}
return this.getTotalWeight() * maxRating;
}
/**
* Method responsible for assessing the actual output range of this
* requirement. Calculation rule: if (minActualTransformedValue == 0)
* koFactor = 1; else koFactor = 0; actualOutputRange = relativeWeight *
* (maxActualTransformedValue - minActualTransformedValue) + koFactor;
*
* @return actual output range. If the plan is not yet at a evaluation stage
* where actual output range can be calculated 0 is returned.
*/
public double getActualOutputRange() {
// If the plan is not yet at a evaluation stage where actual output
// range can be calculated - return 0.
if (transformer == null) {
return 0;
}
// Collect all measured values from all alternatives
List<Value> valueList = new ArrayList<Value>();
Collection<Values> valuesCollection = valueMap.values();
for (Values values : valuesCollection) {
for (Value value : values.getList()) {
valueList.add(value);
}
}
// if nothing is measured yet - return 0
if (valueList.size() == 0) {
return 0;
}
// transform measured values
List<Double> transformedValues = new ArrayList<Double>();
for (Value val : valueList) {
TargetValue targetValue;
// do ordinal transformationCriterion
if (transformer instanceof OrdinalTransformer) {
OrdinalTransformer ordTrans = (OrdinalTransformer) transformer;
if (val instanceof IOrdinalValue) {
try {
targetValue = ordTrans.transform((IOrdinalValue) val);
} catch (NullPointerException e) {
log.warn("Measurement of leaf doesn't match with OrdinalTransformer! Ignoring it!");
log.warn("MeasuredValue-id: " + val.getId() + "; Transformer-id: " + ordTrans.getId());
continue;
}
transformedValues.add(targetValue.getValue());
} else {
log.warn("getActualOutputRange(): INumericValue value passed to OrdinalTransformer - ignore value");
}
}
// do numeric transformation
if (transformer instanceof NumericTransformer) {
NumericTransformer numericTrans = (NumericTransformer) transformer;
if (val instanceof INumericValue) {
targetValue = numericTrans.transform((INumericValue) val);
transformedValues.add(targetValue.getValue());
} else {
log.warn("getActualOutputRange(): IOrdinalValue value passed to NumericTransformer - ignore value");
}
}
}
// if nothing could be transformed successfully - return 0
if (transformedValues.size() == 0) {
return 0;
}
// calculate upper/lower bound
double outputLowerBound = 10;
double outputUpperBound = -10;
for (Double tVal : transformedValues) {
if (tVal > outputUpperBound) {
outputUpperBound = tVal;
}
if (tVal < outputLowerBound) {
outputLowerBound = tVal;
}
}
double koFactor = 0;
if (outputLowerBound == 0) {
koFactor = 1;
}
double actualOutputRange = getTotalWeight() * (outputUpperBound - outputLowerBound) + koFactor;
return actualOutputRange;
}
/**
* touches everything: this, the scale and the transformer (if existing)
*/
@Override
public void touchAll(String username) {
touch(username);
if (scale != null) {
scale.touch(username);
}
if (transformer != null) {
transformer.touch(username);
}
}
/**
* Method responsible for touching this Leaf and its Scale.
*/
public void touchIncludingScale() {
touch();
if (scale != null) {
scale.touch();
}
}
}