package com.w11k.lsql.query;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.w11k.lsql.LSql;
import com.w11k.lsql.ResultSetColumn;
import com.w11k.lsql.converter.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static com.google.common.collect.Lists.newLinkedList;
public class QueryToTreeConverter {
private static class SegmentHeader {
private int firstColumn;
private int lastColumn;
public SegmentHeader(int firstColumn, int lastColumn) {
this.firstColumn = firstColumn;
this.lastColumn = lastColumn;
}
public int getFirstColumn() {
return firstColumn;
}
public int getLastColumn() {
return lastColumn;
}
}
private static class MarkerColumnValue {
private final String path;
private final Object value;
public MarkerColumnValue(String path, Object value) {
this.path = path;
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MarkerColumnValue that = (MarkerColumnValue) o;
return path.equals(that.path) && value.equals(that.value);
}
@Override
public int hashCode() {
int result = path.hashCode();
result = 31 * result + value.hashCode();
return result;
}
@Override
public String toString() {
return "MarkerColumnValue{" +
"path='" + path + '\'' +
", value=" + value +
'}';
}
}
private final Logger logger = LoggerFactory.getLogger(getClass());
private final LSql lSql;
private final EntityCreator entityCreator;
private final AbstractQuery<?> query;
public QueryToTreeConverter(AbstractQuery<?> query,
EntityCreator entityCreator) {
this.query = query;
this.lSql = query.getlSql();
this.entityCreator = entityCreator;
}
public List<?> getTree() {
try {
return buildTree();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private List<?> buildTree()
throws SQLException {
ResultSet resultSet = this.query.getPreparedStatement().executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
// Table segments
TreeMap<String, SegmentHeader> segmentHeaders = Maps.newTreeMap();
// Normal columns
Map<Integer, ResultSetColumn> columns = Maps.newLinkedHashMap();
// Build table segments and columns meta data
int lastMarkerIndex = 1;
String lastLabel = metaData.getColumnLabel(lastMarkerIndex).replaceAll(" ", "");
assert isColumnMarker(lastLabel);
columns.put(1, new ResultSetColumn(1, lastLabel, new MarkerColumnConverter()));
for (int i = 2; i <= metaData.getColumnCount(); i++) {
String label = metaData.getColumnLabel(i);
if (isColumnMarker(label)) {
label = label.replaceAll(" ", "");
columns.put(i, new ResultSetColumn(i, label, new MarkerColumnConverter()));
assert !segmentHeaders.containsKey(lastLabel);
segmentHeaders.put(lastLabel, new SegmentHeader(lastMarkerIndex, i - 1));
lastLabel = label;
lastMarkerIndex = i;
} else {
label = lSql.identifierSqlToJava(label);
Optional<Converter> converter = this.query.getConverterForResultSetColumn(metaData, i, label, false);
if (converter.isPresent()) {
columns.put(i, new ResultSetColumn(i, label, converter.get()));
}
}
}
segmentHeaders.put(lastLabel, new SegmentHeader(lastMarkerIndex, metaData.getColumnCount()));
// Entities
Map<List<MarkerColumnValue>, Object> entities = Maps.newHashMap();
List<Object> topLevelRows = Lists.newLinkedList();
// Iterate rows
while (resultSet.next()) {
TreeMap<String, Object> markerColumnValues = Maps.newTreeMap();
// First get marker column values to be independent from the marker ordering
for (Map.Entry<String, SegmentHeader> segment : segmentHeaders.entrySet()) {
int firstColumnIndex = segment.getValue().getFirstColumn();
ResultSetColumn rsc = columns.get(firstColumnIndex);
String path = rsc.getName();
Object value = rsc.getConverter().getValueFromResultSet(this.lSql, resultSet, firstColumnIndex);
markerColumnValues.put(path, value);
}
// Iterate segments and create entities
for (Map.Entry<String, SegmentHeader> segment : segmentHeaders.entrySet()) {
// Has current row data for this segment?
if (markerColumnValues.get(segment.getKey()) == null) {
continue;
}
// create entity for current segment
List<MarkerColumnValue> fullPath = getFullPath(segment, markerColumnValues);
if (!entities.containsKey(fullPath)) {
List<MarkerColumnValue> parentPath = fullPath.subList(0, fullPath.size() - 1);
Object parent = entities.get(parentPath);
String path = segment.getKey();
String fieldNameInParent = getFieldNameInParent(path);
boolean isList = isPathList(path);
Object entity = createEntity(
parent,
fieldNameInParent,
isList,
resultSet,
columns,
segment.getValue().getFirstColumn() + 1,
segment.getValue().getLastColumn());
entities.put(fullPath, entity);
if (fullPath.size() == 1) {
topLevelRows.add(entity);
}
}
}
}
return topLevelRows;
}
private Object createEntity(Object parent,
String fieldNameInParent,
boolean isList,
ResultSet resultSet,
Map<Integer, ResultSetColumn> columns,
int firstColumn,
int lastColumn) {
Object entity = this.entityCreator.createEntity(parent, fieldNameInParent, isList);
for (int i = firstColumn; i <= lastColumn; i++) {
ResultSetColumn column = columns.get(i);
try {
String label = column.getName();
Object val = column.getConverter().getValueFromResultSet(lSql, resultSet, i);
this.entityCreator.setValue(entity, label, val);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return entity;
}
private List<MarkerColumnValue> getFullPath(Map.Entry<String, SegmentHeader> segment,
TreeMap<String, Object> markerColumnValues) {
List<MarkerColumnValue> fullPath = newLinkedList();
String path = segment.getKey();
for (Map.Entry<String, Object> entry : markerColumnValues.entrySet()) {
if (path.startsWith(entry.getKey())) {
fullPath.add(new MarkerColumnValue(entry.getKey(), entry.getValue()));
}
}
return fullPath;
}
private boolean isColumnMarker(String rawLabel) {
rawLabel = rawLabel.trim();
return rawLabel.startsWith("/") || rawLabel.startsWith("=");
}
private boolean isPathList(String path) {
return path.trim().startsWith("/");
}
private String getFieldNameInParent(String path) {
return path.substring(path.lastIndexOf("/") + 1).trim();
}
}