blob: da2249523c5d249fa57a2b332f5a274b32e61d1d [file] [log] [blame]
/*
* 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);
}
}
}