| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.knox.gateway.shell.table; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ThreadLocalRandom; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.swing.SortOrder; |
| import com.fasterxml.jackson.annotation.JsonFilter; |
| |
| import org.apache.commons.math3.stat.StatUtils; |
| |
| /** |
| * Simple table representation and text based rendering of a table via |
| * toString(). Headers are optional but when used must have the same count as |
| * columns within the rows. |
| */ |
| @JsonFilter("knoxShellTableFilter") |
| public class KnoxShellTable { |
| |
| private enum Conversions { |
| DOUBLE, |
| INTEGER, |
| FLOAT, |
| BYTE, |
| SHORT, |
| LONG, |
| STRING |
| } |
| |
| private static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
| |
| List<String> headers = new ArrayList<>(); |
| List<List<Comparable<? extends Object>>> rows = new ArrayList<>(); |
| String title; |
| long id; |
| |
| public KnoxShellTable() { |
| this.id = getUniqueTableId(); |
| } |
| |
| public KnoxShellTable title(String title) { |
| this.title = title; |
| return this; |
| } |
| |
| public KnoxShellTable header(String header) { |
| headers.add(header); |
| return this; |
| } |
| |
| public KnoxShellTable row() { |
| rows.add(new ArrayList<>()); |
| return this; |
| } |
| |
| public KnoxShellTable value(Comparable<? extends Object> value) { |
| final int index = rows.isEmpty() ? 0 : rows.size() - 1; |
| final List<Comparable<? extends Object>> row = rows.get(index); |
| row.add(value); |
| return this; |
| } |
| |
| public KnoxShellTableCell<? extends Comparable<? extends Object>> cell(int colIndex, int rowIndex) { |
| return new KnoxShellTableCell(headers, rows, colIndex, rowIndex); |
| } |
| |
| public List<Comparable<? extends Object>> values(int colIndex) { |
| List<Comparable<? extends Object>> col = new ArrayList<>(); |
| rows.forEach(row -> col.add(row.get(colIndex))); |
| return col; |
| } |
| |
| public List<Comparable<? extends Object>> values(String colName) { |
| int colIndex = headers.indexOf(colName); |
| List<Comparable<? extends Object>> col = new ArrayList<>(); |
| rows.forEach(row -> col.add(row.get(colIndex))); |
| return col; |
| } |
| |
| private Conversions getConversion(Comparable<? extends Object> colIndex) { |
| Conversions type = null; |
| if (colIndex instanceof Double) { |
| type = Conversions.DOUBLE; |
| } |
| else if (colIndex instanceof Integer) { |
| type = Conversions.INTEGER; |
| } |
| else if (colIndex instanceof Float) { |
| type = Conversions.FLOAT; |
| } |
| else if (colIndex instanceof Byte) { |
| type = Conversions.BYTE; |
| } |
| else if (colIndex instanceof Short) { |
| type = Conversions.SHORT; |
| } |
| else if (colIndex instanceof Long) { |
| type = Conversions.LONG; |
| } |
| else if (colIndex instanceof String) { |
| if (((String) colIndex).matches("-?\\d+(\\.\\d+)?")) { |
| type = Conversions.STRING; |
| } |
| else { |
| throw new IllegalArgumentException("String contains non-numeric characters"); |
| } |
| } |
| else { |
| throw new IllegalArgumentException("Unsupported data type"); |
| } |
| return type; |
| } |
| |
| private double[] toDoubleArray(String colName) throws IllegalArgumentException { |
| List<Comparable<? extends Object>> col = values(colName); |
| double[] colArray = new double[col.size()]; |
| Conversions conversionMethod = null; |
| for (int i = 0; i < col.size(); i++) { |
| if (i == 0) { |
| conversionMethod = getConversion(col.get(i)); |
| } |
| switch (conversionMethod) { |
| case DOUBLE: |
| colArray[i] = (Double) col.get(i); |
| break; |
| case INTEGER: |
| colArray[i] = (Integer) col.get(i); |
| break; |
| case FLOAT: |
| colArray[i] = (Float) col.get(i); |
| break; |
| case BYTE: |
| colArray[i] = (Byte) col.get(i); |
| break; |
| case SHORT: |
| colArray[i] = (Short) col.get(i); |
| break; |
| case LONG: |
| colArray[i] = (double) (Long) col.get(i); |
| break; |
| case STRING: |
| colArray[i] = Double.parseDouble((String) col.get(i)); |
| break; |
| } |
| } |
| return colArray; |
| } |
| |
| /** |
| * Calculates the mean of specified column |
| * @param colName the column for which the mean will be calculated |
| * @return mean |
| */ |
| public double mean(String colName) { |
| return StatUtils.mean(toDoubleArray(colName)); |
| } |
| |
| /** |
| * Calculates the mean of specified column |
| * @param colIndex the column for which the mean will be calculated |
| * @return mean |
| */ |
| public double mean(int colIndex) { |
| return mean(headers.get(colIndex)); |
| } |
| |
| /** |
| * Calculates the median of specified column |
| * @param colName the column for which the median will be calculated |
| * @return median |
| */ |
| public double median(String colName) { |
| return StatUtils.percentile(toDoubleArray(colName), 50); |
| } |
| |
| /** |
| * Calculates the median of specified column |
| * @param colIndex the column for which the median will be calculated |
| * @return median |
| */ |
| public double median(int colIndex) { |
| return median(headers.get(colIndex)); |
| } |
| |
| /** |
| * Calculates the mode of specified column |
| * @param colName the column for which the mode will be calculated |
| * @return mode |
| */ |
| public double mode(String colName) { |
| return StatUtils.mode(toDoubleArray(colName))[0]; |
| } |
| |
| /** |
| * Calculates the mode of specified column |
| * @param colIndex the column for which the mode will be calculated |
| * @return mode |
| */ |
| public double mode(int colIndex) { |
| return mode(headers.get(colIndex)); |
| } |
| |
| /** |
| * Calculates the sum of specified column |
| * @param colName the column for which the sum will be calculated |
| * @return sum |
| */ |
| public double sum(String colName) { |
| return StatUtils.sum(toDoubleArray(colName)); |
| } |
| |
| /** |
| * Calculates the sum of specified column |
| * @param colIndex the column for which the sum will be calculated |
| * @return sum |
| */ |
| public double sum(int colIndex) { |
| return sum(headers.get(colIndex)); |
| } |
| |
| /** |
| * Calculates the max of specified column |
| * @param colName the column for which the max will be calculated |
| * @return max |
| */ |
| public double max(String colName) { |
| return StatUtils.max(toDoubleArray(colName)); |
| } |
| |
| /** |
| * Calculates the max of specified column |
| * @param colIndex the column for which the max will be calculated |
| * @return max |
| */ |
| public double max(int colIndex) { |
| return max(headers.get(colIndex)); |
| } |
| |
| /** |
| * Calculates the min of specified column |
| * @param colName the column for which the min will be calculated |
| * @return min |
| */ |
| public double min(String colName) { |
| return StatUtils.min(toDoubleArray(colName)); |
| } |
| |
| /** |
| * Calculates the min of specified column |
| * @param colIndex the column for which the min will be calculated |
| * @return min |
| */ |
| public double min(int colIndex) { |
| return min(headers.get(colIndex)); |
| } |
| |
| public KnoxShellTable apply(KnoxShellTableCell<? extends Comparable<? extends Object>> cell) { |
| if (!headers.isEmpty()) { |
| headers.set(cell.colIndex, cell.header); |
| } |
| if (!rows.isEmpty()) { |
| rows.get(cell.rowIndex).set(cell.colIndex, cell.value); |
| } |
| return this; |
| } |
| |
| public List<String> getHeaders() { |
| return headers == null || headers.isEmpty() ? null : headers; |
| } |
| |
| public List<List<Comparable<? extends Object>>> getRows() { |
| return rows; |
| } |
| |
| public String getTitle() { |
| return title; |
| } |
| |
| public long getId() { |
| return id; |
| } |
| |
| public static KnoxShellTableBuilder builder() { |
| return new KnoxShellTableBuilder(); |
| } |
| |
| static long getUniqueTableId() { |
| return System.currentTimeMillis() + ThreadLocalRandom.current().nextLong(1000); |
| } |
| |
| public List<KnoxShellTableCall> getCallHistoryList() { |
| final List<KnoxShellTableCall> sanitizedCallHistoryList = new LinkedList<>(); |
| KnoxShellTableCallHistory.getInstance().getCallHistory(id).forEach(call -> { |
| final Map<Object, Class<?>> params = call.hasSensitiveData() ? Collections.singletonMap("***", String.class) : call.getParams(); |
| sanitizedCallHistoryList.add(new KnoxShellTableCall(call.getInvokerClass(), call.getMethod(), call.isBuilderMethod(), params)); |
| }); |
| return sanitizedCallHistoryList; |
| } |
| |
| public String getCallHistory() { |
| final StringBuilder callHistoryStringBuilder = new StringBuilder("Call history (id=") |
| .append(id).append(')').append(LINE_SEPARATOR).append(LINE_SEPARATOR); |
| final AtomicInteger index = new AtomicInteger(1); |
| getCallHistoryList().forEach(callHistory -> callHistoryStringBuilder.append("Step ") |
| .append(index.getAndIncrement()).append(':').append(LINE_SEPARATOR).append(callHistory).append(LINE_SEPARATOR)); |
| return callHistoryStringBuilder.toString(); |
| } |
| |
| public String rollback() { |
| final KnoxShellTable rolledBack = KnoxShellTableCallHistory.getInstance().rollback(id); |
| this.id = rolledBack.id; |
| this.title = rolledBack.title; |
| this.headers = rolledBack.headers; |
| this.rows = rolledBack.rows; |
| return "Successfully rolled back"; |
| } |
| |
| public KnoxShellTable replayAll() { |
| final int step = KnoxShellTableCallHistory.getInstance().getCallHistory(id).size(); |
| return replay(step); |
| } |
| |
| public KnoxShellTable replay(int step) { |
| return replay(id, step); |
| } |
| |
| public static KnoxShellTable replay(long id, int step) { |
| return KnoxShellTableCallHistory.getInstance().replay(id, step); |
| } |
| |
| public KnoxShellTableFilter filter() { |
| return new KnoxShellTableFilter(this); |
| } |
| |
| public KnoxShellTable select(String cols) { |
| KnoxShellTable table = new KnoxShellTable(); |
| List<List<Comparable<? extends Object>>> columns = new ArrayList<>(); |
| String[] colnames = cols.split(","); |
| for (String colName : colnames) { |
| table.header(colName); |
| columns.add(values(headers.indexOf(colName))); |
| } |
| for (int i = 0; i < rows.size(); i++) { |
| table.row(); |
| for (List<Comparable<? extends Object>> col : columns) { |
| table.value(col.get(i)); |
| } |
| } |
| return table; |
| } |
| |
| public KnoxShellTable sort(String colName) { |
| return sort(colName, SortOrder.ASCENDING); |
| } |
| |
| public KnoxShellTable sort(String colName, SortOrder order) { |
| KnoxShellTable table = new KnoxShellTable(); |
| |
| Comparable<? extends Object> value; |
| List<Comparable<? extends Object>> col = values(colName); |
| List<RowIndex> index = new ArrayList<>(); |
| for (int i = 0; i < col.size(); i++) { |
| value = col.get(i); |
| index.add(new RowIndex(value, i)); |
| } |
| if (SortOrder.ASCENDING.equals(order)) { |
| Collections.sort(index); |
| } |
| else { |
| index.sort(Collections.reverseOrder()); |
| } |
| table.headers = new ArrayList<>(headers); |
| for (RowIndex i : index) { |
| table.rows.add(new ArrayList<>(this.rows.get(i.index))); |
| } |
| return table; |
| } |
| |
| private static class RowIndex implements Comparable<RowIndex> { |
| Comparable value; |
| int index; |
| |
| RowIndex(Comparable<? extends Object> value, int index) { |
| this.value = value; |
| this.index = index; |
| } |
| |
| @Override |
| public int compareTo(RowIndex other) { |
| return this.value.compareTo(other.value); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return new KnoxShellTableRenderer(this).toString(); |
| } |
| |
| public String toJSON() { |
| return toJSON((String) null); |
| } |
| |
| public String toJSON(boolean data) { |
| return toJSON(data, null); |
| } |
| |
| public String toJSON(String path) { |
| return toJSON(true, path); |
| } |
| |
| public String toJSON(boolean data, String path) { |
| return KnoxShellTableJSONSerializer.serializeKnoxShellTable(this, data, path); |
| } |
| |
| public String toCSV() { |
| return new KnoxShellTableRenderer(this).toCSV(); |
| } |
| } |