KNOX-2023 - Recording KnoxShellTable builder/filter chain and providing rollback/replay capabilities using the call history as well as allowing end-users to export JSON without data (in this case only the call history will be serialized) (#162)
* KNOX-2023 - Recording KnoxShellTable builder/filter chain and providing rollback/replay capabilities using the call history as well as allowing end-users to export JSON without data (in this case only the call history will be serialized)
* KNOX-2023 - Minor change on separating the BufferedeReader form InputStreamReader within the try block
* KNOX-2023 - Enhanced type check
diff --git a/gateway-shell-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh
index ad205b5..8355ab1 100755
--- a/gateway-shell-release/home/bin/knoxshell.sh
+++ b/gateway-shell-release/home/bin/knoxshell.sh
@@ -94,7 +94,7 @@
;;
*)
buildAppJavaOpts
- $JAVA "${APP_JAVA_OPTS[@]}" -jar "$APP_JAR" "$@" || exit 1
+ $JAVA "${APP_JAVA_OPTS[@]}" -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -jar "$APP_JAR" "$@" || exit 1
;;
esac
diff --git a/gateway-shell-release/src/assembly.xml b/gateway-shell-release/src/assembly.xml
index 181f770..0e15ff2 100644
--- a/gateway-shell-release/src/assembly.xml
+++ b/gateway-shell-release/src/assembly.xml
@@ -86,5 +86,12 @@
<include>org.apache.knox:hadoop-examples</include>
</includes>
</dependencySet>
+ <dependencySet>
+ <outputDirectory>lib</outputDirectory>
+ <outputFileNameMapping>aspectjweaver.jar</outputFileNameMapping>
+ <includes>
+ <include>org.aspectj:aspectjweaver</include>
+ </includes>
+ </dependencySet>
</dependencySets>
</assembly>
\ No newline at end of file
diff --git a/gateway-shell/pom.xml b/gateway-shell/pom.xml
index 139355b..61e61d4 100644
--- a/gateway-shell/pom.xml
+++ b/gateway-shell/pom.xml
@@ -118,5 +118,17 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.aspectj</groupId>
+ <artifactId>aspectjrt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.aspectj</groupId>
+ <artifactId>aspectjweaver</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/CSVKnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/CSVKnoxShellTableBuilder.java
index db5a9e7..cc39265 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/CSVKnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/CSVKnoxShellTableBuilder.java
@@ -20,6 +20,7 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
@@ -28,6 +29,10 @@
private boolean withHeaders;
+ CSVKnoxShellTableBuilder(long id) {
+ super(id);
+ }
+
public CSVKnoxShellTableBuilder withHeaders() {
withHeaders = true;
return this;
@@ -35,17 +40,16 @@
public KnoxShellTable url(String url) throws IOException {
int rowIndex = 0;
- URLConnection connection;
- BufferedReader csvReader = null;
KnoxShellTable table = null;
- try {
- URL urlToCsv = new URL(url);
- connection = urlToCsv.openConnection();
- csvReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
+ URL urlToCsv = new URL(url);
+ URLConnection connection = urlToCsv.openConnection();
+ try (Reader urlConnectionStreamReader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8);
+ BufferedReader csvReader = new BufferedReader(urlConnectionStreamReader);) {
table = new KnoxShellTable();
if (title != null) {
table.title(title);
}
+ table.id(id);
String row = null;
while ((row = csvReader.readLine()) != null) {
boolean addingHeaders = (withHeaders && rowIndex == 0);
@@ -63,8 +67,6 @@
}
rowIndex++;
}
- } finally {
- csvReader.close();
}
return table;
}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
index d958c5d..9d275a2 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
@@ -33,6 +33,10 @@
private Connection conn;
private boolean tableManagedConnection = true;
+ JDBCKnoxShellTableBuilder(long id) {
+ super(id);
+ }
+
@Override
public JDBCKnoxShellTableBuilder title(String title) {
this.title = title;
@@ -75,20 +79,9 @@
KnoxShellTable table = null;
conn = conn == null ? DriverManager.getConnection(connectionUrl) : conn;
if (conn != null) {
- try (Statement statement = conn.createStatement(); ResultSet result = statement.executeQuery(sql);) {
+ try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(sql);) {
table = new KnoxShellTable();
- final ResultSetMetaData metadata = result.getMetaData();
- table.title(metadata.getTableName(1));
- int colcount = metadata.getColumnCount();
- for (int i = 1; i < colcount + 1; i++) {
- table.header(metadata.getColumnName(i));
- }
- while (result.next()) {
- table.row();
- for (int i = 1; i < colcount + 1; i++) {
- table.value(result.getObject(metadata.getColumnName(i), Comparable.class));
- }
- }
+ processResultSet(table, resultSet);
} finally {
if (conn != null && tableManagedConnection) {
conn.close();
@@ -98,21 +91,27 @@
return table;
}
- public KnoxShellTable build(ResultSet resultSet) throws SQLException {
- KnoxShellTable table = new KnoxShellTable();
- ResultSetMetaData metadata = resultSet.getMetaData();
+ // added this as a private method so that KnoxShellTableHistoryAspect will not
+ // intercept this call
+ private void processResultSet(KnoxShellTable table, ResultSet resultSet) throws SQLException {
+ final ResultSetMetaData metadata = resultSet.getMetaData();
+ final int colCount = metadata.getColumnCount();
table.title(metadata.getTableName(1));
- int colcount = metadata.getColumnCount();
- for (int i = 1; i < colcount + 1; i++) {
+ table.id(id);
+ for (int i = 1; i < colCount + 1; i++) {
table.header(metadata.getColumnName(i));
}
while (resultSet.next()) {
table.row();
- for (int i = 1; i < colcount + 1; i++) {
- table.value(resultSet.getString(metadata.getColumnName(i)));
+ for (int i = 1; i < colCount + 1; i++) {
+ table.value(resultSet.getObject(metadata.getColumnName(i), Comparable.class));
}
}
- return table;
}
+ public KnoxShellTable resultSet(ResultSet resultSet) throws SQLException {
+ final KnoxShellTable table = new KnoxShellTable();
+ processResultSet(table, resultSet);
+ return table;
+ }
}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JSONKnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JSONKnoxShellTableBuilder.java
index b2a67cf..71bd27d 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JSONKnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JSONKnoxShellTableBuilder.java
@@ -30,7 +30,16 @@
public class JSONKnoxShellTableBuilder extends KnoxShellTableBuilder {
+ JSONKnoxShellTableBuilder(long id) {
+ super(id);
+ }
+
public KnoxShellTable fromJson(String json) throws IOException {
+ return toKnoxShellTable(json);
+ }
+
+ // introduced a private method so that it can be invoked from both public ones and AspectJ will not intercept it
+ private KnoxShellTable toKnoxShellTable(String json) throws IOException {
final ObjectMapper mapper = new ObjectMapper(new JsonFactory());
final SimpleModule module = new SimpleModule();
module.addDeserializer(KnoxShellTable.class, new KnoxShellTableRowDeserializer());
@@ -41,10 +50,11 @@
if (title != null) {
table.title(title);
}
+ table.id(id);
return table;
}
public KnoxShellTable path(String path) throws IOException {
- return fromJson(FileUtils.readFileToString(new File(path), StandardCharsets.UTF_8));
+ return toKnoxShellTable(FileUtils.readFileToString(new File(path), StandardCharsets.UTF_8));
}
}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JoinKnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JoinKnoxShellTableBuilder.java
index fc74a1d..0cfe327 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JoinKnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JoinKnoxShellTableBuilder.java
@@ -21,11 +21,15 @@
import java.util.Iterator;
import java.util.List;
-class JoinKnoxShellTableBuilder extends KnoxShellTableBuilder {
+public class JoinKnoxShellTableBuilder extends KnoxShellTableBuilder {
private KnoxShellTable left;
private KnoxShellTable right;
+ JoinKnoxShellTableBuilder(long id) {
+ super(id);
+ }
+
@Override
public JoinKnoxShellTableBuilder title(String title) {
this.title = title;
@@ -42,17 +46,18 @@
return this;
}
- KnoxShellTable on(String columnName) {
+ public KnoxShellTable on(String columnName) {
final int leftIndex = left.headers.indexOf(columnName);
final int rightIndex = right.headers.indexOf(columnName);
return on(leftIndex, rightIndex);
}
- KnoxShellTable on(int leftIndex, int rightIndex) {
+ public KnoxShellTable on(int leftIndex, int rightIndex) {
final KnoxShellTable joinedTable = new KnoxShellTable();
if (title != null) {
joinedTable.title(title);
}
+ joinedTable.id(id);
joinedTable.headers.addAll(new ArrayList<String>(left.headers));
for (List<Comparable<? extends Object>> row : left.rows) {
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
index 04e5320..00b47cc 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
@@ -20,10 +20,12 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.SortOrder;
-import org.apache.knox.gateway.util.JsonUtils;
+import com.fasterxml.jackson.annotation.JsonFilter;
/**
@@ -31,17 +33,25 @@
* toString(). Headers are optional but when used must have the same count as
* columns within the rows.
*/
+@JsonFilter("knoxShellTableFilter")
public class KnoxShellTable {
+ private static final String LINE_SEPARATOR = System.getProperty("line.separator");
List<String> headers = new ArrayList<String>();
List<List<Comparable<? extends Object>>> rows = new ArrayList<List<Comparable<? extends Object>>>();
String title;
+ long id;
public KnoxShellTable title(String title) {
this.title = title;
return this;
}
+ public KnoxShellTable id(long id) {
+ this.id = id;
+ return this;
+ }
+
public KnoxShellTable header(String header) {
headers.add(header);
return this;
@@ -98,12 +108,55 @@
return title;
}
+ public long getId() {
+ return id;
+ }
+
public static KnoxShellTableBuilder builder() {
- return new KnoxShellTableBuilder();
+ return new KnoxShellTableBuilder(getUniqueTableId());
+ }
+
+ static long getUniqueTableId() {
+ return System.currentTimeMillis() + ThreadLocalRandom.current().nextLong(1000);
+ }
+
+ public List<KnoxShellTableCall> getCallHistoryList() {
+ return KnoxShellTableCallHistory.getInstance().getCallHistory(id);
+ }
+
+ public String getCallHistory() {
+ final StringBuilder callHistoryStringBuilder = new StringBuilder("Call history (id=" + id + ")" + LINE_SEPARATOR + LINE_SEPARATOR);
+ final AtomicInteger index = new AtomicInteger(1);
+ getCallHistoryList().forEach(callHistory -> {
+ callHistoryStringBuilder.append("Step ").append(index.getAndIncrement()).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().table(this);
+ return new KnoxShellTableFilter(this);
}
public KnoxShellTable select(String cols) {
@@ -171,7 +224,11 @@
}
public String toJSON() {
- return JsonUtils.renderAsJsonString(this);
+ return toJSON(true);
+ }
+
+ public String toJSON(boolean data) {
+ return KnoxShellTableJSONSerializer.serializeKnoxShellTable(this, data);
}
public String toCSV() {
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java
index 5dcc5af..c602243 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java
@@ -17,9 +17,13 @@
*/
package org.apache.knox.gateway.shell.table;
-
public class KnoxShellTableBuilder {
protected String title;
+ protected final long id;
+
+ KnoxShellTableBuilder(long id) {
+ this.id = id;
+ }
public KnoxShellTableBuilder title(String title) {
this.title = title;
@@ -27,18 +31,18 @@
}
public CSVKnoxShellTableBuilder csv() {
- return new CSVKnoxShellTableBuilder();
+ return new CSVKnoxShellTableBuilder(id);
}
public JSONKnoxShellTableBuilder json() {
- return new JSONKnoxShellTableBuilder();
+ return new JSONKnoxShellTableBuilder(id);
}
public JoinKnoxShellTableBuilder join() {
- return new JoinKnoxShellTableBuilder();
+ return new JoinKnoxShellTableBuilder(id);
}
public JDBCKnoxShellTableBuilder jdbc() {
- return new JDBCKnoxShellTableBuilder();
+ return new JDBCKnoxShellTableBuilder(id);
}
}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java
new file mode 100644
index 0000000..5592124
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java
@@ -0,0 +1,91 @@
+/*
+ * 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.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.knox.gateway.util.NoClassNameMultiLineToStringStyle;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+class KnoxShellTableCall {
+
+ static final String KNOX_SHELL_TABLE_FILTER_TYPE = "org.apache.knox.gateway.shell.table.KnoxShellTableFilter";
+
+ private final String invokerClass;
+ private final String method;
+ private boolean builderMethod;
+ private final Map<Object, Class<?>> params;
+
+ KnoxShellTableCall(String invokerClass, String method, boolean builderMethod, Map<Object, Class<?>> params) {
+ this.invokerClass = invokerClass;
+ this.method = method;
+ this.builderMethod = builderMethod;
+ this.params = params;
+ }
+
+ public String getInvokerClass() {
+ return invokerClass;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public boolean isBuilderMethod() {
+ return builderMethod;
+ }
+
+ public Map<Object, Class<?>> getParams() {
+ return params == null ? Collections.emptyMap() : params;
+ }
+
+ @JsonIgnore
+ Class<?>[] getParameterTypes() {
+ final List<Class<?>> parameterTypes = new ArrayList<Class<?>>(params.size());
+ if (KNOX_SHELL_TABLE_FILTER_TYPE.equals(invokerClass) && builderMethod) {
+ parameterTypes.add(Comparable.class);
+ } else {
+ parameterTypes.addAll(params.values());
+ }
+
+ return parameterTypes.toArray(new Class<?>[0]);
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return EqualsBuilder.reflectionEquals(this, obj);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, new NoClassNameMultiLineToStringStyle());
+ }
+
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java
new file mode 100644
index 0000000..da22495
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java
@@ -0,0 +1,152 @@
+/*
+ * 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.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A wrapper class to maintain the chain of builder/filter call invocations
+ * which resulted in a {@link KnoxShellTable} being built
+ *
+ * The following useful functions are exposed:
+ * <ul>
+ * <li>replay: a {@link KnoxShellTable} can be built by replaying a previously saved
+ * call history</li>
+ * <li>rollback: any {@link KnoxShellTable} can be rolled back to it's previous valid
+ * state (if any)</li>
+ * </ul>
+ */
+class KnoxShellTableCallHistory {
+
+ private static final KnoxShellTableCallHistory INSTANCE = new KnoxShellTableCallHistory();
+ private final Map<Long, List<KnoxShellTableCall>> callHistory = new ConcurrentHashMap<>();
+
+ private KnoxShellTableCallHistory() {
+ };
+
+ static KnoxShellTableCallHistory getInstance() {
+ return INSTANCE;
+ }
+
+ void saveCall(long id, KnoxShellTableCall call) {
+ saveCalls(id, Arrays.asList(call));
+ }
+
+ void saveCalls(long id, List<KnoxShellTableCall> calls) {
+ if (!callHistory.containsKey(id)) {
+ callHistory.put(id, new LinkedList<>());
+ }
+ callHistory.get(id).addAll(calls);
+ }
+
+ void removeCallsById(long id) {
+ if (callHistory.containsKey(id)) {
+ callHistory.remove(id);
+ }
+ }
+
+ public List<KnoxShellTableCall> getCallHistory(long id) {
+ return callHistory.containsKey(id) ? Collections.unmodifiableList(callHistory.get(id)) : Collections.emptyList();
+ }
+
+ /**
+ * Rolls back the given table to its previous valid state. This means the table
+ * can be rolled back if there is any previous (i.e. not the last one) step in
+ * its call history that produces a {@link KnoxShellTable}
+ *
+ * @param id
+ * the table to apply the rollback operation on
+ * @return the previous valid state of the table identified by <code>id</code>
+ * @throws IllegalArgumentException
+ * if the rollback operation is not permitted
+ *
+ */
+ KnoxShellTable rollback(long id) {
+ final AtomicInteger counter = new AtomicInteger(1);
+ final List<Integer> validSteps = new ArrayList<>();
+ getCallHistory(id).forEach(call -> {
+ int step = counter.getAndIncrement();
+ if (call.isBuilderMethod()) {
+ validSteps.add(step);
+ }
+ });
+ if (validSteps.size() <= 1) {
+ throw new IllegalArgumentException("There is no valid step to be rollback to");
+ }
+ return replay(id, validSteps.get(validSteps.size() - 2));
+ }
+
+ /**
+ * Tries to replay the previously saved call history of the given table.
+ *
+ * @param id
+ * the table to apply the replay operation on
+ * @param step
+ * the step up to where call history should be replayed
+ * @return the {@link KnoxShellTable} as a result of the previously saved call
+ * invocations
+ * @throws IllegalArgumentException
+ * if the the given call indicated by the given step does not produce
+ * a {@link KnoxShellTable}
+ */
+ KnoxShellTable replay(long id, int step) {
+ final List<KnoxShellTableCall> callHistory = getCallHistory(id);
+ validateReplayStep(step, callHistory);
+ Object callResult = KnoxShellTable.builder();
+ for (int counter = 0; counter < step; counter++) {
+ callResult = invokeCall(callResult, callHistory.get(counter));
+ }
+ return (KnoxShellTable) callResult;
+ }
+
+ private void validateReplayStep(int step, List<KnoxShellTableCall> callHistory) {
+ final AtomicInteger counter = new AtomicInteger(1);
+ callHistory.forEach(call -> {
+ if (counter.getAndIncrement() == step && !call.isBuilderMethod()) {
+ throw new IllegalArgumentException(
+ String.format(Locale.getDefault(), "It is not allowed to replay up to step %d as this step does not produce an intance of KnoxShellTable", step));
+ }
+ });
+ }
+
+ private Object invokeCall(Object callResult, KnoxShellTableCall call) {
+ try {
+ final Class<?> invokerClass = Class.forName(call.getInvokerClass());
+ final Class<?>[] parameterTypes = call.getParameterTypes();
+ final Method method = invokerClass.getMethod(call.getMethod(), parameterTypes);
+ final Object[] params = new Object[call.getParams().size()];
+ final AtomicInteger index = new AtomicInteger(0);
+ for (Map.Entry<Object, Class<?>> param : call.getParams().entrySet()) {
+ params[index.getAndIncrement()] = param.getValue().cast(param.getKey());
+ }
+ return method.invoke(callResult, params);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Error while processing " + call, e);
+ }
+ }
+
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableFilter.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableFilter.java
index 32bf9db..62a80f3 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableFilter.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableFilter.java
@@ -23,12 +23,16 @@
public class KnoxShellTableFilter {
- private KnoxShellTable tableToFilter;
- private int index = -1;
+ final long id;
+ final KnoxShellTable tableToFilter;
+ private int index;
- public KnoxShellTableFilter table(KnoxShellTable table) {
+ KnoxShellTableFilter(KnoxShellTable table) {
+ this.id = KnoxShellTable.getUniqueTableId();
this.tableToFilter = table;
- return this;
+ //inheriting the original table's call history
+ final List<KnoxShellTableCall> callHistory = KnoxShellTableCallHistory.getInstance().getCallHistory(tableToFilter.id);
+ KnoxShellTableCallHistory.getInstance().saveCalls(id, callHistory);
}
public KnoxShellTableFilter name(String name) throws KnoxShellTableFilterException {
@@ -39,7 +43,7 @@
}
}
if (index == -1) {
- throw new KnoxShellTableFilterException("Column name not found");
+ throw new KnoxShellTableFilterException("Column name not found");
}
return this;
}
@@ -49,13 +53,12 @@
return this;
}
- //TODO: use Predicate to evaluate the Pattern.matches
+ // TODO: use Predicate to evaluate the Pattern.matches
// for regular expressions: startsWith, endsWith, contains,
// doesn't contain, etc
- public KnoxShellTable regex(String regex) {
- final Pattern pattern = Pattern.compile(regex);
- final KnoxShellTable filteredTable = new KnoxShellTable();
- filteredTable.headers.addAll(tableToFilter.headers);
+ public KnoxShellTable regex(Comparable<String> regex) {
+ final Pattern pattern = Pattern.compile((String) regex);
+ final KnoxShellTable filteredTable = prepareFilteredTable();
for (List<Comparable<?>> row : tableToFilter.rows) {
if (pattern.matcher(row.get(index).toString()).matches()) {
filteredTable.row();
@@ -67,21 +70,28 @@
return filteredTable;
}
+ private KnoxShellTable prepareFilteredTable() {
+ final KnoxShellTable filteredTable = new KnoxShellTable();
+ filteredTable.id(id);
+ filteredTable.headers.addAll(tableToFilter.headers);
+ filteredTable.title(tableToFilter.title);
+ return filteredTable;
+ }
+
@SuppressWarnings("rawtypes")
private KnoxShellTable filter(Predicate<Comparable> p) throws KnoxShellTableFilterException {
try {
- final KnoxShellTable filteredTable = new KnoxShellTable();
- filteredTable.headers.addAll(tableToFilter.headers);
- for (List<Comparable<? extends Object>> row : tableToFilter.rows) {
- if (p.test(row.get(index))) {
- filteredTable.row(); // Adds a new empty row to filtered table
- // Add each value to the row
- row.forEach(value -> {
- filteredTable.value(value);
- });
+ final KnoxShellTable filteredTable = prepareFilteredTable();
+ for (List<Comparable<? extends Object>> row : tableToFilter.rows) {
+ if (p.test(row.get(index))) {
+ filteredTable.row(); // Adds a new empty row to filtered table
+ // Add each value to the row
+ row.forEach(value -> {
+ filteredTable.value(value);
+ });
+ }
}
- }
- return filteredTable;
+ return filteredTable;
} catch (Exception e) {
throw new KnoxShellTableFilterException(e);
}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableHistoryAspect.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableHistoryAspect.java
new file mode 100644
index 0000000..0679f32
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableHistoryAspect.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+
+/**
+ * An AspectJ aspect that intercepts different {@link KnoxShellTable},
+ * {@link KnoxShellTableBuilder} and {@link KnoxShellTableFilter} method
+ * invocations and records these calls in {@link KnoxShellTableCallHistory}
+ */
+@Aspect
+public class KnoxShellTableHistoryAspect {
+
+ private static final String KNOX_SHELL_TYPE = "org.apache.knox.gateway.shell.table.KnoxShellTable";
+
+ @Pointcut("execution(public org.apache.knox.gateway.shell.table.KnoxShellTableFilter org.apache.knox.gateway.shell.table.KnoxShellTable.filter(..))")
+ public void knoxShellTableCreateFilterPointcut() {
+ }
+
+ @Pointcut("execution(public * org.apache.knox.gateway.shell.table.*KnoxShellTableBuilder.*(..))")
+ public void knoxShellTableBuilderPointcut() {
+ }
+
+ @Pointcut("execution(public * org.apache.knox.gateway.shell.table.*KnoxShellTableFilter.*(..))")
+ public void knoxShellTableFilterPointcut() {
+ }
+
+ @Around("org.apache.knox.gateway.shell.table.KnoxShellTableHistoryAspect.knoxShellTableCreateFilterPointcut()")
+ public KnoxShellTableFilter whenCreatingFilter(ProceedingJoinPoint joinPoint) throws Throwable {
+ KnoxShellTableFilter filter = null;
+ try {
+ filter = (KnoxShellTableFilter) joinPoint.proceed();
+ return filter;
+ } finally {
+ saveKnoxShellTableCall(joinPoint, filter.id);
+ }
+ }
+
+ @After("org.apache.knox.gateway.shell.table.KnoxShellTableHistoryAspect.knoxShellTableBuilderPointcut()")
+ public void afterBuilding(JoinPoint joinPoint) throws Throwable {
+ final long builderId = ((KnoxShellTableBuilder) joinPoint.getTarget()).id;
+ saveKnoxShellTableCall(joinPoint, builderId);
+ }
+
+ @After("org.apache.knox.gateway.shell.table.KnoxShellTableHistoryAspect.knoxShellTableFilterPointcut()")
+ public void afterFiltering(JoinPoint joinPoint) throws Throwable {
+ final long builderId = ((KnoxShellTableFilter) joinPoint.getTarget()).id;
+ saveKnoxShellTableCall(joinPoint, builderId);
+ }
+
+ private void saveKnoxShellTableCall(JoinPoint joinPoint, long builderId) {
+ final Signature signature = joinPoint.getSignature();
+ final boolean builderMethod = KNOX_SHELL_TYPE.equals(((MethodSignature) signature).getReturnType().getCanonicalName());
+ final Map<Object, Class<?>> params = new HashMap<>();
+ Arrays.stream(joinPoint.getArgs()).forEach(param -> params.put(param, param.getClass()));
+ KnoxShellTableCallHistory.getInstance().saveCall(builderId, new KnoxShellTableCall(signature.getDeclaringTypeName(), signature.getName(), builderMethod, params));
+ }
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java
new file mode 100644
index 0000000..748d71f
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java
@@ -0,0 +1,63 @@
+/*
+ * 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.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+import org.apache.knox.gateway.util.JsonUtils;
+
+import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
+import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+
+/**
+ * Utility class that helps serializing a {@link KnoxShellTable} in JSON format. The
+ * reasons this class exists are:
+ * <ol>
+ * <li>to define the @DateFormat we use when serializing/deserializing a @Date
+ * Object</li>
+ * <li>conditionally exclude certain fields from the serialized JSON
+ * representation</li>
+ */
+class KnoxShellTableJSONSerializer {
+
+ static final DateFormat JSON_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.getDefault());
+
+ /**
+ * Serializes the given {@link KnoxShellTable}
+ *
+ * @param table
+ * the table to be serialized
+ * @param data
+ * if this is <code>true</code> the underlying JSON serializer will
+ * output the table's content; otherwise the table's
+ * <code>callHistory</code> will be serilized
+ * @return the serialized table in JSON format
+ */
+ static String serializeKnoxShellTable(KnoxShellTable table, boolean data) {
+ SimpleFilterProvider filterProvider = new SimpleFilterProvider();
+ if (data) {
+ filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("headers", "rows", "title", "id"));
+ } else {
+ filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("callHistoryList"));
+ }
+ return JsonUtils.renderAsJsonString(table, filterProvider, JSON_DATE_FORMAT);
+ }
+
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableRowDeserializer.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableRowDeserializer.java
index cd372d5..bcc839d 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableRowDeserializer.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableRowDeserializer.java
@@ -18,18 +18,29 @@
package org.apache.knox.gateway.shell.table;
import java.io.IOException;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* A custom @JsonDeserializer in order to be able to deserialize a previously
- * serialzed @KnoxShellTable. It is requiored because Jackson is not capable of
- * constructing instance of `java.lang.Comparable` (which we have int he table
- * cells)
+ * serialized {@link KnoxShellTable}. It is required because
+ * <ul>
+ * <li>Jackson is not capable of constructing instance of `java.lang.Comparable`
+ * (which we have in the table cells)</li>
+ * <li><code>callHistory</code> deserialization requires special handling
+ * </ul>
*
*/
@SuppressWarnings("serial")
@@ -46,6 +57,83 @@
@Override
public KnoxShellTable deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
final TreeNode jsonContent = parser.readValueAsTree();
+ if (jsonContent.get("callHistoryList") != null) {
+ return parseJsonWithCallHistory(jsonContent);
+ } else {
+ return parseJsonWithData(jsonContent);
+ }
+ }
+
+ private KnoxShellTable parseJsonWithCallHistory(TreeNode jsonContent) throws IOException {
+ final List<KnoxShellTableCall> calls = parseCallHistoryListJSONNode(jsonContent.get("callHistoryList"));
+ long tempId = KnoxShellTable.getUniqueTableId();
+ KnoxShellTableCallHistory.getInstance().saveCalls(tempId, calls);
+ final KnoxShellTable table = KnoxShellTableCallHistory.getInstance().replay(tempId, calls.size());
+ KnoxShellTableCallHistory.getInstance().removeCallsById(tempId);
+ return table;
+ }
+
+ private List<KnoxShellTableCall> parseCallHistoryListJSONNode(TreeNode callHistoryNode) throws IOException {
+ final List<KnoxShellTableCall> callHistoryList = new LinkedList<KnoxShellTableCall>();
+ TreeNode callNode;
+ Map<Object, Class<?>> params;
+ String invokerClass, method;
+ Boolean builderMethod;
+ for (int i = 0; i < callHistoryNode.size(); i++) {
+ callNode = callHistoryNode.get(i);
+ invokerClass = trimJSONQuotes(callNode.get("invokerClass").toString());
+ method = trimJSONQuotes(callNode.get("method").toString());
+ builderMethod = Boolean.valueOf(trimJSONQuotes(callNode.get("builderMethod").toString()));
+ params = fetchParameterMap(callNode.get("params"));
+ callHistoryList.add(new KnoxShellTableCall(invokerClass, method, builderMethod, params));
+ }
+ return callHistoryList;
+ }
+
+ private Map<Object, Class<?>> fetchParameterMap(TreeNode paramsNode) throws IOException {
+ try {
+ final Map<Object, Class<?>> parameterMap = new HashMap<>();
+ final Iterator<String> paramsFieldNamesIterator = ((ObjectNode) paramsNode).fieldNames();
+ String parameterValueAsString;
+ Class<?> parameterType;
+ while (paramsFieldNamesIterator.hasNext()) {
+ parameterValueAsString = trimJSONQuotes(paramsFieldNamesIterator.next());
+ parameterType = Class.forName(trimJSONQuotes(paramsNode.get(parameterValueAsString).toString()));
+ parameterMap.put(cast(parameterValueAsString, parameterType), parameterType);
+ }
+ return parameterMap;
+ } catch (Exception e) {
+ throw new IOException("Error while fetching parameters " + paramsNode, e);
+ }
+ }
+
+ // This may be done in a different way or using a library; I did not find any (I
+ // did not do a deep search though)
+ private Object cast(String valueAsString, Class<?> type) throws ParseException {
+ if (String.class == type) {
+ return valueAsString;
+ } else if (Byte.class == type) {
+ return Byte.valueOf(valueAsString);
+ } else if (Short.class == type) {
+ return Short.valueOf(valueAsString);
+ } else if (Integer.class == type) {
+ return Integer.valueOf(valueAsString);
+ } else if (Long.class == type) {
+ return Long.valueOf(valueAsString);
+ } else if (Float.class == type) {
+ return Float.valueOf(valueAsString);
+ } else if (Double.class == type) {
+ return Double.valueOf(valueAsString);
+ } else if (Boolean.class == type) {
+ return Boolean.valueOf(valueAsString);
+ } else if (Date.class == type) {
+ return KnoxShellTableJSONSerializer.JSON_DATE_FORMAT.parse(valueAsString);
+ }
+
+ return type.cast(valueAsString); // may throw ClassCastException
+ }
+
+ private KnoxShellTable parseJsonWithData(final TreeNode jsonContent) {
final KnoxShellTable table = new KnoxShellTable();
if (jsonContent.get("title").size() != 0) {
table.title(trimJSONQuotes(jsonContent.get("title").toString()));
@@ -67,7 +155,9 @@
}
/*
- * When serializing an object as JSON all elements within the table receive a surrounding quote pair (e.g. the cell contains myValue -> the JSON serialized string will be "myValue")
+ * When serializing an object as JSON all elements within the table receive a
+ * surrounding quote pair (e.g. the cell contains myValue -> the JSON serialized
+ * string will be "myValue")
*/
private String trimJSONQuotes(String toBeTrimmed) {
return toBeTrimmed.replaceAll("\"", "");
diff --git a/gateway-shell/src/main/resources/META-INF/aop.xml b/gateway-shell/src/main/resources/META-INF/aop.xml
new file mode 100644
index 0000000..0070403
--- /dev/null
+++ b/gateway-shell/src/main/resources/META-INF/aop.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<aspectj>
+ <aspects>
+ <aspect name="org.apache.knox.gateway.shell.table.KnoxShellTableHistoryAspect" />
+ </aspects>
+
+ <weaver>
+ <include within="org.apache.knox.gateway.shell.table.*" />
+ </weaver>
+
+</aspectj>
\ No newline at end of file
diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java
new file mode 100644
index 0000000..ebbf17d
--- /dev/null
+++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class KnoxShellTableCallHistoryTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private static final List<KnoxShellTableCall> CALL_LIST = new LinkedList<>();
+
+ @BeforeClass
+ public static void init() {
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.KnoxShellTableBuilder", "csv", false, Collections.emptyMap()));
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.CSVKnoxShellTableBuilder", "withHeaders", false, Collections.emptyMap()));
+ final String csvPath = KnoxShellTableCallHistoryTest.class.getClassLoader().getResource("knoxShellTableLocationsWithZipLessThan14.csv").getPath();
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.CSVKnoxShellTableBuilder", "url", true, Collections.singletonMap("file://" + csvPath, String.class)));
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.KnoxShellTable", "filter", false, Collections.emptyMap()));
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.KnoxShellTableFilter", "name", false, Collections.singletonMap("ZIP", String.class)));
+ CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.KnoxShellTableFilter", "greaterThan", true, Collections.singletonMap("5", String.class)));
+ }
+
+ @Test
+ public void shouldReturnEmptyListInCaseThereWasNoCall() throws Exception {
+ long id = 0;
+ assertTrue(KnoxShellTableCallHistory.getInstance().getCallHistory(id).isEmpty());
+ }
+
+ @Test
+ public void shouldReturnCallHistoryInProperOrder() throws Exception {
+ final long id = 1;
+ recordCallHistory(id, 2);
+ final List<KnoxShellTableCall> callHistory = KnoxShellTableCallHistory.getInstance().getCallHistory(id);
+ assertFalse(callHistory.isEmpty());
+ assertEquals(CALL_LIST.get(0), callHistory.get(0));
+ assertEquals(CALL_LIST.get(1), callHistory.get(1));
+ }
+
+ @Test
+ public void shouldThrowIllegalArgumentExceptionIfReplayingToInappropriateStep() {
+ final long id = 2;
+ recordCallHistory(id, 3);
+
+ // step 2 - second call - does not produce KnoxShellTable (builderMethod=false)
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("It is not allowed to replay up to step 2 as this step does not produce an intance of KnoxShellTable");
+ KnoxShellTableCallHistory.getInstance().replay(id, 2);
+ }
+
+ @Test
+ public void shouldProduceKnoxShellTableIfReplayingToValidStep() {
+ final long id = 3;
+ recordCallHistory(id, 3);
+
+ final KnoxShellTable table = KnoxShellTableCallHistory.getInstance().replay(id, 3);
+ assertNotNull(table);
+ assertFalse(table.headers.isEmpty());
+ assertEquals(14, table.rows.size());
+ assertEquals(table.values(0).get(13), "14"); // selected the first column (ZIP) where the last element - index 13 - is 14
+ }
+
+ @Test
+ public void shouldNotRollBackIfNoBuilderMethodRecorded() throws Exception {
+ final long id = 4;
+ recordCallHistory(id, 1);
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("There is no valid step to be rollback to");
+ KnoxShellTableCallHistory.getInstance().rollback(id);
+ }
+
+ @Test
+ public void shouldNotRollBackIfOnlyOneBuilderMethodRecorded() throws Exception {
+ final long id = 5;
+ recordCallHistory(id, 3);
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("There is no valid step to be rollback to");
+ KnoxShellTableCallHistory.getInstance().rollback(id);
+ }
+
+ @Test
+ public void shouldRollbackToValidPreviousStep() throws Exception {
+ long id = 6;
+ recordCallHistory(id, 6);
+ // filtered table where ZIPs are greater than "5" (in CSV everything is String)
+ final KnoxShellTable table = KnoxShellTableCallHistory.getInstance().replay(id, 6);
+ assertNotNull(table);
+ assertEquals(4, table.rows.size()); // only 4 rows with ZIP of 6, 7, 8 and 9
+ KnoxShellTableCallHistory.getInstance().saveCalls(table.id, KnoxShellTableCallHistory.getInstance().getCallHistory(id)); // in PROD AspectJ does it for us while replaying
+
+ // rolling back to the original table before filtering
+ table.rollback();
+ assertNotNull(table);
+ assertEquals(14, table.rows.size());
+ assertEquals(table.values(0).get(13), "14"); // selected the first column (ZIP) where the last element - index 13 - is 14
+ }
+
+ private void recordCallHistory(long id, int steps) {
+ for (int i = 0; i < steps; i++) {
+ KnoxShellTableCallHistory.getInstance().saveCall(id, CALL_LIST.get(i));
+ }
+ }
+
+}
diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
index c6c63e0..efb618f 100644
--- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
+++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
@@ -22,8 +22,9 @@
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
-
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.io.File;
@@ -33,6 +34,7 @@
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
+import java.util.Collections;
import javax.swing.SortOrder;
@@ -185,6 +187,18 @@
}
@Test
+ public void testFromJSONUsingCallHistory() throws IOException {
+ final String jsonPath = KnoxShellTableCallHistoryTest.class.getClassLoader().getResource("knoxShellTableCallHistoryWithFiltering.json").getPath();
+ final String csvPath = "file://" + KnoxShellTableCallHistoryTest.class.getClassLoader().getResource("knoxShellTableLocationsWithZipLessThan14.csv").getPath();
+ String json = (FileUtils.readFileToString(new File(jsonPath), StandardCharsets.UTF_8));
+ json = json.replaceAll("CSV_FILE_PATH_PLACEHOLDER", csvPath);
+ // filtered table where ZIPs are greater than "5" (in CSV everything is String)
+ final KnoxShellTable table = KnoxShellTable.builder().json().fromJson(json);
+ assertNotNull(table);
+ assertEquals(4, table.rows.size()); // only 4 rows with ZIP of 6, 7, 8 and 9
+ }
+
+ @Test
public void testSort() throws IOException {
KnoxShellTable table = new KnoxShellTable();
@@ -344,4 +358,15 @@
}
verify(connection, statement, resultSet, metadata);
}
+
+ @Test
+ public void shouldReturnDifferentCallHistoryForDifferentTables() throws Exception {
+ final KnoxShellTable table1 = new KnoxShellTable();
+ table1.id(1);
+ KnoxShellTableCallHistory.getInstance().saveCall(1, new KnoxShellTableCall("class1", "method1", true, Collections.singletonMap("param1", String.class)));
+ final KnoxShellTable table2 = new KnoxShellTable();
+ table1.id(2);
+ KnoxShellTableCallHistory.getInstance().saveCall(2, new KnoxShellTableCall("class2", "method2", false, Collections.singletonMap("param2", String.class)));
+ assertNotEquals(table1.getCallHistoryList(), table2.getCallHistoryList());
+ }
}
diff --git a/gateway-shell/src/test/resources/knoxShellTableCallHistoryWithFiltering.json b/gateway-shell/src/test/resources/knoxShellTableCallHistoryWithFiltering.json
new file mode 100644
index 0000000..a146633
--- /dev/null
+++ b/gateway-shell/src/test/resources/knoxShellTableCallHistoryWithFiltering.json
@@ -0,0 +1,39 @@
+{
+ "callHistoryList" : [ {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.KnoxShellTableBuilder",
+ "method" : "csv",
+ "builderMethod" : false,
+ "params" : { }
+ }, {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.CSVKnoxShellTableBuilder",
+ "method" : "withHeaders",
+ "builderMethod" : false,
+ "params" : { }
+ }, {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.CSVKnoxShellTableBuilder",
+ "method" : "url",
+ "builderMethod" : true,
+ "params" : {
+ "CSV_FILE_PATH_PLACEHOLDER" : "java.lang.String"
+ }
+ }, {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.KnoxShellTable",
+ "method" : "filter",
+ "builderMethod" : false,
+ "params" : { }
+ }, {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.KnoxShellTableFilter",
+ "method" : "name",
+ "builderMethod" : false,
+ "params" : {
+ "ZIP" : "java.lang.String"
+ }
+ }, {
+ "invokerClass" : "org.apache.knox.gateway.shell.table.KnoxShellTableFilter",
+ "method" : "greaterThan",
+ "builderMethod" : true,
+ "params" : {
+ "5" : "java.lang.String"
+ }
+ } ]
+}
diff --git a/gateway-shell/src/test/resources/knoxShellTableLocationsWithZipLessThan14.csv b/gateway-shell/src/test/resources/knoxShellTableLocationsWithZipLessThan14.csv
new file mode 100644
index 0000000..3e4da70
--- /dev/null
+++ b/gateway-shell/src/test/resources/knoxShellTableLocationsWithZipLessThan14.csv
@@ -0,0 +1,15 @@
+ZIP,COUNTRY,STATE,CITY,POPULATION
+1,US,NY,City1,100000
+2,US,NY,City2,200000
+3,US,NY,City3,300000
+4,US,NY,City4,400000
+5,US,NY,City5,500000
+6,US,NY,City6,600000
+7,US,NY,City7,700000
+8,US,NY,City8,800000
+9,US,NY,City9,900000
+10,US,NY,City10,1000000
+11,US,NY,City11,2000000
+12,US,NY,City12,3000000
+13,US,NY,City13,4000000
+14,US,NY,City14,5000000
\ No newline at end of file
diff --git a/gateway-util-common/pom.xml b/gateway-util-common/pom.xml
index d9ba6ec..6eb6c0e 100644
--- a/gateway-util-common/pom.xml
+++ b/gateway-util-common/pom.xml
@@ -48,7 +48,12 @@
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
-
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
index af13728..4eabe2c 100644
--- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
@@ -18,6 +18,7 @@
package org.apache.knox.gateway.util;
import java.io.IOException;
+import java.text.DateFormat;
import java.util.HashMap;
import java.util.Map;
@@ -28,6 +29,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
public class JsonUtils {
private static final GatewayUtilCommonMessages LOG = MessagesFactory.get( GatewayUtilCommonMessages.class );
@@ -46,8 +48,19 @@
}
public static String renderAsJsonString(Object obj) {
+ return renderAsJsonString(obj, null, null);
+ }
+
+ public static String renderAsJsonString(Object obj, FilterProvider filterProvider, DateFormat dateFormat) {
String json = null;
ObjectMapper mapper = new ObjectMapper();
+ if (filterProvider != null) {
+ mapper.setFilterProvider(filterProvider);
+ }
+
+ if (dateFormat != null) {
+ mapper.setDateFormat(dateFormat);
+ }
try {
// write JSON to a file
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java
new file mode 100644
index 0000000..dcc5624
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java
@@ -0,0 +1,39 @@
+/*
+ * 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.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * See https://github.com/apache/commons-lang/pull/308 (at the time of this
+ * class being written the PR is not merged)
+ */
+@SuppressWarnings("serial")
+public class NoClassNameMultiLineToStringStyle extends ToStringStyle {
+
+ public NoClassNameMultiLineToStringStyle() {
+ super();
+ this.setUseClassName(false);
+ this.setUseIdentityHashCode(false);
+ this.setContentStart(StringUtils.EMPTY);
+ this.setFieldSeparator(System.lineSeparator());
+ this.setFieldSeparatorAtStart(false);
+ this.setContentEnd(System.lineSeparator());
+ }
+}
diff --git a/pom.xml b/pom.xml
index 5da25f2..dfed9ba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -150,6 +150,7 @@
<apache-rat-plugin.version>0.13</apache-rat-plugin.version>
<ant-nodeps.version>1.8.1</ant-nodeps.version>
<asm.version>7.2</asm.version>
+ <aspectj.version>1.9.4</aspectj.version>
<bcprov-jdk15on.version>1.63</bcprov-jdk15on.version>
<buildnumber-maven-plugin.version>1.4</buildnumber-maven-plugin.version>
<cglib.version>3.3.0</cglib.version>
@@ -1974,6 +1975,16 @@
<artifactId>spring-web</artifactId>
<version>${spring-core.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.aspectj</groupId>
+ <artifactId>aspectjrt</artifactId>
+ <version>${aspectj.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.aspectj</groupId>
+ <artifactId>aspectjweaver</artifactId>
+ <version>${aspectj.version}</version>
+ </dependency>
<dependency>
<groupId>de.thetaphi</groupId>