/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.tables.beans;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import com.opendoorlogistics.api.ExecutionReport;
import com.opendoorlogistics.api.tables.ODLColumnType;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLDatastoreAlterable;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.ODLTableAlterable;
import com.opendoorlogistics.api.tables.ODLTableDefinition;
import com.opendoorlogistics.api.tables.ODLTableDefinitionAlterable;
import com.opendoorlogistics.api.tables.ODLTableReadOnly;
import com.opendoorlogistics.api.tables.TableFlags;
import com.opendoorlogistics.api.tables.beans.BeanMappedRow;
import com.opendoorlogistics.api.tables.beans.BeanTableMapping;
import com.opendoorlogistics.api.tables.beans.annotations.ODLColumnDescription;
import com.opendoorlogistics.api.tables.beans.annotations.ODLColumnName;
import com.opendoorlogistics.api.tables.beans.annotations.ODLColumnOrder;
import com.opendoorlogistics.api.tables.beans.annotations.ODLDefaultDoubleValue;
import com.opendoorlogistics.api.tables.beans.annotations.ODLDefaultLongValue;
import com.opendoorlogistics.api.tables.beans.annotations.ODLDefaultStringValue;
import com.opendoorlogistics.api.tables.beans.annotations.ODLIgnore;
import com.opendoorlogistics.api.tables.beans.annotations.ODLNullAllowed;
import com.opendoorlogistics.api.tables.beans.annotations.ODLTableFlags;
import com.opendoorlogistics.api.tables.beans.annotations.ODLTableName;
import com.opendoorlogistics.api.tables.beans.annotations.ODLTag;
import com.opendoorlogistics.core.tables.ODLFactory;
import com.opendoorlogistics.core.tables.ColumnValueProcessor;
import com.opendoorlogistics.core.tables.memory.ODLDatastoreImpl;
import com.opendoorlogistics.core.tables.memory.ODLTableDefinitionImpl;
import com.opendoorlogistics.core.tables.memory.ODLTableImpl;
import com.opendoorlogistics.core.tables.utils.DatastoreComparer;
import com.opendoorlogistics.core.tables.utils.DatastoreCopier;
import com.opendoorlogistics.core.utils.Colours;
import com.opendoorlogistics.core.utils.strings.Strings;
final public class BeanMapping {
public static class BeanColumnMapping {
private final PropertyDescriptor descriptor;
private final int userOrder;
private int tableColumnIndex;
private Object defaultValue;
private long flags;
private TreeSet<String> tags;
public BeanColumnMapping(PropertyDescriptor descriptor) {
super();
this.descriptor = descriptor;
ODLColumnOrder readColOrder = descriptor.getReadMethod().getAnnotation(ODLColumnOrder.class);
ODLColumnOrder writeColOrder = descriptor.getWriteMethod().getAnnotation(ODLColumnOrder.class);
int colOrder = Integer.MAX_VALUE;
if (readColOrder != null) {
colOrder = Math.min(readColOrder.value(), colOrder);
}
if (writeColOrder != null) {
colOrder = Math.min(writeColOrder.value(), colOrder);
}
this.userOrder = colOrder;
}
public String getName(){
ODLColumnName annotation = (ODLColumnName)getAnnotation(ODLColumnName.class);
if(annotation!=null){
return annotation.value();
}
// Capitalise the first letter of the name (looks better for field names)
String defaultName =getDescriptor().getName();
StringBuilder builder = new StringBuilder();
int len = defaultName.length();
if(len>0){
builder.append(Character.toUpperCase(defaultName.charAt(0)));
}
if(len>1){
builder.append(defaultName.substring(1, len));
}
return builder.toString();
}
public int getTableColumnIndex() {
return tableColumnIndex;
}
public void setTableColumnIndex(int tableColumnIndex) {
this.tableColumnIndex = tableColumnIndex;
}
public PropertyDescriptor getDescriptor() {
return descriptor;
}
public int getUserOrder() {
return userOrder;
}
public Annotation getAnnotation(Class<? extends Annotation> annotationCls){
Annotation ret = descriptor.getWriteMethod().getAnnotation(annotationCls);
if(ret==null){
ret = descriptor.getReadMethod().getAnnotation(annotationCls);
}
return ret;
}
public Object getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
}
public long getFlags() {
return flags;
}
public void setFlags(long flags) {
this.flags = flags;
}
@Override
public String toString(){
return descriptor.toString();
}
public TreeSet<String> getTags() {
return tags;
}
public void setTags(TreeSet<String> tags) {
this.tags = tags;
}
}
public static interface ReadObjectFilter{
boolean acceptObject(Object obj, ODLTableReadOnly inputTable,int row, long rowId, BeanTableMappingImpl btm);
}
public static class BeanTableMappingImpl implements BeanTableMapping{
private final List<BeanColumnMapping> columns;
private final ODLTableDefinition table;
private final Class<? extends BeanMappedRow> objectType;
private ReadObjectFilter rowfilter;
private boolean readFailsOnDisallowedNull = true;
public BeanTableMappingImpl(Class<? extends BeanMappedRow> type, List<BeanColumnMapping> columns, ODLTableDefinition table) {
this.objectType = type;
this.columns = columns;
this.table = table;
}
@Override
public ODLTableDefinition getTableDefinition() {
return table;
}
@Override
public boolean isReadFailsOnDisallowedNull() {
return readFailsOnDisallowedNull;
}
@Override
public void setReadFailsOnDisallowedNull(boolean failIfNulLValueNotAllowed) {
this.readFailsOnDisallowedNull = failIfNulLValueNotAllowed;
}
@Override
public Class<? extends BeanMappedRow> getBeanClass() {
return objectType;
}
public int getColumnCount(){
return columns.size();
}
public BeanColumnMapping getColumn(int i){
return columns.get(i);
}
/**
* Find the first column index containing the annotation
* @param cls
* @return
*/
public int indexOfAnnotation(Class<? extends Annotation> cls){
for(int i =0 ;i < getColumnCount() ; i++){
if(getColumn(i).getAnnotation(cls)!=null){
return i;
}
}
return -1;
}
/**
* Read object. If report is non-null then any failed read is logged as a failure in the report
* @param inputTable
* @param rowId
* @param report
* @return
*/
public <T extends BeanMappedRow> T readObjectFromTableById(ODLTableReadOnly inputTable, long rowId, ExecutionReport report) {
return readObjectFromTable(inputTable, -1, rowId,report);
}
/**
* Read object. If report is non-null then any failed read is logged as a failure in the report
* @param inputTable
* @param rowId
* @param report
* @return
*/
@Override
public <T extends BeanMappedRow> T readObjectFromTableByRow(ODLTableReadOnly inputTable, int row, ExecutionReport report) {
return readObjectFromTable(inputTable, row, -1,report);
}
public <T extends BeanMappedRow> T readObjectFromTableById(ODLTableReadOnly inputTable, long rowId) {
return readObjectFromTable(inputTable, -1, rowId,null);
}
public <T extends BeanMappedRow> T readObjectFromTableByRow(ODLTableReadOnly inputTable, int row) {
return readObjectFromTable(inputTable, row, -1,null);
}
public ReadObjectFilter getRowfilter() {
return rowfilter;
}
public void setRowfilter(ReadObjectFilter rowfilter) {
this.rowfilter = rowfilter;
}
/**
* Read the object using the row if available, otherwise read by globalid
* @param inputTable
* @param row
* @param rowId
* @return
*/
@SuppressWarnings("unchecked")
private <T extends BeanMappedRow> T readObjectFromTable(ODLTableReadOnly inputTable,int row, long rowId, ExecutionReport report) {
// Commented out this datastore structure check as its slow and probably not needed...
// if (!DatastoreComparer.isSameStructure(this.table, inputTable, DatastoreComparer.ALLOW_EXTRA_COLUMNS_ON_SECOND_TABLE)) {
// throw new RuntimeException("Input table does not match expected structure.");
// }
T ret = null;
try {
ret = (T)objectType.newInstance();
for (BeanColumnMapping bcm : columns) {
int col = bcm.getTableColumnIndex();
Object val = getValue(inputTable, row, rowId, col);
Class<?> fieldType = bcm.getDescriptor().getPropertyType();
val =BeanTypeConversion.getExternalValue(fieldType, val);
if(val==null){
val = bcm.getDefaultValue();
}
// ensure we don't set null on a primitive
if (val != null || fieldType.isPrimitive() == false) {
bcm.getDescriptor().getWriteMethod().invoke(ret, new Object[] { val });
}
if(val==null && bcm.getAnnotation(ODLNullAllowed.class)==null && readFailsOnDisallowedNull){
// cannot read object
if(report!=null){
report.setFailed("Null values are not allowed on field " + bcm.getName() + " in table " + inputTable.getName() + ".");
}
return null;
}
}
if(rowId!=-1){
ret.setGlobalRowId(rowId);
}else{
ret.setGlobalRowId(inputTable.getRowId(row));
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
if(ret!=null && rowfilter!=null){
if(!rowfilter.acceptObject(ret, inputTable, row, rowId, this)){
ret = null;
}
}
return ret;
}
public Object getValue(ODLTableReadOnly inputTable, int row, long rowId, int col) {
Object val;
if(row!=-1){
val = inputTable.getValueAt(row, col);
}else{
val = inputTable.getValueById(rowId, col);
}
return val;
}
@Override
public <T extends BeanMappedRow> List<T> readObjectsFromTable(ODLTableReadOnly inputTable) {
return readObjectsFromTable(inputTable, null);
}
@Override
public <T extends BeanMappedRow> List<T> readObjectsFromTable(ODLTableReadOnly inputTable, ExecutionReport report) {
int nr = inputTable.getRowCount();
ArrayList<T> ret = new ArrayList<>(nr);
for (int row = 0; row < nr; row++) {
T obj = readObjectFromTableByRow(inputTable, row,report);
if(obj!=null){
ret.add(obj);
}
}
return ret;
}
@Override
public <T extends BeanMappedRow> void updateTableRow(T object, ODLTable table, long rowId) {
updateTableRow(object, table, rowId,-1);
}
private void updateTableRow(BeanMappedRow object, ODLTable table, long rowId,int rowNb) {
try {
for (BeanColumnMapping bcm : columns) {
Object val = bcm.getDescriptor().getReadMethod().invoke(object);
int col = bcm.getTableColumnIndex();
ODLColumnType odlType = table.getColumnType(col);
val = ColumnValueProcessor.convertToMe(odlType,val);
if(rowId!=-1){
table.setValueById(val, rowId, col);
}
else if(rowNb!=-1 && rowNb < table.getRowCount()){
table.setValueAt(val, rowNb, col);
}
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Override
public long writeObjectToTable(BeanMappedRow o, ODLTable outTable) {
if (!DatastoreComparer.isSameStructure(this.table, outTable, 0)) {
throw new RuntimeException();
}
if (o == null) {
return -1;
}
if (objectType.isInstance(o) == false) {
throw new RuntimeException();
}
int row = outTable.createEmptyRow(-1);
long id = outTable.getRowId(row);
updateTableRow(o, outTable, id);
return outTable.getRowId(row);
}
public void writeObjectsToTable(BeanMappedRow[] objs, ODLTable outTable) {
for (BeanMappedRow object : objs) {
writeObjectToTable(object, outTable);
}
}
public ODLTableAlterable writeObjectsTable(BeanMappedRow[] objs, ODLDatastoreAlterable<? extends ODLTableAlterable> ds) {
ODLTableAlterable outTable = (ODLTableAlterable)DatastoreCopier.copyTableDefinition(table, ds);
if (outTable != null) {
writeObjectsToTable(objs, outTable);
}
return outTable;
}
public ODLTableAlterable writeObjectsToTable(BeanMappedRow[] objs) {
ODLTableAlterable outTable = createTable();
writeObjectsToTable(objs, outTable);
return outTable;
}
public ODLTableAlterable createTable() {
ODLTableImpl outTable = new ODLTableImpl(table.getImmutableId(), table.getName());
DatastoreCopier.copyTableDefinition(table, outTable);
return outTable;
}
@Override
public <T extends BeanMappedRow> void writeObjectsToTable(List<T> objs, ODLTable outTable) {
BeanMappedRow [] array = new BeanMappedRow[objs.size()];
objs.toArray(array);
writeObjectsToTable(array, outTable);
}
@Override
public <T extends BeanMappedRow> void writeObjectToTable(T obj, ODLTable outTable, int rowNb) {
while(rowNb>=outTable.getRowCount()){
outTable.createEmptyRow(-1);
}
updateTableRow(obj, outTable, -1, rowNb);
}
}
public static class BeanDatastoreMapping {
private final List<BeanTableMappingImpl> tables;
private final ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ds;
public BeanDatastoreMapping(List<BeanTableMappingImpl> tables, ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ds) {
super();
this.tables = tables;
this.ds = ds;
}
public ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> getDefinition() {
return ds;
}
public BeanTableMappingImpl getTableMapping(int tableIndx) {
return tables.get(tableIndx);
}
public BeanTableMappingImpl getTableMapping(String tableName){
for(BeanTableMappingImpl btm: tables){
if(Strings.equalsStd(btm.table.getName(), tableName)){
return btm;
}
}
return null;
}
public ODLDatastore<? extends ODLTable> writeObjectsToDatastore(BeanMappedRow[][] objs) {
if (objs.length != tables.size()) {
throw new RuntimeException();
}
ODLDatastoreAlterable<ODLTableAlterable> ret = ODLDatastoreImpl.alterableFactory.create();
for (int i = 0; i < objs.length; i++) {
tables.get(i).writeObjectsTable(objs[i], ret);
}
return ret;
}
public BeanMappedRow[][] readObjectsFromDatastore(ODLDatastore<? extends ODLTableReadOnly> input) {
if (input.getTableCount() != tables.size()) {
throw new RuntimeException();
}
BeanMappedRow[][] ret = new BeanMappedRow[tables.size()][];
for (int i = 0; i < ret.length; i++) {
List<? extends BeanMappedRow> objs= tables.get(i).readObjectsFromTable(input.getTableAt(i));
ret[i] = objs.toArray(new BeanMappedRow[objs.size()]);
}
return ret;
}
}
@SafeVarargs
public static BeanDatastoreMapping buildDatastore(Class<? extends BeanMappedRow>... classes) {
ODLDatastoreAlterable<ODLTableDefinitionAlterable> ds = ODLFactory.createDefinition();
ArrayList<BeanTableMappingImpl> tables = new ArrayList<>();
for (Class<? extends BeanMappedRow> cls : classes) {
BeanTableMappingImpl table = buildTable(cls, ds);
if (table != null) {
tables.add(table);
}
}
return new BeanDatastoreMapping(tables, ds);
}
private static BeanTableMappingImpl buildTable(Class<? extends BeanMappedRow> cls, ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ds) {
ODLTableDefinitionAlterable table = ds.createTable(getTableName(cls), -1);
if (table == null) {
return null;
}
List<BeanColumnMapping> list = buildTable(cls, table);
return new BeanTableMappingImpl(cls, list, table);
}
public static BeanTableMappingImpl buildTable(Class<? extends BeanMappedRow> cls) {
return buildTable(cls, getTableName(cls));
}
public static String getTableName(Class<? extends BeanMappedRow> cls){
ODLTableName tableName= cls.getAnnotation(ODLTableName.class);
if(tableName!=null){
return tableName.value();
}
return cls.getSimpleName();
}
public static <T extends BeanMappedRow> ODLTable convertToTable(Iterable<T> objs, Class<T> cls){
BeanTableMappingImpl mapping = buildTable(cls);
ODLTable ret = mapping.createTable();
for(T obj:objs){
mapping.writeObjectToTable(obj, ret);
}
return ret;
}
public static BeanTableMappingImpl buildTable(Class<? extends BeanMappedRow> cls, String name) {
ODLTableDefinitionImpl table = new ODLTableDefinitionImpl(-1, name);
List<BeanColumnMapping> list = buildTable(cls, table);
return new BeanTableMappingImpl(cls, list, table);
}
private static void findTags(Annotation [] annotations, Set<String> tags){
for(Annotation annotation :annotations){
if(ODLTag.class.isInstance(annotation)){
tags.add( ((ODLTag)annotation).value());
}
}
}
private static List<BeanColumnMapping> buildTable(Class<? extends BeanMappedRow> cls, ODLTableDefinitionAlterable outTable) {
ODLTableFlags flags= cls.getAnnotation(ODLTableFlags.class);
if(flags!=null){
outTable.setFlags(outTable.getFlags() | flags.value());
}
ArrayList<BeanColumnMapping> bcms = new ArrayList<>();
BeanInfo beanInfo = null;
try {
beanInfo = java.beans.Introspector.getBeanInfo(cls);
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
// get table tags
TreeSet<String> tableTags = new TreeSet<>();
findTags(cls.getAnnotations(), tableTags);
if(tableTags.size()>0){
outTable.setTags(tableTags);
}
for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
if (property.getWriteMethod() != null && property.getReadMethod() != null) {
if (property.getWriteMethod().getAnnotation(ODLIgnore.class) != null || property.getReadMethod().getAnnotation(ODLIgnore.class) != null) {
continue;
}
ODLColumnType colType = BeanTypeConversion.getInternalType(property.getPropertyType());
if (colType!=null) {
BeanColumnMapping bcm = new BeanColumnMapping(property);
// try getting default
switch(colType){
case COLOUR:{
ODLDefaultStringValue anno = (ODLDefaultStringValue)bcm.getAnnotation(ODLDefaultStringValue.class);
if(anno!=null){
bcm.setDefaultValue(Colours.getColourByString(anno.value()));
}
break;
}
case STRING:{
ODLDefaultStringValue anno = (ODLDefaultStringValue)bcm.getAnnotation(ODLDefaultStringValue.class);
if(anno!=null){
bcm.setDefaultValue(anno.value());
}
break;
}
case LONG:{
ODLDefaultLongValue anno = (ODLDefaultLongValue)bcm.getAnnotation(ODLDefaultLongValue.class);
if(anno!=null){
bcm.setDefaultValue(anno.value());
}
break;
}
case DOUBLE:{
ODLDefaultDoubleValue anno = (ODLDefaultDoubleValue)bcm.getAnnotation(ODLDefaultDoubleValue.class);
if(anno!=null){
bcm.setDefaultValue(anno.value());
}
break;
}
default:
break;
}
// set if the field is optional
if(bcm.getAnnotation(ODLNullAllowed.class)!=null){
bcm.setFlags(bcm.getFlags() | TableFlags.FLAG_IS_OPTIONAL);
}
// get column tags
TreeSet<String> tags = new TreeSet<>();
findTags(property.getWriteMethod().getAnnotations(), tags);
findTags(property.getReadMethod().getAnnotations(), tags);
bcm.setTags(tags);
bcms.add(bcm);
} else {
throw new RuntimeException("Found get/set method " + property.getName() + " with unsupported type " + property.getPropertyType().getName()
+ " in class " + cls.getName() + ".");
}
}
}
// sort by user column order
Collections.sort(bcms, new Comparator<BeanColumnMapping>() {
@Override
public int compare(BeanColumnMapping o1, BeanColumnMapping o2) {
int diff = Integer.compare(o1.getUserOrder(), o2.getUserOrder());
if (diff == 0) {
diff = o1.getName().compareTo(o2.getName());
}
return diff;
}
});
if (outTable.getColumnCount() != 0) {
throw new RuntimeException();
}
// read table tags
TreeSet<String> tags = new TreeSet<>();
findTags(cls.getAnnotations(), tags);
outTable.setTags(tags);
// add the columns to the table
Iterator<BeanColumnMapping> it = bcms.iterator();
while (it.hasNext()) {
BeanColumnMapping bcm = it.next();
ODLColumnType odlType = BeanTypeConversion.getInternalType(bcm.getDescriptor().getPropertyType());
if (outTable.addColumn(-1,bcm.getName(), odlType, bcm.getFlags())!=-1) {
int col=outTable.getColumnCount() - 1;
bcm.setTableColumnIndex(col);
outTable.setColumnTags(col, bcm.getTags());
// read the description
ODLColumnDescription description = (ODLColumnDescription)bcm.getAnnotation(ODLColumnDescription.class);
if(description!=null){
outTable.setColumnDescription(col, description.value());
}
// set default values
if(bcm.getDefaultValue()!=null){
outTable.setColumnDefaultValue(col, bcm.getDefaultValue());
}
} else {
it.remove();
}
}
return bcms;
}
}