/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* 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.
*/
package org.jkiss.dbeaver.model.impl.edit;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.edit.*;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.exec.DBCExecutionPurpose;
import org.jkiss.dbeaver.model.exec.DBCSession;
import org.jkiss.dbeaver.model.exec.DBCTransactionManager;
import org.jkiss.dbeaver.model.messages.ModelMessages;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.util.*;
/**
* DBECommandContextImpl
*/
public abstract class AbstractCommandContext implements DBECommandContext {
private static final Log log = Log.getLog(AbstractCommandContext.class);
private final DBCExecutionContext executionContext;
private final List<CommandInfo> commands = new ArrayList<>();
private final List<CommandInfo> undidCommands = new ArrayList<>();
private List<CommandQueue> commandQueues;
private final Map<Object, Object> userParams = new HashMap<>();
private final List<DBECommandListener> listeners = new ArrayList<>();
private final boolean atomic;
/**
* Creates new context
* @param executionContext Execution context
* @param atomic atomic context reflect commands in UI only after all commands were executed. Non-atomic
* reflects each command at the moment it executed
*/
public AbstractCommandContext(DBCExecutionContext executionContext, boolean atomic)
{
this.executionContext = executionContext;
this.atomic = atomic;
}
@Override
public DBCExecutionContext getExecutionContext()
{
return executionContext;
}
@Override
public boolean isDirty()
{
synchronized (commands) {
return !getCommandQueues().isEmpty();
}
}
@Override
public void saveChanges(DBRProgressMonitor monitor) throws DBException {
if (!executionContext.isConnected()) {
throw new DBException("Context [" + executionContext.getContextName() + "] isn't connected to the database");
}
List<CommandQueue> commandQueues = getCommandQueues();
// Validate commands
for (CommandQueue queue : commandQueues) {
for (CommandInfo cmd : queue.commands) {
cmd.command.validateCommand();
}
}
// Execute commands
List<CommandInfo> executedCommands = new ArrayList<>();
try {
for (CommandQueue queue : commandQueues) {
// Make list of not-executed commands
for (int i = 0; i < queue.commands.size(); i++) {
if (monitor.isCanceled()) {
break;
}
CommandInfo cmd = queue.commands.get(i);
while (cmd.mergedBy != null) {
cmd = cmd.mergedBy;
}
if (!cmd.executed) {
// Persist changes
//if (CommonUtils.isEmpty(cmd.persistActions)) {
DBEPersistAction[] persistActions = cmd.command.getPersistActions();
if (!ArrayUtils.isEmpty(persistActions)) {
cmd.persistActions = new ArrayList<>(persistActions.length);
for (DBEPersistAction action : persistActions) {
cmd.persistActions.add(new PersistInfo(action));
}
}
//}
if (!CommonUtils.isEmpty(cmd.persistActions)) {
try (DBCSession session = openCommandPersistContext(monitor, cmd.command)) {
DBException error = null;
for (PersistInfo persistInfo : cmd.persistActions) {
DBEPersistAction.ActionType actionType = persistInfo.action.getType();
if (persistInfo.executed && actionType == DBEPersistAction.ActionType.NORMAL) {
continue;
}
if (monitor.isCanceled()) {
break;
}
try {
if (error == null || actionType == DBEPersistAction.ActionType.FINALIZER) {
queue.objectManager.executePersistAction(session, cmd.command, persistInfo.action);
}
persistInfo.executed = true;
} catch (DBException e) {
persistInfo.error = e;
persistInfo.executed = false;
if (actionType != DBEPersistAction.ActionType.OPTIONAL) {
error = e;
}
}
}
if (error != null) {
throw error;
}
// Commit metadata changes
DBCTransactionManager txnManager = DBUtils.getTransactionManager(session.getExecutionContext());
if (txnManager != null && !txnManager.isAutoCommit()) {
txnManager.commit(session);
}
}
cmd.executed = true;
}
}
if (cmd.executed) {
// Remove only executed commands
// Commands which do not perform any persist actions
// should remain - they constructs queue by merging with other commands
synchronized (commands) {
// Remove original command from stack
//final CommandInfo thisCommand = queue.commands.get(i);
commands.remove(cmd);
}
}
if (!executedCommands.contains(cmd)) {
executedCommands.add(cmd);
}
}
}
// Let's clear commands
// If everything went well then there should be nothing to do else.
// But some commands may still remain in queue if they merged each other
// (e.g. create + delete of the same entity produce 2 commands and zero actions).
// There were no exceptions during save so we assume that everything went well
commands.clear();
userParams.clear();
/*
// Refresh object states
for (CommandQueue queue : commandQueues) {
if (queue.getObject() instanceof DBPStatefulObject) {
try {
((DBPStatefulObject) queue.getObject()).refreshObjectState(monitor);
} catch (DBCException e) {
// Just report an error
log.error(e);
}
}
}
*/
}
finally {
try {
// Update UI
if (atomic) {
for (CommandInfo cmd : executedCommands) {
if (cmd.reflector != null) {
cmd.reflector.redoCommand(cmd.command);
}
}
}
// Update model
for (CommandInfo cmd : executedCommands) {
cmd.command.updateModel();
}
} catch (Exception e) {
log.warn("Error updating model", e);
}
clearCommandQueues();
clearUndidCommands();
// Notify listeners
for (DBECommandListener listener : getListeners()) {
listener.onSave();
}
}
}
@Override
public void resetChanges()
{
synchronized (commands) {
try {
while (!commands.isEmpty()) {
undoCommand();
}
clearUndidCommands();
clearCommandQueues();
commands.clear();
userParams.clear();
} finally {
for (DBECommandListener listener : getListeners()) {
listener.onReset();
}
}
}
}
@Override
public Collection<? extends DBECommand<?>> getFinalCommands()
{
synchronized (commands) {
List<DBECommand<?>> cmdCopy = new ArrayList<>(commands.size());
for (CommandQueue queue : getCommandQueues()) {
for (CommandInfo cmdInfo : queue.commands) {
while (cmdInfo.mergedBy != null) {
cmdInfo = cmdInfo.mergedBy;
}
if (!cmdCopy.contains(cmdInfo.command)) {
cmdCopy.add(cmdInfo.command);
}
}
}
return cmdCopy;
}
}
@Override
public Collection<? extends DBECommand<?>> getUndoCommands()
{
synchronized (commands) {
List<DBECommand<?>> result = new ArrayList<>();
for (int i = commands.size() - 1; i >= 0; i--) {
CommandInfo cmd = commands.get(i);
while (cmd.prevInBatch != null) {
cmd = cmd.prevInBatch;
i--;
}
if (!cmd.command.isUndoable()) {
break;
}
result.add(cmd.command);
}
return result;
}
}
@Override
public Collection<DBPObject> getEditedObjects()
{
final List<CommandQueue> queues = getCommandQueues();
List<DBPObject> result = new ArrayList<>(queues.size());
for (CommandQueue queue : queues) {
result.add(queue.getObject());
}
return result;
}
@Override
public void addCommand(
DBECommand command,
DBECommandReflector reflector)
{
addCommand(command, reflector, false);
}
@Override
public void addCommand(DBECommand command, DBECommandReflector reflector, boolean execute)
{
synchronized (commands) {
commands.add(new CommandInfo(command, reflector));
clearUndidCommands();
clearCommandQueues();
}
fireCommandChange(command);
if (execute && reflector != null && !atomic) {
reflector.redoCommand(command);
}
refreshCommandState();
}
/*
public void addCommandBatch(List<DBECommand> commandBatch, DBECommandReflector reflector, boolean execute)
{
if (commandBatch.isEmpty()) {
return;
}
synchronized (commands) {
CommandInfo prevInfo = null;
for (int i = 0, commandBatchSize = commandBatch.size(); i < commandBatchSize; i++) {
DBECommand command = commandBatch.get(i);
final CommandInfo info = new CommandInfo(command, i == 0 ? reflector : null);
info.prevInBatch = prevInfo;
commands.add(info);
prevInfo = info;
}
clearUndidCommands();
clearCommandQueues();
}
// Fire only single event
fireCommandChange(commandBatch.get(0));
if (execute && reflector != null) {
reflector.redoCommand(commandBatch.get(0));
}
refreshCommandState();
}
*/
@Override
public void removeCommand(DBECommand<?> command)
{
synchronized (commands) {
for (CommandInfo cmd : commands) {
if (cmd.command == command) {
commands.remove(cmd);
break;
}
}
clearUndidCommands();
clearCommandQueues();
}
fireCommandChange(command);
}
@Override
public void updateCommand(DBECommand<?> command, DBECommandReflector commandReflector)
{
synchronized (commands) {
boolean found = false;
for (CommandInfo cmd : commands) {
if (cmd.command == command) {
found = true;
break;
}
}
if (!found) {
// Actually it is a new command
addCommand(command, commandReflector);
} else {
clearUndidCommands();
clearCommandQueues();
}
}
fireCommandChange(command);
}
@Override
public void addCommandListener(DBECommandListener listener)
{
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removeCommandListener(DBECommandListener listener)
{
synchronized (listeners) {
listeners.remove(listener);
}
}
@Override
public Map<Object, Object> getUserParams()
{
return userParams;
}
private void fireCommandChange(DBECommand<?> command)
{
for (DBECommandListener listener : getListeners()) {
listener.onCommandChange(command);
}
}
DBECommandListener[] getListeners()
{
synchronized (listeners) {
return listeners.toArray(new DBECommandListener[listeners.size()]);
}
}
@Override
public DBECommand getUndoCommand()
{
synchronized (commands) {
if (!commands.isEmpty()) {
CommandInfo cmd = commands.get(commands.size() - 1);
while (cmd.prevInBatch != null) {
cmd = cmd.prevInBatch;
}
if (cmd.command.isUndoable()) {
return cmd.command;
}
}
return null;
}
}
@Override
public DBECommand getRedoCommand()
{
synchronized (commands) {
if (!undidCommands.isEmpty()) {
CommandInfo cmd = undidCommands.get(undidCommands.size() - 1);
while (cmd.prevInBatch != null) {
cmd = cmd.prevInBatch;
}
return cmd.command;
}
return null;
}
}
@Override
public void undoCommand()
{
if (getUndoCommand() == null) {
throw new IllegalStateException("Can't undo command");
}
List<CommandInfo> processedCommands = new ArrayList<>();
synchronized (commands) {
CommandInfo lastCommand = commands.get(commands.size() - 1);
if (!lastCommand.command.isUndoable()) {
throw new IllegalStateException("Last executed command is not undoable");
}
// Undo command batch
while (lastCommand != null) {
commands.remove(lastCommand);
undidCommands.add(lastCommand);
processedCommands.add(lastCommand);
lastCommand = lastCommand.prevInBatch;
}
clearCommandQueues();
getCommandQueues();
}
refreshCommandState();
// Undo UI changes (always because undo doesn't make sense in atomic contexts)
for (CommandInfo cmd : processedCommands) {
if (cmd.reflector != null && !atomic) {
cmd.reflector.undoCommand(cmd.command);
}
}
}
@Override
public void redoCommand()
{
if (getRedoCommand() == null) {
throw new IllegalStateException("Can't redo command");
}
List<CommandInfo> processedCommands = new ArrayList<>();
synchronized (commands) {
// Just redo UI changes and put command on the top of stack
CommandInfo commandInfo = null;
// Redo batch
while (!undidCommands.isEmpty() &&
(commandInfo == null || undidCommands.get(undidCommands.size() - 1).prevInBatch == commandInfo))
{
commandInfo = undidCommands.remove(undidCommands.size() - 1);
commands.add(commandInfo);
processedCommands.add(commandInfo);
}
clearCommandQueues();
getCommandQueues();
}
// Redo UI changes (always because redo doesn't make sense in atomic contexts)
for (CommandInfo cmd : processedCommands) {
if (cmd.reflector != null) {
cmd.reflector.redoCommand(cmd.command);
}
}
refreshCommandState();
}
private void clearUndidCommands()
{
undidCommands.clear();
}
private List<CommandQueue> getCommandQueues()
{
if (commandQueues != null) {
return commandQueues;
}
commandQueues = new ArrayList<>();
CommandInfo aggregator = null;
// Create queues from commands
for (CommandInfo commandInfo : commands) {
if (commandInfo.command instanceof DBECommandAggregator) {
aggregator = commandInfo;
}
DBPObject object = commandInfo.command.getObject();
CommandQueue queue = null;
if (!commandQueues.isEmpty()) {
for (CommandQueue tmpQueue : commandQueues) {
if (tmpQueue.getObject() == object) {
queue = tmpQueue;
break;
}
}
}
if (queue == null) {
DBEObjectManager<?> objectManager = executionContext.getDataSource().getContainer().getPlatform().getEditorsRegistry().getObjectManager(object.getClass());
if (objectManager == null) {
throw new IllegalStateException("Can't find object manager for '" + object.getClass().getName() + "'");
}
queue = new CommandQueue(objectManager, null, object);
commandQueues.add(queue);
}
queue.addCommand(commandInfo);
}
// Merge commands
for (CommandQueue queue : commandQueues) {
final Map<DBECommand, CommandInfo> mergedByMap = new IdentityHashMap<>();
final List<CommandInfo> mergedCommands = new ArrayList<>();
for (int i = 0; i < queue.commands.size(); i++) {
CommandInfo lastCommand = queue.commands.get(i);
lastCommand.mergedBy = null;
CommandInfo firstCommand = null;
DBECommand<?> result = lastCommand.command;
if (mergedCommands.isEmpty()) {
result = lastCommand.command.merge(null, userParams);
} else {
boolean skipCommand = false;
for (int k = mergedCommands.size(); k > 0; k--) {
firstCommand = mergedCommands.get(k - 1);
result = lastCommand.command.merge(firstCommand.command, userParams);
if (result == null) {
// Remove first and skip last command
mergedCommands.remove(firstCommand);
skipCommand = true;
} else if (result != lastCommand.command) {
break;
}
}
if (skipCommand) {
continue;
}
}
mergedCommands.add(lastCommand);
if (result == lastCommand.command) {
// No changes
//firstCommand.mergedBy = lastCommand;
} else if (firstCommand != null && result == firstCommand.command) {
// Remove last command from queue
lastCommand.mergedBy = firstCommand;
} else {
// Some other command
// May be it is some earlier command from queue or some new command (e.g. composite)
CommandInfo mergedBy = mergedByMap.get(result);
if (mergedBy == null) {
// Try to find in command stack
for (int k = i; k >= 0; k--) {
if (queue.commands.get(k).command == result) {
mergedBy = queue.commands.get(k);
break;
}
}
if (mergedBy == null) {
// Create new command info
mergedBy = new CommandInfo(result, null);
}
mergedByMap.put(result, mergedBy);
}
lastCommand.mergedBy = mergedBy;
if (!mergedCommands.contains(mergedBy)) {
mergedCommands.add(mergedBy);
}
}
}
queue.commands = mergedCommands;
}
// Filter commands
for (CommandQueue queue : commandQueues) {
if (queue.objectManager instanceof DBECommandFilter) {
((DBECommandFilter) queue.objectManager).filterCommands(queue);
}
}
// Aggregate commands
if (aggregator != null) {
((DBECommandAggregator)aggregator.command).resetAggregatedCommands();
for (CommandQueue queue : commandQueues) {
for (CommandInfo cmd : queue.commands) {
if (cmd.command != aggregator.command && cmd.mergedBy == null && ((DBECommandAggregator)aggregator.command).aggregateCommand(cmd.command)) {
cmd.mergedBy = aggregator;
}
}
}
}
return commandQueues;
}
private void clearCommandQueues()
{
commandQueues = null;
}
protected DBCSession openCommandPersistContext(
DBRProgressMonitor monitor,
DBECommand<?> command)
throws DBException
{
return executionContext.openSession(
monitor,
DBCExecutionPurpose.META_DDL,
ModelMessages.model_edit_execute_ + command.getTitle());
}
protected void refreshCommandState()
{
}
private static class PersistInfo {
final DBEPersistAction action;
boolean executed = false;
Throwable error;
public PersistInfo(DBEPersistAction action)
{
this.action = action;
}
}
public static class CommandInfo {
final DBECommand<?> command;
final DBECommandReflector<?, DBECommand<?>> reflector;
List<PersistInfo> persistActions;
CommandInfo mergedBy = null;
CommandInfo prevInBatch = null;
boolean executed = false;
CommandInfo(DBECommand<?> command, DBECommandReflector<?, DBECommand<?>> reflector)
{
this.command = command;
this.reflector = reflector;
}
}
private static class CommandQueue extends AbstractCollection<DBECommand<DBPObject>> implements DBECommandQueue<DBPObject> {
private final CommandQueue parent;
private List<DBECommandQueue> subQueues;
private final DBPObject object;
private final DBEObjectManager objectManager;
private List<CommandInfo> commands = new ArrayList<>();
private CommandQueue(DBEObjectManager objectManager, CommandQueue parent, DBPObject object)
{
this.parent = parent;
this.object = object;
this.objectManager = objectManager;
if (parent != null) {
parent.addSubQueue(this);
}
}
void addSubQueue(CommandQueue queue)
{
if (subQueues == null) {
subQueues = new ArrayList<>();
}
subQueues.add(queue);
}
void addCommand(CommandInfo info)
{
commands.add(info);
}
@Override
public DBPObject getObject()
{
return object;
}
@Override
public DBECommandQueue getParentQueue()
{
return parent;
}
@Override
public Collection<DBECommandQueue> getSubQueues()
{
return subQueues;
}
@Override
public boolean add(DBECommand dbeCommand)
{
return commands.add(new CommandInfo(dbeCommand, null));
}
@Override
public Iterator<DBECommand<DBPObject>> iterator()
{
return new Iterator<DBECommand<DBPObject>>() {
private int index = -1;
@Override
public boolean hasNext()
{
return index < commands.size() - 1;
}
@Override
public DBECommand<DBPObject> next()
{
index++;
return (DBECommand<DBPObject>) commands.get(index).command;
}
@Override
public void remove()
{
commands.remove(index);
}
};
}
@Override
public int size()
{
return commands.size();
}
}
}