knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lmc...@apache.org
Subject [knox] branch master updated: 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)
Date Tue, 08 Oct 2019 23:37:42 GMT
This is an automated email from the ASF dual-hosted git repository.

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new b5c6486  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)
b5c6486 is described below

commit b5c6486db72030d269f75974bd282b3eec55549d
Author: Sandor Molnar <smolnar@apache.org>
AuthorDate: Wed Oct 9 01:37:37 2019 +0200

    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
---
 gateway-shell-release/home/bin/knoxshell.sh        |   2 +-
 gateway-shell-release/src/assembly.xml             |   7 +
 gateway-shell/pom.xml                              |  12 ++
 .../shell/table/CSVKnoxShellTableBuilder.java      |  18 +--
 .../shell/table/JDBCKnoxShellTableBuilder.java     |  41 +++---
 .../shell/table/JSONKnoxShellTableBuilder.java     |  12 +-
 .../shell/table/JoinKnoxShellTableBuilder.java     |  11 +-
 .../knox/gateway/shell/table/KnoxShellTable.java   |  65 ++++++++-
 .../gateway/shell/table/KnoxShellTableBuilder.java |  14 +-
 .../gateway/shell/table/KnoxShellTableCall.java    |  91 ++++++++++++
 .../shell/table/KnoxShellTableCallHistory.java     | 152 +++++++++++++++++++++
 .../gateway/shell/table/KnoxShellTableFilter.java  |  52 ++++---
 .../shell/table/KnoxShellTableHistoryAspect.java   |  85 ++++++++++++
 .../shell/table/KnoxShellTableJSONSerializer.java  |  63 +++++++++
 .../shell/table/KnoxShellTableRowDeserializer.java |  98 ++++++++++++-
 gateway-shell/src/main/resources/META-INF/aop.xml  |  27 ++++
 .../shell/table/KnoxShellTableCallHistoryTest.java | 134 ++++++++++++++++++
 .../gateway/shell/table/KnoxShellTableTest.java    |  27 +++-
 .../knoxShellTableCallHistoryWithFiltering.json    |  39 ++++++
 .../knoxShellTableLocationsWithZipLessThan14.csv   |  15 ++
 gateway-util-common/pom.xml                        |   7 +-
 .../org/apache/knox/gateway/util/JsonUtils.java    |  13 ++
 .../util/NoClassNameMultiLineToStringStyle.java    |  39 +++---
 pom.xml                                            |  11 ++
 24 files changed, 943 insertions(+), 92 deletions(-)

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 @@ function main {
          ;;
       *)
          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 @@ package org.apache.knox.gateway.shell.table;
 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 @@ public class CSVKnoxShellTableBuilder extends KnoxShellTableBuilder {
 
   private boolean withHeaders;
 
+  CSVKnoxShellTableBuilder(long id) {
+    super(id);
+  }
+
   public CSVKnoxShellTableBuilder withHeaders() {
     withHeaders = true;
     return this;
@@ -35,17 +40,16 @@ public class CSVKnoxShellTableBuilder extends KnoxShellTableBuilder {
 
   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 @@ public class CSVKnoxShellTableBuilder extends KnoxShellTableBuilder {
         }
         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 @@ public class JDBCKnoxShellTableBuilder extends KnoxShellTableBuilder {
   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 @@ public class JDBCKnoxShellTableBuilder extends KnoxShellTableBuilder {
     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 @@ public class JDBCKnoxShellTableBuilder extends KnoxShellTableBuilder {
     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 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 
 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 @@ public class JSONKnoxShellTableBuilder extends KnoxShellTableBuilder {
     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.ArrayList;
 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 @@ class JoinKnoxShellTableBuilder extends KnoxShellTableBuilder {
     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 @@ package org.apache.knox.gateway.shell.table;
 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 @@ import org.apache.knox.gateway.util.JsonUtils;
  * 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 @@ public class KnoxShellTable {
     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 class KnoxShellTable {
   }
 
   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 class KnoxShellTableBuilder {
   }
 
   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 @@ import java.util.regex.Pattern;
 
 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 @@ public class KnoxShellTableFilter {
       }
     }
     if (index == -1) {
-        throw new KnoxShellTableFilterException("Column name not found");
+      throw new KnoxShellTableFilterException("Column name not found");
     }
     return this;
   }
@@ -49,13 +53,12 @@ public class KnoxShellTableFilter {
     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 @@ public class KnoxShellTableFilter {
     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 @@ public class KnoxShellTableRowDeserializer extends StdDeserializer<KnoxShellTabl
   @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 @@ public class KnoxShellTableRowDeserializer extends StdDeserializer<KnoxShellTabl
   }
 
   /*
-   * 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.expect;
 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.Connection;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.Statement;
+import java.util.Collections;
 
 import javax.swing.SortOrder;
 
@@ -185,6 +187,18 @@ public class KnoxShellTableTest {
   }
 
   @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 @@ public class KnoxShellTableTest {
     }
     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.JsonFactory;
 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 class JsonUtils {
   }
 
   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-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java
similarity index 54%
copy from gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java
copy to gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java
index 5dcc5af..dcc5624 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableBuilder.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/NoClassNameMultiLineToStringStyle.java
@@ -15,30 +15,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.shell.table;
+package org.apache.knox.gateway.util;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringStyle;
 
-public class KnoxShellTableBuilder {
-  protected String title;
-
-  public KnoxShellTableBuilder title(String title) {
-    this.title = title;
-    return this;
-  }
-
-  public CSVKnoxShellTableBuilder csv() {
-    return new CSVKnoxShellTableBuilder();
-  }
-
-  public JSONKnoxShellTableBuilder json() {
-    return new JSONKnoxShellTableBuilder();
-  }
-
-  public JoinKnoxShellTableBuilder join() {
-    return new JoinKnoxShellTableBuilder();
-  }
+/**
+ * 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 JDBCKnoxShellTableBuilder jdbc() {
-    return new JDBCKnoxShellTableBuilder();
+  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>


Mime
View raw message