package er.grouping;
import java.text.Format;
import java.util.Enumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation.NSTimestamp;
import com.webobjects.foundation.NSTimestampFormatter;
import er.extensions.foundation.ERXValueUtilities;
/**
* Defines the specifics of a {@link DRMasterCriteria}.
* How to retrieve the values, how to convert them into
* values that can be grouped and how to group them
* into a set of ranges, if required.
*/
public class DRSubMasterCriteria {
private static final Logger log = LoggerFactory.getLogger(DRSubMasterCriteria.class);
protected boolean _useMethod;
protected String _key;
protected boolean _useTimeFormat;
/**
* Defines the array of possible groupings.
*/
protected final static NSArray _possibleUseTypes = new NSArray(new Object[]{"usePredefined" , "useRange" , "usePeriodic", "NONE"});
protected String _format;
protected boolean _groupEdges;
protected NSArray _rawPossibleValues;
/** */
protected NSMutableArray _possibleValues;
protected double _periodicDelta;
// an array of numbers or dates. Only used if
// possibleValuesUseType is non-_nil
protected String _possibleValuesUseType;
// if 'usePredefined', uses a list of pre-existing values as
// possible matches to tests and no other possible values
// for key
// if 'useRange', possibleValues are used with two tests to assess
// whether the record is between each pair of contiguous values
// in the possibleValues list
// if 'usePeriodic', expect exactly 2 values in possibleValues representing
// delta and TYPE of delta: e.g. date vs. number
// these two values also serve to set the start point from
// which deltas are built.
protected boolean _isPreset;
protected boolean _isPeriodic;
protected boolean _mustSearchForLookup;
protected NSDictionary _presetLookupDict;
protected NSSelector _selKey;
protected boolean _nonNumberOrDate;
protected String _label;
static public DRSubMasterCriteria withDefinitionDictionaryPossibleValues(NSDictionary smcdict, NSArray apossibleValues) {
return new DRSubMasterCriteria(smcdict, apossibleValues);
}
static public DRSubMasterCriteria withKeyUseMethodUseTimeFormatFormatPossibleValuesUseTypeGroupEdgesPossibleValues(String akey, boolean auseMethod, boolean auseTimeFormat, String aformat, String apossibleValuesUseType, boolean agroupEdges, NSArray apossibleValues) {
DRSubMasterCriteria aVal = new DRSubMasterCriteria(akey, auseMethod, auseTimeFormat, aformat, apossibleValuesUseType, agroupEdges, apossibleValues);
return aVal;
}
public NSMutableArray possibleRangeValuesFromRawValues(NSArray rawPossVals) {
int i;
NSMutableArray possVals = new NSMutableArray();
int rawCount = rawPossVals.count();
int newCount = rawCount-1;
if (groupEdges()) {
Object lowVal = "L";
Object highVal = rawPossVals.objectAtIndex(0);
possVals.addObject(valDictMaxMin(highVal, lowVal));
}
for (i = 0; i < newCount; i++) {
Object rawPossValLow;
Object rawPossValHigh;
Object newPossVal;
rawPossValLow = rawPossVals.objectAtIndex(i);
rawPossValHigh = rawPossVals.objectAtIndex(i+1);
newPossVal = valDictMaxMin(rawPossValHigh, rawPossValLow);
possVals.addObject(newPossVal);
}
if (groupEdges()) {
Object lowVal = rawPossVals.lastObject();
Object highVal = "H";
possVals.addObject(valDictMaxMin(highVal, lowVal));
}
return possVals;
}
/** Constructor that uses a {@link NSDictionary} which defines the properties. */
public DRSubMasterCriteria(NSDictionary smcdict, NSArray apossibleValues) {
this(
(String)smcdict.objectForKey("key"),
ERXValueUtilities.booleanValue(smcdict.objectForKey("useMethod")),
ERXValueUtilities.booleanValue(smcdict.objectForKey("useTimeFormat")),
(String)smcdict.objectForKey("format"),
(String)smcdict.objectForKey("possibleValuesUseType"),
ERXValueUtilities.booleanValue(smcdict.objectForKey("groupEdges")),
apossibleValues);
}
public DRSubMasterCriteria(String akey, boolean auseMethod, boolean auseTimeFormat, String aformat, String apossibleValuesUseType, boolean agroupEdges, NSArray apossibleValues) {
if(log.isDebugEnabled()) {
log.debug("akey: {}", akey);
log.debug("auseMethod: {}", auseMethod);
log.debug("auseTimeFormat: {}", auseTimeFormat);
log.debug("aformat: {}", aformat);
log.debug("apossibleValuesUseType: {}", apossibleValuesUseType);
log.debug("agroupEdges: {}", agroupEdges);
log.debug("apossibleValues: {}", apossibleValues);
}
_label = null;
setUseMethod(auseMethod);
setUseTimeFormat(auseTimeFormat);
setGroupEdges(agroupEdges);
setKey(akey);
setFormat(aformat);
setPossibleValuesUseType(apossibleValuesUseType);
setRawPossibleValues(apossibleValues);
if (isPreset() && mustSearchForLookup()) {
_possibleValues = possibleRangeValuesFromRawValues(_rawPossibleValues);
} else {
_possibleValues = new NSMutableArray(_rawPossibleValues);
}
if (isPreset()) {
_presetLookupDict = buildPresetLookupDict();
}
}
public String label() {
if (_label == null) {
String lbl = _key;
if (_useTimeFormat) {
lbl = lbl + " " + _format;
}
if (_possibleValuesUseType != null) {
lbl = lbl + " [" + _possibleValuesUseType + "]";
}
_label = lbl;
}
return _label;
}
public NSDictionary buildPresetLookupDict() {
NSMutableDictionary adict = new NSMutableDictionary();
Enumeration anEnum = _possibleValues.objectEnumerator();
while (anEnum.hasMoreElements()) {
Object aval = anEnum.nextElement();
// WARNING
adict.setObjectForKey(aval, aval);
}
return new NSDictionary(adict);
}
public DRSubMasterCriteria() {
super();
}
public boolean nonNumberOrDate() {
return _nonNumberOrDate;
}
/**
* Decides if the extraction is by method or instance variable.
* If this returns true, then only methods will be used to extract
* values from the raw objects, not their instance variables.
*/
public boolean useMethod() {
return _useMethod;
}
public void setUseMethod(boolean v) {
_useMethod = v;
}
/**
* Decides if the {@link #format()} given is used to convert dates
* into strings before comparison or just compare {@link NSTimestamp}.
* If you set this, you should also set a valid {@link NSTimestampFormatter}
* pattern in {@link #format()}.
*/
public boolean useTimeFormat() {
return _useTimeFormat;
}
public void setUseTimeFormat(boolean v) {
_useTimeFormat = v;
}
/**
* Defines if the values not falling into the {@link #possibleValues()} are also grouped.
* If they are, then they fall into a special <b>H</b>igh and <b>L</b>ow bucket.
*/
public boolean groupEdges() {
return _groupEdges;
}
public void setGroupEdges(boolean v) {
_groupEdges = v;
}
/** The key used for retrieving values from the records by. */
public String key() {
return _key;
}
public void setKey(String v) {
if(v !=null)
_key = v;
else
_key = null;
if (_useMethod) {
_selKey = new NSSelector(_key);
}
}
/**
* When {@link #useTimeFormat()} is set, then date values
* will be converted to a string before a comparison by using this format.
* The string can be any valid {@link NSTimestampFormatter} string,
* which means that you can also use {@link java.text.DateFormat}
* patterns.
*/
public String format() {
return _format;
}
public void setFormat(String v) {
if (_useTimeFormat && v == null) {
_format = "";
log.error("Can't have empty format when useTimeFormat=true: {}", this);
}
if(v != null)
_format = v;
else
_format = null;
}
protected boolean usePeriodic() {
return "usePeriodic".equals(_possibleValuesUseType);
}
protected boolean useRange() {
return "useRange".equals(_possibleValuesUseType);
}
protected boolean usePredefined() {
return "usePredefined".equals(_possibleValuesUseType);
}
public String possibleValuesUseType() {
return _possibleValuesUseType;
}
public void setPossibleValuesUseType(String v) {
_mustSearchForLookup = false;
_isPreset = false;
if(v == null) {
_possibleValuesUseType = null;
} else {
if (!_possibleUseTypes.containsObject(v)) {
// invalid possibleValuesUseType
log.error("Invalid possibleValuesUseType: {}. Allowed are only: {} {}", v, _possibleUseTypes, this);
_possibleValuesUseType = null;
} else {
_possibleValuesUseType = v;
_mustSearchForLookup = useRange() || usePeriodic();
_isPreset = useRange() || usePredefined();
_isPeriodic = usePeriodic();
}
}
}
public NSArray rawPossibleValues() {
return _rawPossibleValues;
}
public void setRawPossibleValues(NSArray arr) {
if (_possibleValuesUseType != null && (arr == null || arr.count() == 0)) {
log.warn("Should use possible values but got none: {}", this);
_rawPossibleValues = NSArray.EmptyArray;
} else {
_rawPossibleValues = new NSArray(arr);
Object obj = _rawPossibleValues.lastObject();
if (!(obj instanceof String) && !(obj instanceof Number)) {
_nonNumberOrDate = true;
}
}
if(isPeriodic()) {
double v1 = DRValueConverter.converter().doubleForValue(_rawPossibleValues.lastObject());
double v2 = DRValueConverter.converter().doubleForValue(_rawPossibleValues.objectAtIndex(0));
_periodicDelta = v1 - v2;
}
}
public NSArray possibleValues() {
return _possibleValues;
}
public boolean isPreset() {
return _isPreset;
}
public boolean isPeriodic() {
return _isPeriodic;
}
public boolean mustSearchForLookup() {
return _mustSearchForLookup;
}
public NSDictionary valDictMaxMin(Object highVal, Object lowVal) {
NSDictionary valDict = new NSDictionary(new Object[]{lowVal, highVal}, new Object[]{"L", "H"});
return valDict;
}
public NSMutableArray possibleValuesToUse() {
if (isPreset() && mustSearchForLookup()) {
return new NSMutableArray(_rawPossibleValues);
}
return _possibleValues;
}
/** Will test inbetween'ness, will create new groups for periodics */
public NSDictionary valDictFromSearchForLookup(Object aval) {
Object lowVal = null;
Object highVal = null;
NSMutableArray possibleValuesToUse = possibleValuesToUse();
Object maxVal = possibleValuesToUse.lastObject();
Object minVal = possibleValuesToUse.objectAtIndex(0);
double v = DRValueConverter.converter().doubleForValue(aval);
double maxv = DRValueConverter.converter().doubleForValue(maxVal);
double minv = DRValueConverter.converter().doubleForValue(minVal);
if (!isPeriodic()) {
if (!groupEdges()) {
if (v > maxv) {
return null;
}
if (v <= minv) {
return null;
}
} else {
if (v > maxv) {
lowVal = maxVal;
highVal = "H";
return valDictMaxMin(highVal, lowVal);
}
if (v <= minv) {
lowVal = "L";
highVal = minVal;
return valDictMaxMin(highVal, lowVal);
}
}
} else {
if (v > maxv) {
lowVal = maxVal;
highVal = newWithDelta(maxVal, _periodicDelta);
possibleValuesToUse.addObject(highVal);
return valDictMaxMin(highVal, lowVal);
}
if (v <= minv) {
lowVal = newWithDelta(minVal, -_periodicDelta);
highVal = minVal;
possibleValuesToUse.insertObjectAtIndex(lowVal, 0);
return valDictMaxMin(highVal, lowVal);
}
}
int pvcount = possibleValuesToUse.count();
for (int i = 0; i < pvcount; i++) {
int nextIndex = i+1;
if (nextIndex == pvcount) {
return null;
}
lowVal = possibleValuesToUse.objectAtIndex(i);
highVal = possibleValuesToUse.objectAtIndex(nextIndex);
minv = DRValueConverter.converter().doubleForValue(lowVal);
maxv = DRValueConverter.converter().doubleForValue(highVal);
if ((v <= maxv) && (v > minv)) {
break;
}
}
return valDictMaxMin(highVal, lowVal);
}
/**
* Returns a new value by adding a delta to it.
* In case of a {@link NSTimestamp}, the delta will be seconds,
* in case of a {@link java.lang.Number Number}, the delta is added as a double.
* Otherwise, a conversion to a double is attempted and the delta is added afterwards.
*/
protected Object newWithDelta(Object val, double delta) {
double v;
if (val instanceof NSTimestamp) {
NSTimestamp vts = (NSTimestamp)val;
NSTimestamp nvts = vts.timestampByAddingGregorianUnits(0, 0, 0, 0, 0, (int)delta);
return nvts;
} else if (val instanceof Number) {
v = DRValueConverter.converter().doubleForValue(val) + delta;
return (Double.valueOf(v));
}
v = DRValueConverter.converter().doubleForValue(val) + delta;
return Double.toString(v);
}
/**
* Returns the value for the given record.
* If {@link #useMethod()} is given, the method is called and no further
* action is taken if that fails.
* Otherwise we use {@link NSKeyValueCoding} which also considers instance
* variables.
*/
public Object valueForRecord(DRRecord rec) {
Object aval = null;
if (_useMethod) {
try{
aval = _selKey.invoke(rec.rawRecord());
} catch(IllegalAccessException e) {
} catch(IllegalArgumentException e) {
} catch(java.lang.reflect.InvocationTargetException e) {
} catch(NoSuchMethodException e) {
}
} else {
aval = rec.rawRecord().valueForKeyPath(_key);
}
return aval;
}
public Object lookUpValueForRecord(DRRecord rec) {
Object aval = valueForRecord(rec);
if (mustSearchForLookup()) {
aval = valDictFromSearchForLookup(aval);
} else if (isPreset()) {
//WARNING
aval = _presetLookupDict.objectForKey(aval);
}
return aval;
}
/**
* Converts a given object to a grouping value.
* If case the value if a {@link NSTimestamp},
* {@link #useTimeFormat()} is set and {@link #format()}
* is a valid date format, the formatted value will returned.
*/
public String lookUpKeyForValue(Object aVal) {
String s;
if (_useTimeFormat) {
NSTimestamp ts = DRValueConverter.converter().timestampForValue(aVal);
Format formatter = DRCriteria.formatterForFormat(_format);
try {
s = formatter.format(ts);
} catch(Exception ex) {
log.warn("Error lookup {}, value={}: {}", ex, aVal, this);
s = aVal.toString();
}
} else {
s = aVal.toString();
}
return s;
}
/** Returns the array of possible use types. */
public NSArray possibleUseTypes() {
return _possibleUseTypes;
}
/** Holds the description for the {@link #key()}. */
private String _keyDesc = null;
/** Returns the description for the {@link #key()}. */
public String keyDesc() {
if(_keyDesc == null) {
_keyDesc = super.toString();
}
return _keyDesc;
}
@Override
public String toString() {
return "<DRSubMasterCriteria key: \"" + key() + "\"; label: \"" + label() + "\"; >";
}
}