package de.jpaw.bonaparte.api;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.jpaw.bonaparte.pojos.meta.AlphanumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.AlphanumericEnumSetDataItem;
import de.jpaw.bonaparte.pojos.meta.BasicNumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.BinaryElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.ClassDefinition;
import de.jpaw.bonaparte.pojos.meta.EnumDataItem;
import de.jpaw.bonaparte.pojos.meta.EnumDefinition;
import de.jpaw.bonaparte.pojos.meta.FieldDefinition;
import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.NumericEnumSetDataItem;
import de.jpaw.bonaparte.pojos.meta.ObjectReference;
import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.XEnumDataItem;
import de.jpaw.bonaparte.pojos.meta.XEnumDefinition;
import de.jpaw.bonaparte.pojos.meta.XEnumSetDataItem;
import de.jpaw.bonaparte.pojos.meta.XEnumSetDefinition;
import de.jpaw.bonaparte.pojos.ui.Alignment;
import de.jpaw.bonaparte.pojos.ui.LayoutHint;
import de.jpaw.bonaparte.pojos.ui.UIColumn;
import de.jpaw.bonaparte.pojos.ui.UIColumnConfiguration;
import de.jpaw.bonaparte.pojos.ui.UIDefaults;
import de.jpaw.bonaparte.pojos.ui.UIMeta;
import de.jpaw.bonaparte.util.FieldGetter;
import de.jpaw.bonaparte.util.UtilException;
/** Utility class which helps to guess an initial UI configuration. */
public class ColumnCollector {
// some tunable constants...
public static final UIDefaults DEFAULTS = new UIDefaults(5, 32, 24, 80, 120, 10, 12, 200);
static {
DEFAULTS.freeze();
}
public final List<UIColumn> columns = new ArrayList<UIColumn>();
private final UIDefaults prefs;
private final boolean keepObjects;
public ColumnCollector() {
prefs = DEFAULTS;
keepObjects = false;
}
public ColumnCollector(UIDefaults preferences) {
prefs = preferences;
keepObjects = false;
}
public ColumnCollector(UIDefaults preferences, boolean keepObjects) {
prefs = preferences;
this.keepObjects = keepObjects;
}
private int width(int chars) {
int calculatedSize = prefs.getWidthOffset() + chars * prefs.getWidthPerCharacter();
return calculatedSize > prefs.getWidthMax() ? prefs.getWidthMax() : calculatedSize;
}
private void addFieldToColumns(String pathname, ClassDefinition cls, FieldDefinition f) {
// create a preliminary column descriptor
UIColumn d = new UIColumn();
d.setFieldName(pathname);
String type = f.getDataType().toLowerCase();
if (f instanceof ObjectReference) {
// probability to descend further...
ObjectReference o = (ObjectReference)f;
if (o.getLowerBound() != null) {
// yes, descend! (discards the descriptor prepared before...)
if (keepObjects) {
d.setWidth(prefs.getWidthObject());
d.setAlignment(Alignment.LEFT); // probably replaced by some description
d.setLayoutHint(LayoutHint.OBJECT);
columns.add(d);
}
addToColumns(pathname + ".", o.getSecondaryLowerBound() != null ? o.getSecondaryLowerBound() : o.getLowerBound());
} else {
// regular object
d.setWidth(prefs.getWidthObject());
d.setAlignment(Alignment.CENTER);
d.setLayoutHint(LayoutHint.OBJECT);
columns.add(d);
}
return;
}
if (f instanceof BasicNumericElementaryDataItem) {
// all numerical fields
BasicNumericElementaryDataItem b = (BasicNumericElementaryDataItem)f;
d.setWidth(width(b.getTotalDigits() + (b.getIsSigned() ? 1 : 0) + (b.getDecimalDigits() > 0 ? 1 : 0)));
d.setAlignment(Alignment.RIGHT);
d.setLayoutHint(LayoutHint.TEXT);
columns.add(d);
return;
}
if (f instanceof AlphanumericElementaryDataItem) {
// all alphabetical fields
AlphanumericElementaryDataItem a = (AlphanumericElementaryDataItem)f;
d.setWidth(width(a.getLength()));
d.setAlignment(Alignment.LEFT);
d.setLayoutHint(LayoutHint.TEXT);
columns.add(d);
return;
}
if (f instanceof TemporalElementaryDataItem) {
// all date / time fields: size depends on type
TemporalElementaryDataItem t = (TemporalElementaryDataItem)f;
int fractionalSecondsSize = t.getFractionalSeconds() > 0 ? 4 : 0;
if ("day".equals(type))
d.setWidth(width(10));
else if ("time".equals(type))
d.setWidth(width(8 + fractionalSecondsSize));
else
d.setWidth(width(19 + fractionalSecondsSize));
d.setAlignment(Alignment.CENTER);
d.setLayoutHint(LayoutHint.TEXT);
columns.add(d);
return;
}
if (f instanceof MiscElementaryDataItem) {
// types like char, UUID, boolean
d.setWidth(width(1));
d.setAlignment(Alignment.CENTER);
d.setLayoutHint(LayoutHint.TEXT);
if ("uuid".equals(type)) {
d.setWidth(width(36));
} else if ("boolean".equals(type)) {
d.setLayoutHint(LayoutHint.CHECKBOX);
}
columns.add(d);
return;
}
if (f instanceof BinaryElementaryDataItem) {
// all binary fields
BinaryElementaryDataItem a = (BinaryElementaryDataItem)f;
d.setWidth(width(2 + 2 * a.getLength())); // space for 0x, followed by hex representation
d.setAlignment(Alignment.LEFT);
d.setLayoutHint(LayoutHint.TEXT); // image just in some cases
columns.add(d);
return;
}
// enum or enumset...
String category = f.getDataCategory().name();
if (category.contains("ENUM")) {
d.setWidth(category.contains("ENUMSET") ? prefs.getWidthEnumset() : prefs.getWidthEnum());
d.setAlignment(Alignment.LEFT);
d.setLayoutHint(LayoutHint.TEXT);
columns.add(d);
return;
}
// anything else (binary for example) ignore (for images or JSON, context menus / popups should be used)
}
private void addToColumnsNoRecursion(String prefix, ClassDefinition cls) {
for (FieldDefinition f : cls.getFields()) {
if (f.getMaxCount() != null && f.getMaxCount().intValue() > 0) {
// make all array elements available
int num = f.getMaxCount();
if (num > prefs.getRenderMaxArrayColumns())
num = prefs.getRenderMaxArrayColumns(); // but don't overdo!
for (int i = 0; i < num; ++i)
addFieldToColumns(prefix + f.getName() + String.format("[%d]", i), cls, f);
} else {
// is a single instance
addFieldToColumns(prefix + f.getName(), cls, f);
}
}
}
private void addToColumns(String prefix, ClassDefinition cls) {
if (cls != null) {
// first the fields of any superclass...
if (cls.getParentMeta() != null)
addToColumns(prefix, cls.getParentMeta());
// then my own...
addToColumnsNoRecursion(prefix, cls);
}
}
/** Main external entry. */
public void addToColumns(ClassDefinition cls) {
addToColumns("", cls);
}
/** Creates the UIMeta object, without the properties yet. */
public UIMeta createMeta(FieldDefinition meta) {
UIMeta m = new UIMeta();
// transfer fields which are in the main class
m.setIsRequired(meta.getIsRequired());
m.setDataCategory(meta.getDataCategory().name());
m.setDataType(meta.getBonaparteType());
m.setFieldProperties(meta.getProperties());
switch (meta.getMultiplicity()) {
case ARRAY:
case LIST:
case SET:
m.setIsList(Boolean.TRUE);
break;
case MAP:
m.setIsMap(Boolean.TRUE);
break;
case SCALAR:
// no entry
break;
}
if (meta instanceof ObjectReference) {
ObjectReference or = (ObjectReference)meta;
ClassDefinition bound = or.getSecondaryLowerBound();
if (bound == null)
bound = or.getLowerBound();
if (bound != null) {
m.setPqon(bound.getName());
m.setClassProperties(bound.getProperties());
}
} else {
if (meta instanceof BasicNumericElementaryDataItem) {
BasicNumericElementaryDataItem bn = (BasicNumericElementaryDataItem)meta;
m.setLength(bn.getTotalDigits());
m.setIsSigned(bn.getIsSigned());
m.setDecimalDigits(bn.getDecimalDigits());
} else if (meta instanceof AlphanumericElementaryDataItem) {
AlphanumericElementaryDataItem an = (AlphanumericElementaryDataItem)meta;
m.setLength(an.getLength());
m.setMinLength(an.getMinLength());
if (an.getAllowControlCharacters())
m.setAllowCtrl(Boolean.TRUE);
} else if (meta instanceof TemporalElementaryDataItem) {
TemporalElementaryDataItem tn = (TemporalElementaryDataItem)meta;
m.setDecimalDigits(tn.getFractionalSeconds());
} else if (meta instanceof BinaryElementaryDataItem) {
BinaryElementaryDataItem bn = (BinaryElementaryDataItem)meta;
m.setLength(bn.getLength());
} else if (meta instanceof EnumDataItem) {
EnumDataItem en = (EnumDataItem)meta;
EnumDefinition ed = en.getBaseEnum();
m.setPqon(ed.getName());
m.setEnumInstances(ed.getIds());
m.setEnumTokens(ed.getTokens());
} else if (meta instanceof XEnumDataItem) {
XEnumDataItem xen = (XEnumDataItem)meta;
XEnumDefinition xed = xen.getBaseXEnum();
EnumDefinition ed = xed.getBaseEnum(); // currently we associate the xenum with the base enum.
m.setPqon2(xed.getName());
m.setPqon(ed.getName());
m.setEnumInstances(ed.getIds());
m.setEnumTokens(ed.getTokens());
} else if (meta instanceof AlphanumericEnumSetDataItem) {
AlphanumericEnumSetDataItem en = (AlphanumericEnumSetDataItem)meta;
EnumDefinition ed = en.getBaseEnumset().getBaseEnum();
m.setPqon(ed.getName());
m.setEnumInstances(ed.getIds());
m.setEnumTokens(ed.getTokens());
} else if (meta instanceof NumericEnumSetDataItem) {
NumericEnumSetDataItem en = (NumericEnumSetDataItem)meta;
EnumDefinition ed = en.getBaseEnumset().getBaseEnum();
m.setPqon(ed.getName());
m.setEnumInstances(ed.getIds());
} else if (meta instanceof XEnumSetDataItem) {
XEnumSetDataItem xs = (XEnumSetDataItem)meta;
XEnumSetDefinition xsd = xs.getBaseXEnumset();
XEnumDefinition xed = xsd.getBaseXEnum();
EnumDefinition ed = xed.getBaseEnum(); // currently we associate the xenum with the base enum.
m.setPqon2(xed.getName());
m.setPqon(ed.getName());
m.setEnumInstances(ed.getIds());
m.setEnumTokens(ed.getTokens());
} else if (meta instanceof TemporalElementaryDataItem) {
TemporalElementaryDataItem tn = (TemporalElementaryDataItem)meta;
m.setDecimalDigits(tn.getFractionalSeconds());
}
}
return m;
}
protected void addFieldProperty(String fieldname, String propertyname, String value, Map<String, UIColumnConfiguration> fields) {
UIColumnConfiguration ui = fields.get(fieldname);
if (ui != null) {
UIMeta m = ui.getMeta();
if (m != null) { // should be... (we just set it before)
Map<String, String> fp = m.getFieldProperties();
if (fp == null) {
// first property of this field
fp = new HashMap<String, String>();
m.setFieldProperties(fp);
}
fp.put(propertyname, value);
}
}
}
/** Given a path name of the form field1[index].field2[index2], with optional array indexes, return a stripped field name field1.field2.
* Nested brackets are currently NOT supported. */
public String stripArrayIndexes(String pathname) throws UtilException {
int i = pathname.indexOf('[');
if (i < 0) {
// not a single index contained - skip allocating a StringBuilder and return the original pathname
return pathname;
}
StringBuilder result = new StringBuilder(pathname.length());
int rest = 0; // start index of the remaining path
while (i >= 0) {
// transfer portion before i
if (i > rest)
result.append(pathname.substring(rest, i));
// find end of skip
i = pathname.indexOf(']', i+1);
if (i < 0)
throw new UtilException(UtilException.NO_CLOSING_BRACKET, pathname);
rest = i+1;
i = pathname.indexOf('[', rest);
}
// no more brackets found, return the rest
result.append(pathname.substring(rest));
return result.toString();
}
/** Create meta for a single field. No properties stored. */
public void createUIMeta(UIColumnConfiguration ui, ClassDefinition cls) throws UtilException {
String strippedPathname = stripArrayIndexes(ui.getFieldName()); // properties are the same for different indexes of the same field
FieldDefinition fd = FieldGetter.getFieldDefinitionForPathname(cls, strippedPathname);
ui.setMeta(createMeta(fd));
}
/** Create meta data (now extended to support nested names as well). Returns class level properties.
* For a method to evaluate the value of a field for a given instance, see FieldGetter.getSingleField in project bonaparte-core */
public Map<String, String> createUIMetas(List<UIColumnConfiguration> uis, ClassDefinition cls) throws UtilException {
// create a hash of the field names
Map<String, UIColumnConfiguration> fields = new HashMap<String, UIColumnConfiguration>(uis.size() * 2);
Set<String> usedClasses = new HashSet<String>();
// walk all fields, use the hash to avoid computing similar names multiple times
for (UIColumnConfiguration ui : uis) {
String strippedPathname = stripArrayIndexes(ui.getFieldName()); // properties are the same for different indexes of the same field
UIColumnConfiguration previousUi = fields.get(strippedPathname);
if (previousUi != null) {
ui.setMeta(previousUi.getMeta());
} else {
// have to compute it
fields.put(strippedPathname, ui);
FieldDefinition fd = FieldGetter.getFieldDefinitionForPathname(cls, strippedPathname);
ui.setMeta(createMeta(fd));
// if the stripped classname contains at least one dot, remember the class part
int i = strippedPathname.lastIndexOf('.');
if (i > 0)
usedClasses.add(strippedPathname.substring(0, i)); // remember all classes which host fields
}
}
// now assign the properties at field level
// TODO: nested fields still TODO - enhance meta information to simplify life...
Map<String, String> classProperties = new HashMap<String, String>();
for (Map.Entry<String, String> e : cls.getProperties().entrySet()) {
String key = e.getKey();
int i = key.indexOf('.');
if (i >= 0) {
// field property
String fieldname = key.substring(0, i);
addFieldProperty(fieldname, key.substring(i+1), e.getValue(), fields);
} else {
// class property
classProperties.put(key, e.getValue());
}
}
return classProperties;
}
}