/******************************************************************************* * 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); } }