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 <smol...@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>