package net.openhft.chronicle.engine.map;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.util.ObjectUtils;
import net.openhft.chronicle.engine.api.column.ClosableIterator;
import net.openhft.chronicle.engine.api.column.Column;
import net.openhft.chronicle.engine.api.column.MapColumnView;
import net.openhft.chronicle.engine.api.column.Row;
import net.openhft.chronicle.engine.api.map.MapView;
import net.openhft.chronicle.engine.api.tree.Asset;
import net.openhft.chronicle.engine.api.tree.RequestContext;
import net.openhft.chronicle.wire.FieldInfo;
import net.openhft.chronicle.wire.Marshallable;
import net.openhft.chronicle.wire.Wires;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
import static net.openhft.chronicle.core.util.ObjectUtils.convertTo;
/**
* @author Rob Austin.
*/
public class MapWrappingColumnView<K, V> implements MapColumnView {
private final RequestContext requestContext;
private final Asset asset;
@NotNull
private final MapView<K, V> mapView;
private final boolean valueMarshallable;
private final boolean valueMap;
@Nullable
private ArrayList<String> columnNames = null;
public MapWrappingColumnView(RequestContext requestContext,
Asset asset,
@NotNull MapView<K, V> mapView) {
this.requestContext = requestContext;
this.asset = asset;
this.mapView = mapView;
valueMarshallable = Marshallable.class.isAssignableFrom(mapView.valueType());
valueMap = Map.class.isAssignableFrom(mapView.valueType());
}
@Override
public void registerChangeListener(@NotNull Runnable r) {
mapView.registerSubscriber(o -> r.run());
}
private Comparator<Map.Entry<K, V>> sort(@NotNull final List<MarshableOrderBy> marshableOrderBy) {
return (o1, o2) -> {
if (o1 == null)
return o2 == null ? 0 : -1;
if (o2 == null)
return 1;
for (@NotNull MarshableOrderBy order : marshableOrderBy) {
final String column = order.column;
int result = 0;
Object c1;
Object c2;
if (column.equals("key")) {
c1 = o1.getKey();
c2 = o2.getKey();
} else if (valueMap) {
c1 = ((Map) o1).get(column);
c2 = ((Map) o2).get(column);
} else if (valueMarshallable) {
try {
FieldInfo field = Wires.fieldInfo(o1.getValue().getClass(), column);
c1 = field.get(o1.getValue());
c2 = field.get(o2.getValue());
} catch (Exception e) {
Jvm.warn().on(MapWrappingColumnView.class, e);
// skip the field.
continue;
}
} else if (column.equals("value")) {
c1 = o1.getValue();
c2 = o2.getValue();
} else {
// no such column.
continue;
}
if (c1.getClass() == c2.getClass() && c1 instanceof Comparable && !(c1 instanceof CharSequence)) {
result = ((Comparable) c1).compareTo(c2);
} else {
result = String.CASE_INSENSITIVE_ORDER.compare(c1.toString(), c2.toString());
}
if (result != 0)
return order.isAscending ? -result : result;
}
return 0;
};
}
@NotNull
@Override
public ClosableIterator<Row> iterator(@NotNull final SortedFilter sortedFilter) {
final Iterator<Map.Entry<K, V>> core = mapView.entrySet().stream()
.filter(filter(sortedFilter.marshableFilters))
.sorted(sort(sortedFilter.marshableOrderBy))
.iterator();
@NotNull final ClosableIterator<Row> result = new ClosableIterator<Row>() {
@Override
public void close() {
// do nothing
}
@Override
public boolean hasNext() {
return core.hasNext();
}
@NotNull
@Override
public Row next() {
final Map.Entry e = core.next();
@NotNull final Row row = new Row(columns());
addColumns(row, mapView.keyType(), "key", e.getKey());
addColumns(row, mapView.valueType(), "value", e.getValue());
return row;
}
private void addColumns(@NotNull Row row,
final Class type,
final String defaultColumnName,
final Object item) {
final List<FieldInfo> fieldInfos = Wires.fieldInfos(type);
if (fieldInfos.isEmpty()) {
row.set(defaultColumnName, item);
} else {
@NotNull final Marshallable value = (Marshallable) item;
for (@NotNull final FieldInfo info : fieldInfos) {
if (!columnNames().contains(info.name()))
continue;
try {
final Object newValue = info.get(value);
row.set(info.name(), newValue);
} catch (Exception e1) {
Jvm.warn().on(VanillaMapView.class, e1);
}
}
}
}
};
long x = 0;
while (x++ < sortedFilter.fromIndex && result.hasNext()) {
result.next();
}
return result;
}
@Override
public boolean containsRowWithKey(@NotNull List keys) {
assert keys.size() == 1;
return mapView.containsKey((K) keys.get(0));
}
@Nullable
@Override
public ObjectSubscription objectSubscription() {
return mapView.asset().getView(ObjectSubscription.class);
}
@Override
public Asset asset() {
return asset;
}
@NotNull
@Override
public List<Column> columns() {
@NotNull List<Column> result = new ArrayList<>();
if ((Marshallable.class.isAssignableFrom(keyType()))) {
for (@NotNull final FieldInfo info : Wires.fieldInfos(keyType())) {
result.add(new Column(info.name(), true, true, "", info.type(), true));
}
} else {
result.add(new Column("key", true, true, "", keyType(), true));
}
boolean isReadOnly = requestContext.toUri().startsWith("/proc");
if ((Marshallable.class.isAssignableFrom(valueType()))) {
//valueType.isAssignableFrom()
for (@NotNull final FieldInfo info : Wires.fieldInfos(valueType())) {
result.add(new Column(info.name(), isReadOnly, false, "", info.type(), true));
}
} else {
result.add(new Column("value", isReadOnly, false, "", valueType(), true));
}
return result;
}
private Class<?> keyType() {
return mapView.keyType();
}
@Nullable
private ArrayList<String> columnNames() {
if (columnNames != null)
return columnNames;
@NotNull LinkedHashSet<String> result = new LinkedHashSet<>();
if ((Marshallable.class.isAssignableFrom(keyType()))) {
for (@NotNull final FieldInfo fi : Wires.fieldInfos(keyType())) {
result.add(fi.name());
}
} else
result.add("key");
if (Marshallable.class.isAssignableFrom(valueType())) {
for (@NotNull final FieldInfo fi : Wires.fieldInfos(valueType())) {
result.add(fi.name());
}
} else {
result.add("value");
}
columnNames = new ArrayList<>(result);
return columnNames;
}
private Class<V> valueType() {
return mapView.valueType();
}
@Override
public boolean canDeleteRows() {
if (requestContext.toUri().startsWith("/proc"))
return false;
return true;
}
@Override
public int changedRow(@NotNull Map<String, Object> row, @NotNull Map<String, Object> oldRow) {
assert row.isEmpty() && oldRow.isEmpty() : "both rows can not be empty";
if (row.isEmpty()) {
@NotNull final K k = (K) oldRow.get("key");
if (k == null)
throw new IllegalStateException("key not found");
return mapView.remove(k) == null ? 0 : 1;
}
final Object oldKey = oldRow.get("key");
final Object newKey = row.get("key");
if (oldKey != null && !oldKey.equals(newKey))
mapView.remove((K) oldKey);
final Class<V> valueType = mapView.valueType();
if (!Marshallable.class.isAssignableFrom(valueType)) {
if (!row.containsKey("value"))
throw new IllegalStateException("value not found");
assert row.size() == 2;
assert oldRow.size() == 2;
mapView.put((K) newKey, (V) row.get("value"));
return 1;
}
@NotNull final V v = ObjectUtils.newInstance(valueType);
for (@NotNull Map.Entry<String, Object> e : row.entrySet()) {
if ("key".equals(e.getKey()))
continue;
FieldInfo fi = Wires.fieldInfo(valueType, e.getKey());
fi.set(v, e.getValue());
}
mapView.put((K) newKey, v);
return 1;
}
@Nullable
private Predicate<Map.Entry<K, V>> filter(@NotNull List<MarshableFilter> filters) {
@Nullable final Predicate<Number> predicate = predicate(filters);
return entry -> {
if (filters.isEmpty())
return true;
try {
for (@NotNull MarshableFilter f : filters) {
Object item;
if ("key".equals(f.columnName)) {
item = entry.getKey();
} else if (!(Marshallable.class.isAssignableFrom(mapView.valueType())) &&
"value".equals(f.columnName)) {
item = entry.getValue();
} else if (Marshallable.class.isAssignableFrom(mapView.valueType())) {
try {
final Class valueClass = entry.getValue().getClass();
final FieldInfo info = Wires.fieldInfo(valueClass, f.columnName);
final Object o = info.get(entry.getValue());
if (o == null)
return false;
if (o instanceof Number) {
if (predicate.test((Number) o))
continue;
return false;
}
item = o;
} catch (Exception e) {
return false;
}
} else {
throw new UnsupportedOperationException();
}
if (item instanceof CharSequence) {
if (!item.toString().toLowerCase().contains(f.filter.toLowerCase()))
return false;
} else if (item instanceof Number) {
if (!predicate.test((Number) item))
return false;
} else {
if (!item.equals(convertTo(item.getClass(), f.filter.trim())))
return false;
}
}
return true;
} catch (NumberFormatException e) {
return false;
}
};
}
/**
* @param sortedFilter if {@code sortedFilter} == null or empty all the total number of rows is
* returned
* @return the number of rows the matches this query
*/
@Override
public int rowCount(@Nullable SortedFilter sortedFilter) {
if (sortedFilter == null || sortedFilter.marshableFilters.isEmpty())
return (int) mapView.longSize();
return (int) mapView.entrySet().stream()
.filter(filter(sortedFilter.marshableFilters))
.count();
}
}