/*
* 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.ui.dialogs.data;
import org.eclipse.jface.action.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TreeEditor;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.themes.ITheme;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.core.CoreMessages;
import org.jkiss.dbeaver.core.DBeaverUI;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBPMessageType;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.data.*;
import org.jkiss.dbeaver.model.exec.DBCException;
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.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithResult;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.struct.DBSAttributeBase;
import org.jkiss.dbeaver.model.struct.DBSDataType;
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
import org.jkiss.dbeaver.ui.DBeaverIcons;
import org.jkiss.dbeaver.ui.UIIcon;
import org.jkiss.dbeaver.ui.UIUtils;
import org.jkiss.dbeaver.ui.controls.resultset.ThemeConstants;
import org.jkiss.dbeaver.ui.data.*;
import org.jkiss.dbeaver.ui.data.registry.ValueManagerRegistry;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.IdentityHashMap;
import java.util.Map;
/**
* Structure object editor
*/
public class ComplexObjectEditor extends TreeViewer {
private static final Log log = Log.getLog(ComplexObjectEditor.class);
private static class ComplexElement {
boolean created, modified;
Object value;
}
private static class CompositeField extends ComplexElement {
final DBSAttributeBase attribute;
DBDValueHandler valueHandler;
private CompositeField(DBPDataSource dataSource, DBSAttributeBase attribute, @Nullable Object value)
{
this.attribute = attribute;
this.value = value;
this.valueHandler = DBUtils.findValueHandler(dataSource, attribute);
}
}
private static class ArrayInfo {
private DBDValueHandler valueHandler;
private DBSDataType componentType;
}
private static class ArrayItem extends ComplexElement {
final ArrayInfo array;
int index;
private ArrayItem(ArrayInfo array, int index, Object value)
{
this.array = array;
this.index = index;
this.value = value;
}
}
private final IValueController parentController;
private final IValueEditor editor;
private DBCExecutionContext executionContext;
private final TreeEditor treeEditor;
private IValueEditor curCellEditor;
private Color backgroundAdded;
private Color backgroundDeleted;
private Color backgroundModified;
private CopyAction copyNameAction;
private CopyAction copyValueAction;
private Action addElementAction;
private Action removeElementAction;
private Map<Object, ComplexElement[]> childrenMap = new IdentityHashMap<>();
public ComplexObjectEditor(IValueController parentController, IValueEditor editor, int style)
{
super(parentController.getEditPlaceholder(), style | SWT.SINGLE | SWT.FULL_SELECTION);
this.parentController = parentController;
this.editor = editor;
ITheme currentTheme = parentController.getValueSite().getWorkbenchWindow().getWorkbench().getThemeManager().getCurrentTheme();
this.backgroundAdded = currentTheme.getColorRegistry().get(ThemeConstants.COLOR_SQL_RESULT_CELL_NEW_BACK);
this.backgroundDeleted = currentTheme.getColorRegistry().get(ThemeConstants.COLOR_SQL_RESULT_CELL_DELETED_BACK);
this.backgroundModified = currentTheme.getColorRegistry().get(ThemeConstants.COLOR_SQL_RESULT_CELL_MODIFIED_BACK);
final Tree treeControl = super.getTree();
treeControl.setHeaderVisible(true);
treeControl.setLinesVisible(true);
treeControl.addControlListener(new ControlAdapter() {
private boolean packing = false;
@Override
public void controlResized(ControlEvent e)
{
if (!packing) {
packing = true;
UIUtils.packColumns(treeControl, true, new float[]{0.2f, 0.8f});
if (treeControl.getColumn(0).getWidth() < 100) {
treeControl.getColumn(0).setWidth(100);
}
treeControl.removeControlListener(this);
}
}
});
ColumnViewerToolTipSupport.enableFor(this, ToolTip.NO_RECREATE);
{
TreeViewerColumn column = new TreeViewerColumn(this, SWT.NONE);
column.getColumn().setWidth(200);
column.getColumn().setMoveable(true);
column.getColumn().setText(CoreMessages.ui_properties_name);
column.setLabelProvider(new PropsLabelProvider(true));
}
{
TreeViewerColumn column = new TreeViewerColumn(this, SWT.NONE);
column.getColumn().setWidth(120);
column.getColumn().setMoveable(true);
column.getColumn().setText(CoreMessages.ui_properties_value);
column.setLabelProvider(new PropsLabelProvider(false));
}
treeEditor = new TreeEditor(treeControl);
treeEditor.horizontalAlignment = SWT.RIGHT;
treeEditor.verticalAlignment = SWT.CENTER;
treeEditor.grabHorizontal = true;
treeEditor.minimumWidth = 50;
treeControl.addMouseListener(new MouseAdapter() {
@Override
public void mouseDoubleClick(MouseEvent e)
{
TreeItem item = treeControl.getItem(new Point(e.x, e.y));
if (item != null && UIUtils.getColumnAtPos(item, e.x, e.y) == 1) {
showEditor(item, false);
}
}
});
treeControl.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e)
{
if (e.detail == SWT.TRAVERSE_RETURN) {
final TreeItem[] selection = treeControl.getSelection();
if (selection.length == 0) {
return;
}
if (treeEditor.getEditor() != null && !treeEditor.getEditor().isDisposed()) {
// Give a chance to catch it in editor handler
e.doit = true;
return;
}
showEditor(selection[0], (e.stateMask & SWT.SHIFT) == SWT.SHIFT);
e.doit = false;
e.detail = SWT.TRAVERSE_NONE;
}
}
});
super.setContentProvider(new StructContentProvider());
this.copyNameAction = new CopyAction(true);
this.copyValueAction = new CopyAction(false);
this.addElementAction = new AddElementAction();
this.removeElementAction = new RemoveElementAction();
addElementAction.setEnabled(true);
removeElementAction.setEnabled(false);
addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
final IStructuredSelection selection = (IStructuredSelection)event.getSelection();
if (selection == null || selection.isEmpty()) {
copyNameAction.setEnabled(false);
copyValueAction.setEnabled(false);
removeElementAction.setEnabled(false);
addElementAction.setEnabled(getInput() instanceof DBDCollection);
} else {
copyNameAction.setEnabled(true);
copyValueAction.setEnabled(true);
final Object element = selection.getFirstElement();
if (element instanceof ArrayItem) {
removeElementAction.setEnabled(true);
addElementAction.setEnabled(true);
}
}
}
});
createContextMenu();
}
private void createContextMenu()
{
Control control = getControl();
MenuManager menuMgr = new MenuManager();
Menu menu = menuMgr.createContextMenu(control);
menuMgr.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager)
{
if (!getSelection().isEmpty()) {
manager.add(copyNameAction);
manager.add(copyValueAction);
manager.add(new Separator());
}
try {
parentController.getValueManager().contributeActions(manager, parentController, editor);
} catch (DBCException e) {
log.error(e);
}
}
});
menuMgr.setRemoveAllWhenShown(true);
control.setMenu(menu);
}
@Override
public DBDComplexValue getInput() {
return (DBDComplexValue)super.getInput();
}
public void setModel(DBCExecutionContext executionContext, final DBDComplexValue value)
{
getTree().setRedraw(false);
try {
this.executionContext = executionContext;
this.childrenMap.clear();
setInput(value);
expandToLevel(2);
} finally {
getTree().setRedraw(true);
}
}
private void showEditor(final TreeItem item, boolean advanced) {
// Clean up any previous editor control
disposeOldEditor();
if (item == null) {
return;
}
try {
IValueController valueController = new ComplexValueController(
(ComplexElement)item.getData(),
advanced ? IValueController.EditType.EDITOR : IValueController.EditType.INLINE);
curCellEditor = valueController.getValueManager().createEditor(valueController);
if (curCellEditor != null) {
curCellEditor.createControl();
if (curCellEditor instanceof IValueEditorStandalone) {
((IValueEditorStandalone) curCellEditor).showValueEditor();
} else if (curCellEditor.getControl() != null) {
treeEditor.setEditor(curCellEditor.getControl(), item, 1);
}
if (!advanced) {
curCellEditor.primeEditorValue(valueController.getValue());
}
}
} catch (DBException e) {
UIUtils.showErrorDialog(getControl().getShell(), "Cell editor", "Can't open cell editor", e);
}
}
private void disposeOldEditor()
{
curCellEditor = null;
Control oldEditor = treeEditor.getEditor();
if (oldEditor != null) oldEditor.dispose();
}
public Object extractValue() {
DBDComplexValue complexValue = getInput();
final ComplexElement[] items = childrenMap.get(complexValue);
if (complexValue instanceof DBDValueCloneable) {
try {
complexValue = (DBDComplexValue) ((DBDValueCloneable) complexValue).cloneValue(new VoidProgressMonitor());
} catch (DBCException e) {
log.error("Error cloning complex value", e);
}
}
if (complexValue instanceof DBDComposite) {
for (int i = 0; i < items.length; i++) {
((DBDComposite) complexValue).setAttributeValue(((CompositeField)items[i]).attribute, items[i].value);
}
} else if (complexValue instanceof DBDCollection) {
if (items != null) {
final Object[] newValues = new Object[items.length];
for (int i = 0; i < items.length; i++) {
newValues[i] = items[i].value;
}
((DBDCollection) complexValue).setContents(newValues);
}
}
return complexValue;
}
private String getColumnText(ComplexElement obj, int columnIndex, DBDDisplayFormat format) {
if (obj instanceof CompositeField) {
CompositeField field = (CompositeField) obj;
if (columnIndex == 0) {
return field.attribute.getName();
}
return getValueText(field.valueHandler, field.attribute, field.value, format);
} else if (obj instanceof ArrayItem) {
ArrayItem item = (ArrayItem) obj;
if (columnIndex == 0) {
return String.valueOf(item.index);
}
return getValueText(item.array.valueHandler, item.array.componentType, item.value, format);
}
return String.valueOf(columnIndex);
}
private String getValueText(@NotNull DBDValueHandler valueHandler, @NotNull DBSTypedObject type, @Nullable Object value, @NotNull DBDDisplayFormat format)
{
if (value instanceof DBDCollection) {
return "[" + ((DBDCollection) value).getComponentType().getName() + " - " + ((DBDCollection) value).getItemCount() + "]";
} else if (value instanceof DBDComposite) {
return "[" + ((DBDComposite) value).getDataType().getName() + "]";
} else if (value instanceof DBDReference) {
return "--> [" + ((DBDReference) value).getReferencedType().getName() + "]";
} else {
return valueHandler.getValueDisplayString(type, value, format);
}
}
private class ComplexValueController implements IValueController, IMultiController {
private final ComplexElement item;
private final DBDValueHandler valueHandler;
private final DBSTypedObject type;
private final String name;
private final Object value;
private final EditType editType;
public ComplexValueController(ComplexElement obj, EditType editType) throws DBCException {
this.item = obj;
if (this.item instanceof CompositeField) {
CompositeField field = (CompositeField) this.item;
valueHandler = field.valueHandler;
type = field.attribute;
name = field.attribute.getName();
value = field.value;
} else if (this.item instanceof ArrayItem) {
ArrayItem arrayItem = (ArrayItem) this.item;
valueHandler = arrayItem.array.valueHandler;
type = arrayItem.array.componentType;
name = type.getTypeName() + "[" + arrayItem.index + "]";
value = arrayItem.value;
} else {
throw new DBCException("Unsupported complex object element: " + this.item);
}
this.editType = editType;
}
@NotNull
@Override
public DBCExecutionContext getExecutionContext()
{
return executionContext;
}
@Override
public String getValueName()
{
return name;
}
@Override
public DBSTypedObject getValueType()
{
return type;
}
@Nullable
@Override
public Object getValue()
{
return value;
}
@Override
public void updateValue(Object value, boolean updatePresentation)
{
if (CommonUtils.equalObjects(this.item.value, value)) {
return;
}
this.item.value = value;
this.item.modified = true;
refresh(this.item);
}
@Override
public DBDValueHandler getValueHandler()
{
return valueHandler;
}
@Override
public IValueManager getValueManager() {
DBSTypedObject valueType = getValueType();
return ValueManagerRegistry.findValueManager(
getExecutionContext().getDataSource(),
valueType,
getValueHandler().getValueObjectType(valueType));
}
@Override
public EditType getEditType()
{
return editType;
}
@Override
public boolean isReadOnly()
{
return parentController.isReadOnly();
}
@Override
public IWorkbenchPartSite getValueSite()
{
return parentController.getValueSite();
}
@Override
public Composite getEditPlaceholder()
{
return getTree();
}
@Override
public void refreshEditor() {
parentController.refreshEditor();
}
@Override
public void showMessage(String message, DBPMessageType messageType)
{
}
@Override
public void closeInlineEditor() {
disposeOldEditor();
}
@Override
public void nextInlineEditor(boolean next) {
disposeOldEditor();
}
}
class StructContentProvider implements IStructuredContentProvider, ITreeContentProvider
{
public StructContentProvider()
{
}
@Override
public void inputChanged(Viewer v, Object oldInput, Object newInput)
{
}
@Override
public void dispose()
{
}
@Override
public Object[] getElements(Object parent)
{
return getChildren(parent);
}
@Nullable
@Override
public Object getParent(Object child)
{
return null;
}
@Override
public ComplexElement[] getChildren(Object parent)
{
ComplexElement[] children = childrenMap.get(parent);
if (children != null) {
return children;
}
if (parent instanceof DBDComposite) {
DBDComposite structure = (DBDComposite)parent;
try {
DBSAttributeBase[] attributes = structure.getAttributes();
children = new CompositeField[attributes.length];
for (int i = 0; i < attributes.length; i++) {
DBSAttributeBase attr = attributes[i];
Object value = structure.getAttributeValue(attr);
children[i] = new CompositeField(structure.getDataType().getDataSource(), attr, value);
}
} catch (DBException e) {
log.error("Error getting structure meta data", e);
}
} else if (parent instanceof DBDCollection) {
DBDCollection array = (DBDCollection)parent;
ArrayInfo arrayInfo = makeArrayInfo(array);
children = new ArrayItem[array.getItemCount()];
for (int i = 0; i < children.length; i++) {
children[i] = new ArrayItem(arrayInfo, i, array.getItem(i));
}
} else if (parent instanceof DBDReference) {
final DBDReference reference = (DBDReference)parent;
DBRRunnableWithResult<Object> runnable = new DBRRunnableWithResult<Object>() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException
{
try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.UTIL, "Read reference value")) {
result = reference.getReferencedObject(session);
} catch (DBCException e) {
throw new InvocationTargetException(e);
}
}
};
DBeaverUI.runInUI(runnable);
children = getChildren(runnable.getResult());
} else if (parent instanceof CompositeField) {
Object value = ((CompositeField) parent).value;
if (isComplexType(value)) {
children = getChildren(value);
}
} else if (parent instanceof ArrayItem) {
Object value = ((ArrayItem) parent).value;
if (isComplexType(value)) {
children = getChildren(value);
}
}
if (children != null) {
childrenMap.put(parent, children);
}
return children;
}
private boolean isComplexType(Object value) {
return value instanceof DBDComplexValue;
}
@Override
public boolean hasChildren(Object parent)
{
return
parent instanceof DBDComposite ||
parent instanceof DBDCollection ||
parent instanceof DBDReference ||
(parent instanceof CompositeField && hasChildren(((CompositeField) parent).value)) ||
(parent instanceof ArrayItem && hasChildren(((ArrayItem) parent).value));
}
}
@NotNull
private ArrayInfo makeArrayInfo(DBDCollection array) {
ArrayInfo arrayInfo = new ArrayInfo();
arrayInfo.componentType = array.getComponentType();
arrayInfo.valueHandler = DBUtils.findValueHandler(arrayInfo.componentType.getDataSource(), arrayInfo.componentType);
return arrayInfo;
}
private void shiftArrayItems(ComplexElement[] arrayItems, int startIndex, int inc) {
for (int i = startIndex; i < arrayItems.length; i++) {
((ArrayItem)arrayItems[i]).index += inc;
}
}
private class PropsLabelProvider extends CellLabelProvider
{
private final boolean isName;
public PropsLabelProvider(boolean isName)
{
this.isName = isName;
}
public String getText(ComplexElement obj, int columnIndex)
{
return getColumnText(obj, columnIndex, DBDDisplayFormat.UI);
}
@Override
public String getToolTipText(Object obj)
{
if (obj instanceof CompositeField) {
return ((CompositeField) obj).attribute.getName() + " " + ((CompositeField) obj).attribute.getTypeName();
}
return null;
}
@Override
public void update(ViewerCell cell)
{
ComplexElement element = (ComplexElement) cell.getElement();
cell.setText(getText(element, cell.getColumnIndex()));
if (element.created) {
cell.setBackground(backgroundAdded);
} else if (element.modified) {
cell.setBackground(backgroundModified);
} else {
cell.setBackground(null);
}
}
}
private class CopyAction extends Action {
private final boolean isName;
public CopyAction(boolean isName) {
super(CoreMessages.controls_itemlist_action_copy + " " + getTree().getColumn(isName ? 0 : 1).getText());
this.isName = isName;
}
@Override
public void run()
{
final IStructuredSelection selection = getStructuredSelection();
if (!selection.isEmpty()) {
String text = getColumnText(
(ComplexElement) selection.getFirstElement(),
isName ? 0 : 1,
DBDDisplayFormat.NATIVE);
if (text != null) {
UIUtils.setClipboardContents(getTree().getDisplay(), TextTransfer.getInstance(), text);
}
}
}
}
private class AddElementAction extends Action {
public AddElementAction() {
super("Add element", DBeaverIcons.getImageDescriptor(UIIcon.ROW_ADD));
}
@Override
public void run() {
DBDCollection collection = (DBDCollection) getInput();
ComplexElement[] arrayItems = childrenMap.get(collection);
if (arrayItems == null) {
log.error("Can't find children items for add");
return;
}
final IStructuredSelection selection = getStructuredSelection();
ArrayItem newItem;
if (selection.isEmpty()) {
newItem = new ArrayItem(makeArrayInfo(collection), 0, null);
} else {
ArrayItem curItem = (ArrayItem) selection.getFirstElement();
newItem = new ArrayItem(curItem.array, curItem.index + 1, null);
}
shiftArrayItems(arrayItems, newItem.index, 1);
arrayItems = ArrayUtils.insertArea(ComplexElement.class, arrayItems, newItem.index, new ComplexElement[] {newItem} );
childrenMap.put(collection, arrayItems);
refresh();
final Widget treeItem = findItem(newItem);
if (treeItem != null) {
showEditor((TreeItem) treeItem, false);
}
}
}
private class RemoveElementAction extends Action {
public RemoveElementAction() {
super("Remove element", DBeaverIcons.getImageDescriptor(UIIcon.ROW_DELETE));
}
@Override
public void run() {
final IStructuredSelection selection = getStructuredSelection();
if (selection.isEmpty()) {
return;
}
DBDCollection collection = (DBDCollection) getInput();
ComplexElement[] arrayItems = childrenMap.get(collection);
if (arrayItems == null) {
log.error("Can't find children items for delete");
return;
}
ArrayItem item = (ArrayItem)selection.getFirstElement();
shiftArrayItems(arrayItems, item.index, -1);
arrayItems = ArrayUtils.remove(ComplexElement.class, arrayItems, item);
childrenMap.put(collection, arrayItems);
refresh();
}
}
public void contributeActions(IContributionManager manager) {
manager.add(addElementAction);
manager.add(removeElementAction);
}
}