/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.orm.sqldataexpress.object;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.scooterframework.common.util.Converters;
import com.scooterframework.common.util.StringUtil;
import com.scooterframework.orm.sqldataexpress.config.DatabaseConfig;
/**
* RowData class represents a row in TableData object.
*
* @author (Fei) John Chen
*/
public class RowData implements RESTified, Serializable {
/**
* Generated serialVersionUID
*/
private static final long serialVersionUID = 891263627275680195L;
public RowData(RowInfo rowInfo, Object[] data) {
if (rowInfo != null) {
this.rowInfo = rowInfo;
}
else {
this.rowInfo = new RowInfo();
}
if (data == null) {
if (rowInfo.getDimension() >0)
this.data = rowInfo.getColumnDefaults();
}
else {
initialized = true;
this.data = data;
}
//set dataMap
createDataMap();
}
/**
* <p>
* Returns the restified id of the resource.
* </p>
*
* <p>By default, this method returns a string of the primary key value of
* the record. If the primary key is a composite key, a separator
* ({@link com.scooterframework.orm.sqldataexpress.config.DatabaseConfig#PRIMARY_KEY_SEPARATOR}) is used
* to link values of the key fields. The order of the fields of a composite
* primary key is defined by the {@link #getRestfulIdNames getRestfulIdNames} method.
* </p>
*
* <p>If the underlying data does not have primary key, <tt>null</tt> is
* returned.</p>
*
* <p>Subclass may override this method if a customized string format is
* required.</p>
*
* @return id String
*/
public String getRestfulId() {
if (!hasInitialized()) return null;
String[] idNames = getRestfulIdNames();
if (idNames == null || idNames.length == 0) return null;
String result = "";
int total = idNames.length;
for (int i = 0; i < total-1; i++) {
Object o = getField(idNames[i]);
String pkValue = (o != null)?o.toString():"";
result += pkValue + DatabaseConfig.PRIMARY_KEY_SEPARATOR;
}
result += getField(idNames[total-1]);
return result;
}
/**
* Returns column names corresponding to the RESTful id. If there is
* primary key, the column names come from primary key. Otherwise,
* <tt>null</tt> is returned.
*
* @return a string array
*/
public String[] getRestfulIdNames() {
return (rowInfo != null)?rowInfo.getPrimaryKeyColumnNames():null;
}
/**
* Returns the data map for the restified id. By default, the keys in the
* map are primary key column names in lower case. If there is no primary
* key, an empty map is returned.
*
* @return map of restified id data
*/
public Map<String, Object> getRestfulIdMap() {
Map<String, Object> map = new HashMap<String, Object>();
if (!hasInitialized()) return map;
String[] idNames = getRestfulIdNames();
if (idNames == null) return map;
int total = idNames.length;
for (int i = 0; i < total-1; i++) {
String key = idNames[i];
Object value = getField(key);
map.put(key, value);
}
return map;
}
/**
* <p>Sets the id value of the resource. The format of the id string must
* follow the pattern of the corresponding id config. If the id is backed
* by a composite primary key, a separator
* ({@link com.scooterframework.orm.sqldataexpress.config.DatabaseConfig#PRIMARY_KEY_SEPARATOR})
* must be used to link values of each primary key column.</p>
*
* <pre>
* Examples:
* id string id config array description
* --------- --------------- -------
* 0001 [id] an order
* 0001-99 [order_id, id] an item of an order
*
* </pre>
*
* @param id
*/
public void setRestfulId(String id) {
if (id == null) throw new IllegalArgumentException("Input id is null.");
String[] ids = Converters.convertStringToStringArray(id, DatabaseConfig.PRIMARY_KEY_SEPARATOR, false);
String[] fields = getRestfulIdNames();
if (ids.length != fields.length)
throw new IllegalArgumentException("Input id does not match id config.");
int total = ids.length;
for (int i = 0; i < total; i++) {
setField(fields[i], ids[i]);
}
}
/**
* returns plain data
*/
public Object[] getFields() {
return data;
}
/**
* <p>Sets data.</p>
*
* <p>The order of values in the data array must be the same as the order of
* column names in the RowInfo object of this RowData instance.</p>
*/
public void setFields(Object[] data) {
this.data = data;
//reset dataMap
createDataMap();
}
/**
* Returns column data for a column index
*
* index: 0, 1, 2, ...
*/
public Object getField(int index) {
if (data == null || index >= data.length) return null;
return data[index];
}
/**
* Sets column data for a column index
*
* index: 0, 1, 2, ...
*/
public void setField(int index, Object columnData) {
if (data == null || index >= data.length) return;
data[index] = columnData;
createDataMap();
}
/**
* Returns column data for a column name
*/
public Object getField(String columnName) {
if (data == null || columnName == null) return null;
return dataMap.get(columnName.toUpperCase());
}
/**
* Sets column data for a column name
*
* If there is no such a columnName, an InvalidColumnNameException
* will be thrown.
*/
public void setField(String columnName, Object columnData) {
if (data == null || columnName == null) return;
if (rowInfo == null || rowInfo.getDimension() == 0) return;
int columnIndex = rowInfo.getColumnPositionIndex(columnName);
data[columnIndex] = columnData;
dataMap.put(columnName.toUpperCase(), columnData);
}
/**
* returns row meta data
*/
public RowInfo getRowInfo() {
return rowInfo;
}
/**
* sets row meta data
*/
void setRowInfo(RowInfo newRowInfo) {
if (newRowInfo != null) {
if (newRowInfo.getDimension() != rowInfo.getDimension()) {
throw new IllegalArgumentException("The input newRowInfo " +
"must have the same dimension as the original rowInfo.");
}
rowInfo = newRowInfo;
}
else {
throw new IllegalArgumentException("\"rowInfo\" input cannot be null in setRowInfo().");
}
//reset dataMap
createDataMap();
}
/**
* Clears all existing data except the primary key data.
*
*/
public void clearData() {
if (data != null && data.length > 0) {
int columnSize = data.length;
for (int i=0; i<columnSize; i++) {
ColumnInfo ci = rowInfo.getColumnInfo(i);
if (ci == null || ci.isPrimaryKey()) continue;
data[i] = null;
dataMap.put(ci.getColumnName().toUpperCase(), null);
}
}
}
/**
* Clears all existing data and resets data from a Map.
*
* The key of the data entry in the Map is corresponding to a
* column name in the RowInfo object. If the key is not a column name,
* its value is ignored. If the column name is not in the key set of
* the Map, the column data is set to null.
*
*/
public void clearAndSetData(Map<String, ?> inputDataMap) {
clearData();
setData(inputDataMap);
}
/**
* <p>Sets data from a Map.</p>
*
* <p>The key of the data entry in the Map is corresponding to a
* column name in the RowInfo object. If the key is not a column name, its
* value is ignored. If the column name is not in the key set of the
* Map, the column data is not updated. To set those column data
* to null when the column name is not in the key set, use the
* {@link #setField(java.lang.String, java.lang.Object) setField} method.</p>
*
* <p>This method is restrictive. If a column is readonly, or not writable,
* or is primary key, then the data for the column in the
* <tt>inputDataMap</tt> is ignored. In that case, use the
* {@link #setField(java.lang.String, java.lang.Object) setField} method</p>
*
* @return a list of modified column names
*/
public List<String> setData(Map<String, ?> inputDataMap) {
if (inputDataMap == null) return null;
if (rowInfo == null)
throw new IllegalArgumentException("Failed to setData(Map) for a " +
"RowData instance because the instance has no RowInfo.");
//convert all keys to capital font strings
Map<String, Object> tmp = StringUtil.convertKeyToUpperCase(inputDataMap);
Set<String> inputKeys = tmp.keySet();
int recordWidth = rowInfo.getDimension();
//create a new data array if there isn't.
if (data == null) data = new Object[recordWidth];
ColumnInfo ci = null;
List<String> modifiedColumnList = new ArrayList<String>();
for (int i=0; i<recordWidth; i++) {
ci = rowInfo.getColumnInfo(i);
String columnName = ci.getColumnName();
if (columnName == null) continue;
columnName = columnName.toUpperCase();
if (ci.isReadOnly() || !ci.isWritable() ||
!inputKeys.contains(columnName)) continue;
data[i] = tmp.get(columnName);
modifiedColumnList.add(columnName);
}
//reset dataMap
createDataMap();
return modifiedColumnList;
}
/**
* returns the data as a readonly Map. The keys in the Map are
* column names in upper case.
*
* Modifications to this map have no impact on the row data.
*/
public Map<String, Object> getDataMap() {
return dataMap;
}
/**
* returns the data as a readonly Map. The keys in the Map are
* requested column names in upper case.
*
* Modifications to this map have no impact on the row data.
*/
public Map<String, Object> getDataMap(List<String> columnNames) {
Map<String, Object> newMap = new HashMap<String, Object>();
if (columnNames == null) return newMap;
Iterator<String> it = columnNames.iterator();
while(it.hasNext()) {
String columnName = it.next();
Object columnData = getField(columnName);
newMap.put(columnName, columnData);
}
return newMap;
}
/**
* returns primary key data as a readonly Map. The keys in the
* Map are primary key column names in lower case.
*
* Modifications to this map have no impact on the row data.
*/
public Map<String, Object> getPrimaryKeyDataMap() {
if (rowInfo == null) return new HashMap<String, Object>();
Map<String, Object> pkDataMap = new HashMap<String, Object>();
String[] pkNames = rowInfo.getPrimaryKeyColumnNames();
if (pkNames == null) return pkDataMap;
int pkColCount = pkNames.length;
for (int i = 0; i < pkColCount; i++) {
String pkName = pkNames[i];
Object colData = null;
if (data != null && data.length > 0) {
colData = getField(pkName);
}
pkDataMap.put(pkName.toLowerCase(), colData);
}
return pkDataMap;
}
/**
* returns primary key data as a string which comes from the primary key
* data map.
*
* @return string
*/
public String getPrimaryKeyDataString() {
Map<String, Object> pkMap = getPrimaryKeyDataMap();
if (pkMap == null) return null;
return pkMap.toString();
}
/**
* Indicates if there is primary key defined.
*
* @return true if there is primary key defined.
*/
public boolean hasPrimaryKey() {
return (rowInfo != null)?rowInfo.hasPrimaryKey():false;
}
/**
* Checks if the record is initialized.
*
* @return true if the record is initialized
*/
public boolean hasInitialized() {
return initialized;
}
/**
* returns columnSize
*/
public int getSize() {
return (data != null)?data.length:0;
}
/**
* returns a list of RowData objects for a child from Map
*/
public List<RowData> getChildRowListFromMap(String key) {
if (key == null) return null;
return childRowListMap.get(key.toUpperCase());
}
/**
* adds a child RowData object to Map
*/
public void addChildRowToMap(String key, RowData childRow) {
List<RowData> childRowList = getChildRowListFromMap(key);
if (childRowList != null) childRowList.add(childRow);
}
/**
* adds a list of child RowData objects to Map
*/
public void addChildRowToMap(String key, List<RowData> childRowList) {
if (key == null) return;
childRowListMap.put(key.toUpperCase(), childRowList);
}
/**
* Returns a string representation of the object.
* @return String
*/
public String toString() {
StringBuilder returnString = new StringBuilder();
String LINE_BREAK = "\r\n";
returnString.append("columnSize = " + getSize());
returnString.append(LINE_BREAK);
returnString.append("data (separated by comma): ");
if (data != null) {
for (int i = 0; i < data.length; i++) {
returnString.append(data[i] + ", ");
}
}
returnString.append(LINE_BREAK);
returnString.append("childMap size = " + childRowListMap.size());
returnString.append(LINE_BREAK);
for (Map.Entry<String, List<RowData>> entry : childRowListMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
returnString.append("child key = " + key + " content = " + value);
returnString.append(LINE_BREAK);
}
return returnString.toString();
}
/**
* set a Map of column name and column data.
*/
private void createDataMap() {
if (rowInfo == null)
throw new IllegalArgumentException("RowInfo must be set first.");
initialized = true;
dataMap.clear();
if (data != null && data.length > 0) {
int columnSize = data.length;
for (int i=0; i<columnSize; i++) {
ColumnInfo ci = rowInfo.getColumnInfo(i);
if (ci == null)
throw new IllegalArgumentException("There is no ColumnInfo for index " + i);
dataMap.put(ci.getColumnName().toUpperCase(), data[i]);
}
}
}
private boolean initialized = false;
private RowInfo rowInfo = new RowInfo();
private Object[] data = null;
private Map<String, Object> dataMap = new HashMap<String, Object>();
private Map<String, List<RowData>> childRowListMap = new HashMap<String, List<RowData>>();
}