/*******************************************************************************
* Copyright 2012 University of Southern California
*
* Licensed 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.
*
* This code was developed by the Information Integration Group as part
* of the Karma project at the Information Sciences Institute of the
* University of Southern California. For more information, publications,
* and related projects, please see: http://www.isi.edu/integration
******************************************************************************/
/**
*
*/
package edu.isi.karma.controller.history;
import edu.isi.karma.controller.command.Command;
import edu.isi.karma.controller.command.CommandException;
import edu.isi.karma.controller.command.CommandType;
import edu.isi.karma.controller.command.ICommand;
import edu.isi.karma.controller.command.ICommand.CommandTag;
import edu.isi.karma.controller.history.HistoryJsonUtil.ClientJsonKeys;
import edu.isi.karma.controller.history.HistoryJsonUtil.ParameterType;
import edu.isi.karma.controller.update.HistoryUpdate;
import edu.isi.karma.controller.update.UpdateContainer;
import edu.isi.karma.rep.HNode;
import edu.isi.karma.rep.Workspace;
import edu.isi.karma.util.JSONUtil;
import edu.isi.karma.view.VWorkspace;
import org.apache.commons.lang3.tuple.Pair;
import org.json.JSONArray;
import org.json.JSONObject;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* @author szekely
*
*/
public class CommandHistory implements Cloneable{
private WorksheetCommandHistory worksheetCommandHistory = new WorksheetCommandHistory();
private Command previewCommand;
/**
* If the last command was undo, and then we do a command that goes on the
* history, then we need to send the browser the full history BEFORE we send
* the update for the command the user just did. The reason is that the
* history may contain undoable commands, and the browser does not know how
* to reset the history.
*/
/**
* Used to keep a pointer to the command which require user-interaction
* through multiple HTTP requests.
*/
private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
private static Map<String, IHistorySaver> historySavers = new HashMap<>();
private final static Set<CommandConsolidator> consolidators = new HashSet<>();
static {
Reflections reflections = new Reflections("edu.isi.karma");
Set<Class<? extends CommandConsolidator>> subTypes =
reflections.getSubTypesOf(CommandConsolidator.class);
for (Class<? extends CommandConsolidator> subType : subTypes)
{
if(!Modifier.isAbstract(subType.getModifiers()) && !subType.isInterface()) {
try {
consolidators.add(subType.newInstance());
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public enum HistoryArguments {
worksheetId, commandName, inputParameters, hNodeId, tags, model
}
public CommandHistory() {
}
public CommandHistory(WorksheetCommandHistory worksheetCommandHistory) {
this.worksheetCommandHistory = worksheetCommandHistory;
}
public List<ICommand> _getHistory() {
return worksheetCommandHistory.getAllCommands();
}
public CommandHistory clone() {
return new CommandHistory(worksheetCommandHistory.clone());
}
/**
* Commands go onto the history when they have all their arguments and are
* ready to be executed. If a command needs multiple user interactions to
* define the parameters, then the command object must be created,
* interacted with and then executed.
*
* @param command
* @param workspace
* @return UpdateContainer with all the changes done by the command.
* @throws CommandException
*/
public UpdateContainer doCommand(Command command, Workspace workspace)
throws CommandException {
return doCommand(command, workspace, true);
}
public UpdateContainer doCommand(Command command, Workspace workspace, boolean saveToHistory)
throws CommandException {
UpdateContainer effects = new UpdateContainer();
Pair<ICommand, Object> consolidatedCommand = null;
String consolidatorName = null;
String worksheetId = worksheetCommandHistory.getWorksheetId(command);
List<ICommand> potentialConsolidateCommands = worksheetCommandHistory.getCommandsFromWorksheetIdAndCommandTag(worksheetId, command.getTagFromPriority());
for (CommandConsolidator consolidator : consolidators) {
consolidatedCommand = consolidator.consolidateCommand(potentialConsolidateCommands, command, workspace);
if (consolidatedCommand != null) {
consolidatorName = consolidator.getConsolidatorName();
break;
}
}
if (consolidatedCommand != null) {
worksheetCommandHistory.setStale(worksheetId, true);
if (consolidatorName.equals("PyTransformConsolidator")) {
effects.append(consolidatedCommand.getLeft().doIt(workspace));
}
if (consolidatorName.equals("UnassignSemanticTypesConsolidator")) {
worksheetCommandHistory.removeCommandFromHistory(Arrays.asList(consolidatedCommand.getLeft()));
effects.append(command.doIt(workspace));
}
if (consolidatorName.equals("SemanticTypesConsolidator")) {
worksheetCommandHistory.replaceCommandFromHistory(consolidatedCommand.getKey(), (ICommand)consolidatedCommand.getRight());
effects.append(((ICommand) consolidatedCommand.getRight()).doIt(workspace));
}
if (consolidatorName.equals("OrganizeColumnsConsolidator")) {
effects.append(((ICommand) consolidatedCommand.getRight()).doIt(workspace));
worksheetCommandHistory.replaceCommandFromHistory(consolidatedCommand.getKey(), (ICommand)consolidatedCommand.getRight());
}
if (consolidatorName.equals("DeleteNodeConsolidator")) {
worksheetCommandHistory.removeCommandFromHistory(Arrays.asList(consolidatedCommand.getLeft()));
effects.append(command.doIt(workspace));
}
if (consolidatorName.equals("AddLiteralNodeConsolidator")) {
worksheetCommandHistory.replaceCommandFromHistory(consolidatedCommand.getKey(), (ICommand)consolidatedCommand.getRight());
effects.append(((ICommand) consolidatedCommand.getRight()).doIt(workspace));
}
if (consolidatorName.equals("DeleteLinkConsolidator")) {
worksheetCommandHistory.removeCommandFromHistory(Arrays.asList(consolidatedCommand.getLeft()));
effects.append(command.doIt(workspace));
}
if (consolidatorName.equals("AddLinkConsolidator")) {
worksheetCommandHistory.replaceCommandFromHistory(consolidatedCommand.getKey(), (ICommand)consolidatedCommand.getRight());
effects.append(((ICommand) consolidatedCommand.getRight()).doIt(workspace));
}
}
else {
effects.append(command.doIt(workspace));
}
command.setExecuted(true);
if (command.getCommandType() != CommandType.notInHistory) {
worksheetId = worksheetCommandHistory.getWorksheetId(command);
worksheetCommandHistory.clearRedoCommand(worksheetId);
worksheetCommandHistory.setCurrentCommand(command, consolidatedCommand);
if (consolidatedCommand == null) {
worksheetCommandHistory.insertCommandToHistory(command);
}
effects.add(new HistoryUpdate(this));
}
if(saveToHistory) {
// Save the modeling commands
if (!(instanceOf(command, "ResetKarmaCommand"))) {
try {
if(isHistoryWriteEnabled && command.isSavedInHistory() &&
(command.hasTag(CommandTag.Modeling)
|| command.hasTag(CommandTag.Transformation)
|| command.hasTag(CommandTag.Selection)
|| command.hasTag(CommandTag.SemanticType)
) && historySavers.get(workspace.getId()) != null) {
writeHistoryPerWorksheet(workspace, historySavers.get(workspace.getId()));
}
} catch (Exception e) {
logger.error("Error occured while writing history!" , e);
logger.error("Error with this command: {}, Input params: {}", command.getCommandName(), command.getInputParameterJson());
}
}
}
return effects;
}
private void writeHistoryPerWorksheet(Workspace workspace, IHistorySaver historySaver) throws Exception {
String workspaceId = workspace.getId();
Map<String, JSONArray> comMap = new HashMap<>();
for(ICommand command : _getHistory()) {
if(command.isSavedInHistory() &&
(command.hasTag(CommandTag.Modeling)
|| command.hasTag(CommandTag.Transformation)
|| command.hasTag(CommandTag.Selection)
|| command.hasTag(CommandTag.SemanticType)
)) {
JSONArray json = new JSONArray(command.getInputParameterJson());
String worksheetId = HistoryJsonUtil.getStringValue(HistoryArguments.worksheetId.name(), json);
if(workspace.getWorksheet(worksheetId) != null)
{
try {
if(comMap.get(worksheetId) == null)
comMap.put(worksheetId, new JSONArray());
comMap.get(worksheetId).put(getCommandJSON(workspace, command));
} catch(Exception e) {
logger.error("Error serializing command {} to history, Input:{}", command.getCommandName(), command.getInputParameterJson());
}
}
}
}
for(Map.Entry<String, JSONArray> stringJSONArrayEntry : comMap.entrySet()) {
JSONArray comms = stringJSONArrayEntry.getValue();
historySaver.saveHistory(workspaceId, stringJSONArrayEntry.getKey(), comms);
}
}
public JSONObject getCommandJSON(Workspace workspace, ICommand comm) {
JSONObject commObj = new JSONObject();
commObj.put(HistoryArguments.commandName.name(), comm.getCommandName());
commObj.put(HistoryArguments.model.name(), comm.getModel());
// Populate the tags
JSONArray tagsArr = new JSONArray();
for (CommandTag tag : comm.getTags())
tagsArr.put(tag.name());
commObj.put(HistoryArguments.tags.name(), tagsArr);
JSONArray inputArr = new JSONArray(comm.getInputParameterJson() == null ? "[]" : comm.getInputParameterJson());
for (int i = 0; i < inputArr.length(); i++) {
JSONObject inpP = inputArr.getJSONObject(i);
/*** Check the input parameter type and accordingly make changes ***/
if(HistoryJsonUtil.getParameterType(inpP) == ParameterType.hNodeIdList) {
JSONArray hnodes = (JSONArray) JSONUtil.createJson(inpP.getString(ClientJsonKeys.value.name()));
for (int j = 0; j < hnodes.length(); j++) {
JSONObject obj = (JSONObject)hnodes.get(j);
Object value = obj.get(ClientJsonKeys.value.name());
if (value instanceof String) {
String hNodeId = (String) value;
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
obj.put(ClientJsonKeys.value.name(), hNodeRepresentation);
}
}
inpP.put(ClientJsonKeys.value.name(), hnodes);
} else if(HistoryJsonUtil.getParameterType(inpP) == ParameterType.orderedColumns) {
Object tmp = inpP.get(ClientJsonKeys.value.name());
JSONArray hnodes = (JSONArray) JSONUtil.createJson(tmp.toString());
for (int j = 0; j < hnodes.length(); j++) {
JSONObject obj = (JSONObject)hnodes.get(j);
String hNodeId = obj.getString(ClientJsonKeys.id.name());
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
obj.put(ClientJsonKeys.id.name(), hNodeRepresentation);
if (obj.has(ClientJsonKeys.children.name()))
obj.put(ClientJsonKeys.children.name(), parseChildren(obj.get(ClientJsonKeys.children.name()).toString(), workspace));
}
inpP.put(ClientJsonKeys.value.name(), hnodes);
} else if(HistoryJsonUtil.getParameterType(inpP) == ParameterType.hNodeId) {
String hNodeId = inpP.getString(ClientJsonKeys.value.name());
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
inpP.put(ClientJsonKeys.value.name(), hNodeRepresentation);
} else if (HistoryJsonUtil.getParameterType(inpP) == ParameterType.worksheetId) {
inpP.put(ClientJsonKeys.value.name(), "W");
} else if(HistoryJsonUtil.getParameterType(inpP) == ParameterType.linkWithHNodeId) {
String link = inpP.getString(ClientJsonKeys.value.name());
String[] linkParts = link.split("---");
String subject = linkParts[0];
String predicate = linkParts[1];
String object = linkParts[2];
JSONObject linkObj = new JSONObject();
HNode subjectNode = workspace.getFactory().getHNode(subject);
if(subjectNode != null) {
JSONArray hNodeRepresentation = subjectNode.getJSONArrayRepresentation(workspace.getFactory());
linkObj.put("subject", hNodeRepresentation);
} else {
linkObj.put("subject", subject);
}
linkObj.put("predicate", predicate);
HNode objectNode = workspace.getFactory().getHNode(object);
if(objectNode != null) {
JSONArray hNodeRepresentation = objectNode.getJSONArrayRepresentation(workspace.getFactory());
linkObj.put("object", hNodeRepresentation);
} else {
linkObj.put("object", object);
}
inpP.put(ClientJsonKeys.value.name(), linkObj);
} else {
// do nothing
}
}
if (comm instanceof Command) {
Command tmp = (Command)comm;
JSONArray inputArray = new JSONArray();
for (String hNodeId : tmp.getInputColumns()) {
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
JSONObject obj1 = new JSONObject();
obj1.put(ClientJsonKeys.value.name(), hNodeRepresentation);
inputArray.put(obj1);
}
JSONObject obj = HistoryJsonUtil.getJSONObjectWithName("inputColumns", inputArr);
if (obj == null) {
obj = new JSONObject();
obj.put(ClientJsonKeys.name.name(), "inputColumns");
obj.put(ClientJsonKeys.value.name(), inputArray.toString());
obj.put(ClientJsonKeys.type.name(), ParameterType.hNodeIdList.name());
inputArr.put(obj);
}
else {
obj.put(ClientJsonKeys.value.name(), inputArray.toString());
}
JSONArray outputArray = new JSONArray();
for (String hNodeId : tmp.getOutputColumns()) {
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
JSONObject obj1 = new JSONObject();
obj1.put(ClientJsonKeys.value.name(), hNodeRepresentation);
outputArray.put(obj1);
}
obj = HistoryJsonUtil.getJSONObjectWithName("outputColumns", inputArr);
if (obj == null) {
obj = new JSONObject();
obj.put(ClientJsonKeys.name.name(), "outputColumns");
obj.put(ClientJsonKeys.value.name(), outputArray.toString());
obj.put(ClientJsonKeys.type.name(), ParameterType.hNodeIdList.name());
inputArr.put(obj);
}
else {
obj.put(ClientJsonKeys.value.name(), outputArray.toString());
}
}
commObj.put(HistoryArguments.inputParameters.name(), inputArr);
return commObj;
}
private boolean instanceOf(Object o, String className) { // TODO this is a hack, but instanceof doesn't really seem appropriate here
return o.getClass().getName().toLowerCase().contains(className.toLowerCase());
}
private String parseChildren(String inputJSON, Workspace workspace) {
JSONArray array = (JSONArray) JSONUtil.createJson(inputJSON);
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
String hNodeId = obj.getString(ClientJsonKeys.id.name());
HNode node = workspace.getFactory().getHNode(hNodeId);
JSONArray hNodeRepresentation = node.getJSONArrayRepresentation(workspace.getFactory());
obj.put(ClientJsonKeys.id.name(), hNodeRepresentation);
if (obj.has(ClientJsonKeys.children.name()))
obj.put(ClientJsonKeys.children.name(), parseChildren(obj.get(ClientJsonKeys.children.name()).toString(), workspace));
}
return array.toString();
}
/**
* @param workspace
* is the id of a command that should be either in the undo or
* redo histories. If it is in none, then nothing will be done.
* @return the effects of the undone or redone commands.
* @throws CommandException
*/
public UpdateContainer undoOrRedoCommand(Workspace workspace, String worksheetId) throws CommandException {
RedoCommandObject currentCommand = worksheetCommandHistory.getCurrentRedoCommandObject(worksheetId);
RedoCommandObject lastCommand = worksheetCommandHistory.getLastRedoCommandObject(worksheetId);
UpdateContainer container = new UpdateContainer();
if (lastCommand == null) {
worksheetCommandHistory.setLastRedoCommandObject(currentCommand);
Pair<ICommand, Object> pair = currentCommand.getConsolidatedCommand();
if (pair == null) {
container.append(currentCommand.getCommand().undoIt(workspace));
worksheetCommandHistory.removeCommandFromHistory(Arrays.asList(currentCommand.getCommand()));
} else {
if (pair.getLeft().getCommandName().equals("SubmitPythonTransformationCommand")) {
pair.getLeft().setInputParameterJson(pair.getRight().toString());
try {
Method method = pair.getLeft().getClass().getMethod("setTransformationCode", String.class);
method.invoke(pair.getLeft(), HistoryJsonUtil.getStringValue("transformationCode", (JSONArray)pair.getRight()));
container.append(pair.getLeft().doIt(workspace));
} catch (Exception e) {
logger.warn("Method invocation failure", e);
}
}
else if (pair.getLeft().getCommandName().equals("SetSemanticTypeCommand") || pair.getLeft().getCommandName().equals("SetMetaPropertyCommand")) {
container.append(pair.getLeft().doIt(workspace));
worksheetCommandHistory.insertCommandToHistory(pair.getLeft());
}
}
}
else {
container.append(doCommand((Command) lastCommand.getCommand(), workspace));
}
container.add(new HistoryUpdate(this));
return container;
}
public void generateFullHistoryJson(String prefix, PrintWriter pw,
VWorkspace vWorkspace) {
boolean isFirst = true;
for (String worksheetId : worksheetCommandHistory.getAllWorksheetId()) {
Iterator<ICommand> histIt = worksheetCommandHistory.getCommandsFromWorksheetId(worksheetId).iterator();
RedoCommandObject currentCommand = worksheetCommandHistory.getCurrentRedoCommandObject(worksheetId);
RedoCommandObject redoCommandObject = worksheetCommandHistory.getLastRedoCommandObject(worksheetId);
while (histIt.hasNext()) {
ICommand command = histIt.next();
if (isFirst) {
isFirst = false;
}
else {
pw.println(prefix + ",");
}
if (currentCommand != null && command == currentCommand.getCommand()) {
command.generateJson(prefix, pw, vWorkspace,
Command.HistoryType.lastRun);
}
else if (currentCommand != null && currentCommand.getConsolidatedCommand() != null && currentCommand.getConsolidatedCommand().getKey() == command) {
command.generateJson(prefix, pw, vWorkspace,
Command.HistoryType.lastRun);
}
else {
command.generateJson(prefix, pw, vWorkspace,
Command.HistoryType.normal);
}
}
if (redoCommandObject != null) {
if (isFirst) {
isFirst = false;
}
else {
pw.println(prefix + ",");
}
redoCommandObject.getCommand().generateJson(prefix, pw, vWorkspace,
Command.HistoryType.redo);
}
}
}
public void removeCommands(String worksheetId) {
List<ICommand> commandsFromWorksheet = worksheetCommandHistory.getCommandsFromWorksheetId(worksheetId);
this.worksheetCommandHistory.removeCommandFromHistory(commandsFromWorksheet);
}
public List<Command> getCommandsFromWorksheetId(String worksheetId) {
List<Command> commandsFromWorksheet = new ArrayList<>();
List<ICommand> history = worksheetCommandHistory.getCommandsFromWorksheetId(worksheetId);
for(ICommand command : history) {
if(command instanceof Command && command.isSavedInHistory() &&
(command.hasTag(CommandTag.Modeling)
|| command.hasTag(CommandTag.Transformation)
|| command.hasTag(CommandTag.Selection)
|| command.hasTag(CommandTag.SemanticType)
)) {
commandsFromWorksheet.add((Command) command);
}
}
return commandsFromWorksheet;
}
public void addPreviewCommand(Command c) {
previewCommand = c;
}
public boolean isStale(String worksheetId) {
return worksheetCommandHistory.isStale(worksheetId);
}
public void setStale(String worksheetId, boolean stale) {
worksheetCommandHistory.setStale(worksheetId, stale);
}
public void clearCurrentCommand(String worksheetId) {
worksheetCommandHistory.clearCurrentCommand(worksheetId);
}
public void clearRedoCommand(String worksheetId) {
worksheetCommandHistory.clearRedoCommand(worksheetId);
}
public void removeWorksheetHistory(String worksheetId) {
worksheetCommandHistory.removeWorksheet(worksheetId);
}
public Command getPreviewCommand(String commandId) {
if (previewCommand.getId().equals(commandId))
return previewCommand;
return null;
}
private static boolean isHistoryWriteEnabled = false;
public static void setIsHistoryEnabled(boolean isHistoryWriteEnabled)
{
CommandHistory.isHistoryWriteEnabled = isHistoryWriteEnabled;
}
public static void setHistorySaver(String workspaceId, IHistorySaver saver) {
historySavers.put(workspaceId, saver);
}
public static IHistorySaver getHistorySaver(String workspaceId) {
return historySavers.get(workspaceId);
}
}