/* * Copyright 2011-2012 Amazon Technologies, Inc. * * 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://aws.amazon.com/apache2.0 * * This file 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 com.amazonaws.eclipse.dynamodb.editor; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueEditor.NUMBER; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueEditor.STRING; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.N; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.NS; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.S; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.SS; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.format; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.getDataType; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.getValuesFromAttribute; import static com.amazonaws.eclipse.dynamodb.editor.AttributeValueUtil.setAttribute; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.core.commands.IHandler; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.commands.ActionHandler; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.TableEditor; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.part.EditorPart; import org.eclipse.ui.statushandlers.StatusManager; import com.amazonaws.AmazonClientException; import com.amazonaws.eclipse.core.AwsToolkitCore; import com.amazonaws.eclipse.core.ui.AbstractTableLabelProvider; import com.amazonaws.eclipse.dynamodb.AbstractAddNewAttributeDialog; import com.amazonaws.eclipse.dynamodb.DynamoDBPlugin; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.model.AttributeAction; import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; import com.amazonaws.services.dynamodbv2.model.ScanRequest; import com.amazonaws.services.dynamodbv2.model.ScanResult; import com.amazonaws.services.dynamodbv2.model.TableDescription; import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; /** * Scan editor for DynamoDB tables. */ public class DynamoDBTableEditor extends EditorPart { private static final String[] exportExtensions = new String[] { "*.csv" }; /* * SWT editor glue */ public static final String ID = "com.amazonaws.eclipse.dynamodb.editor.tableEditor"; private TableEditorInput tableEditorInput; private boolean dirty; /* * Large-scale UI elements */ private ToolBarManager toolBarManager; private ToolBar toolBar; private TableViewer viewer; private ContentProvider contentProvider; /* * Data model for UI: list of scan conditions assembled by the user and a * set of items they've edited, added and deleted. */ private final List<ScanConditionRow> scanConditions = new LinkedList<ScanConditionRow>(); private final EditedItems editedItems = new EditedItems(); private final Collection<Map<String, AttributeValue>> deletedItems = new LinkedList<Map<String, AttributeValue>>(); private final Collection<Map<String, AttributeValue>> addedItems = new LinkedList<Map<String, AttributeValue>>(); /* * Table info that we fetch and store */ private KeySchemaWithAttributeType tableKey; final Set<String> knownAttributes = new HashSet<String>(); private ScanResult scanResult = new ScanResult(); /* * Actions to enable and disable */ private Action runScanAction; private Action saveAction; private Action nextPageResultsAction; private Action exportAsCSVAction; private Action addNewAttributeAction; @Override public void doSave(IProgressMonitor monitor) { monitor.beginTask("Saving changes", editedItems.size() + deletedItems.size()); try{ AmazonDynamoDB dynamoDBClient = AwsToolkitCore.getClientFactory(tableEditorInput.getAccountId()) .getDynamoDBV2Client(); /* * Save all edited items, only touching edited attributes. */ if ( !editedItems.isEmpty() ) { for ( Iterator<Entry<Map<String, AttributeValue>, EditedItem>> iter = editedItems.iterator(); iter.hasNext(); ) { Entry<Map<String, AttributeValue>, EditedItem> editedItem = iter.next(); try { /* * Due to a bug in Dynamo, updateItem will not create a * new item when only the key is specified. Therefore, * we need two code paths here, as in * DynamoDBMapper.save(). */ if ( editedItem.getValue().getEditedAttributes().isEmpty() ) { PutItemRequest rq = new PutItemRequest().withTableName(tableEditorInput.getTableName()); rq.setItem(editedItem.getValue().getAttributes()); Map<String, ExpectedAttributeValue> expected = new HashMap<String, ExpectedAttributeValue>(); for ( String attr : editedItem.getValue().getAttributes().keySet() ) { expected.put(attr, new ExpectedAttributeValue().withExists(false)); } rq.setExpected(expected); dynamoDBClient.putItem(rq); } else { UpdateItemRequest rq = new UpdateItemRequest().withTableName(tableEditorInput .getTableName()); rq.setKey(editedItem.getKey()); Map<String, AttributeValueUpdate> values = new HashMap<String, AttributeValueUpdate>(); for ( String attributeName : editedItem.getValue().getEditedAttributes() ) { AttributeValueUpdate update = new AttributeValueUpdate(); AttributeValue attributeValue = editedItem.getValue().getAttributes() .get(attributeName); if ( attributeValue == null ) { update.setAction(AttributeAction.DELETE); } else { update.setAction(AttributeAction.PUT); update.setValue(attributeValue); } values.put(attributeName, update); } rq.setAttributeUpdates(values); dynamoDBClient.updateItem(rq); } for ( int col = 0; col < viewer.getTable().getColumnCount(); col++ ) { editedItem.getValue().getTableItem() .setForeground(col, Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); } iter.remove(); monitor.worked(1); } catch ( AmazonClientException e ) { StatusManager.getManager().handle( new Status(IStatus.ERROR, DynamoDBPlugin.PLUGIN_ID, "Error saving item with key " + editedItem.getKey() + ": " + e.getMessage()), StatusManager.SHOW); return; } } } /* * Delete all deleted items. */ if ( !deletedItems.isEmpty() ) { for ( Iterator<Map<String, AttributeValue>> iter = deletedItems.iterator(); iter.hasNext(); ) { Map<String, AttributeValue> deletedItem = iter.next(); try { dynamoDBClient.deleteItem(new DeleteItemRequest() .withTableName(tableEditorInput.getTableName()).withKey(deletedItem)); } catch ( AmazonClientException e ) { StatusManager.getManager().handle( new Status(IStatus.ERROR, DynamoDBPlugin.PLUGIN_ID, "Error deleting item with key " + deletedItem + ": " + e.getMessage()), StatusManager.SHOW); return; } iter.remove(); monitor.worked(1); } } /* * Exception handling: if we fail to execute any action above, the * editor is left in a sane state -- we clean up edited state as we * make each service call, so all we have to do is notify of the * exception and return without updating the editor's dirty state. */ } finally { monitor.done(); } dirty = false; this.saveAction.setEnabled(false); firePropertyChange(PROP_DIRTY); } @Override public void doSaveAs() { // unsupported } @Override public void init(IEditorSite site, IEditorInput input) throws PartInitException { setSite(site); setInput(input); this.tableEditorInput = (TableEditorInput) input; setPartName(input.getName()); } @Override public boolean isDirty() { return dirty; } private void markDirty() { dirty = true; saveAction.setEnabled(true); firePropertyChange(PROP_DIRTY); } @Override public boolean isSaveAsAllowed() { return false; } @Override public void createPartControl(Composite composite) { composite.setLayout(new FormLayout()); // Create the sash first, so the other controls // can be attached to it. final Sash sash = new Sash(composite, SWT.HORIZONTAL); FormData data = new FormData(); // Initial position is a quarter of the way down the composite data.top = new FormAttachment(25, 0); // And filling 100% of horizontal space data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); sash.setLayoutData(data); sash.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { // Move the sash to its new position and redraw it ((FormData) sash.getLayoutData()).top = new FormAttachment(0, event.y); sash.getParent().layout(); } }); this.toolBarManager = new ToolBarManager(SWT.LEFT); this.toolBar = this.toolBarManager.createControl(composite); Composite scanEditor = createScanEditor(composite); data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(scanEditor, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); this.toolBar.setLayoutData(data); data = new FormData(); data.top = new FormAttachment(this.toolBar, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); scanEditor.setLayoutData(data); // Results table is attached to the top of the sash Composite resultsComposite = new Composite(composite, SWT.BORDER); data = new FormData(); data.top = new FormAttachment(sash, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); resultsComposite.setLayoutData(data); createResultsTable(resultsComposite); createActions(); // initialize the table with results runScan(); } /** * Creates the composite to edit scan requests */ private Composite createScanEditor(Composite composite) { final Composite scanEditor = new Composite(composite, SWT.None); GridLayoutFactory.fillDefaults().applyTo(scanEditor); final Button addCondition = new Button(scanEditor, SWT.PUSH); addCondition.setToolTipText("Add scan condition"); addCondition.setText("Add scan condition"); addCondition.setImage(AwsToolkitCore.getDefault().getImageRegistry().get(AwsToolkitCore.IMAGE_ADD)); GridDataFactory.swtDefaults().indent(5, 0).applyTo(addCondition); // Selection listener creates a new row in the editor, then moves the // button below this new row. addCondition.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { scanEditor.getParent().setRedraw(false); ScanConditionRow scanConditionRow = new ScanConditionRow(scanEditor, knownAttributes); scanConditions.add(scanConditionRow); addCondition.moveBelow(scanConditionRow); scanEditor.pack(true); scanEditor.getParent().layout(true); scanEditor.getParent().setRedraw(true); } }); return scanEditor; } private void createActions() { runScanAction = new Action() { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_START); } @Override public String getText() { return "Run scan"; } @Override public String getToolTipText() { return getText(); } @Override public void run() { runScan(); } @Override public String getActionDefinitionId() { return "com.amazonaws.eclipse.dynamodb.editor.runScan"; } }; IHandler handler = new ActionHandler(runScanAction); IHandlerService handlerService = (IHandlerService) getSite().getService(IHandlerService.class); handlerService.activateHandler(runScanAction.getActionDefinitionId(), handler); saveAction = new Action() { @Override public ImageDescriptor getImageDescriptor() { return PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_ETOOL_SAVE_EDIT); } @Override public String getText() { return "Save changes to Dynamo"; } @Override public void run() { getEditorSite().getPage().saveEditor(DynamoDBTableEditor.this, false); } @Override public String getActionDefinitionId() { return "org.eclipse.ui.file.save"; } }; handlerService.activateHandler(saveAction.getActionDefinitionId(), new ActionHandler(saveAction)); nextPageResultsAction = new Action() { @Override public ImageDescriptor getImageDescriptor() { return DynamoDBPlugin.getDefault().getImageRegistry().getDescriptor(DynamoDBPlugin.IMAGE_NEXT_RESULTS); } @Override public String getText() { return "Next page of results"; } @Override public void run() { getNextPageResults(); } }; exportAsCSVAction = new Action() { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_EXPORT); } @Override public String getText() { return "Export as CSV"; } @Override public void run() { FileDialog dialog = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); dialog.setOverwrite(true); dialog.setFilterExtensions(exportExtensions); String csvFile = dialog.open(); if (csvFile != null) { writeCsvFile(csvFile); } } private void writeCsvFile(final String csvFile) { try { // truncate file before writing RandomAccessFile raf = new RandomAccessFile(new File(csvFile), "rw"); raf.setLength(0L); raf.close(); List<Map<String, AttributeValue>> items = new LinkedList<Map<String,AttributeValue>>(); for ( TableItem tableItem : viewer.getTable().getItems() ) { @SuppressWarnings("unchecked") Map<String, AttributeValue> e = (Map<String, AttributeValue>) tableItem.getData(); items.add(e); } BufferedWriter out = new BufferedWriter(new FileWriter(csvFile)); boolean seenOne = false; for (String col : contentProvider.getColumns()) { if ( seenOne ) { out.write(","); } else { seenOne = true; } out.write(col); } out.write("\n"); for ( Map<String, AttributeValue> item : items ) { seenOne = false; for (String col : contentProvider.getColumns()) { if (seenOne) { out.write(","); } else { seenOne = true; } AttributeValue values = item.get(col); if (values != null) { String value = format(values); // For csv files, we need to quote all values and escape all quotes value = value.replaceAll("\"", "\"\""); value = "\"" + value + "\""; out.write(value); } } out.write("\n"); } out.close(); } catch (Exception e) { AwsToolkitCore.getDefault().logError("Couldn't save CSV file", e); } } }; addNewAttributeAction = new Action() { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_ADD); } @Override public String getText() { return "Add attribute column"; } @Override public void run() { NewAttributeDialog dialog = new NewAttributeDialog(); if ( dialog.open() == 0 ) { contentProvider.columns.add(dialog.getNewAttributeName()); contentProvider.createColumn(viewer.getTable(), (TableColumnLayout) viewer.getTable().getParent() .getLayout(), dialog.getNewAttributeName()); viewer.getTable().getParent().layout(); } } final class NewAttributeDialog extends AbstractAddNewAttributeDialog { @Override public void validate() { if ( getButton(0) == null ) return; if ( getNewAttributeName().length() == 0 || contentProvider.columns.contains(getNewAttributeName()) ) { getButton(0).setEnabled(false); return; } getButton(0).setEnabled(true); return; } } }; runScanAction.setEnabled(false); saveAction.setEnabled(false); nextPageResultsAction.setEnabled(false); exportAsCSVAction.setEnabled(false); addNewAttributeAction.setEnabled(false); toolBarManager.add(runScanAction); toolBarManager.add(nextPageResultsAction); toolBarManager.add(saveAction); toolBarManager.add(exportAsCSVAction); toolBarManager.add(addNewAttributeAction); toolBarManager.update(true); } private void createResultsTable(Composite resultsComposite) { TableColumnLayout tableColumnLayout = new TableColumnLayout(); resultsComposite.setLayout(tableColumnLayout); this.viewer = new TableViewer(resultsComposite); this.viewer.getTable().setLinesVisible(true); this.viewer.getTable().setHeaderVisible(true); this.contentProvider = new ContentProvider(); this.viewer.setContentProvider(this.contentProvider); this.viewer.setLabelProvider(new LabelProvider()); final Table table = this.viewer.getTable(); final TableEditor editor = new TableEditor(table); editor.horizontalAlignment = SWT.LEFT; editor.grabHorizontal = true; final TextCellEditorListener listener = new TextCellEditorListener(table, editor); table.addListener(SWT.MouseUp, listener); table.addListener(SWT.FocusOut, listener); table.addListener(SWT.KeyDown, listener); MenuManager menuManager = new MenuManager("#PopupMenu"); menuManager.setRemoveAllWhenShown(true); menuManager.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { if ( table.getSelectionCount() > 0 ) { manager.add(new Action() { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_REMOVE); } @Override public void run() { listener.deleteItems(); } @Override public String getText() { if ( table.getSelectionCount() == 1 ) { return "Delete selected item"; } else { return "Delete selected items"; } } }); } } }); table.setMenu(menuManager.createContextMenu(table)); } @Override public void setFocus() { // no-op } /** * Updates the query results asynchronously. Must be called from the UI * thread. */ private void runScan() { // Clear out the existing table and edit states this.viewer.getTable().setEnabled(false); runScanAction.setEnabled(false); nextPageResultsAction.setEnabled(false); exportAsCSVAction.setEnabled(false); addNewAttributeAction.setEnabled(false); editedItems.clear(); deletedItems.clear(); // this.exportAsCSV.setEnabled(false); for ( TableColumn col : this.viewer.getTable().getColumns() ) { col.dispose(); } new Thread() { @Override public void run() { if ( tableKey == null ) { DescribeTableResult describeTable = AwsToolkitCore .getClientFactory(DynamoDBTableEditor.this.tableEditorInput.getAccountId()) .getDynamoDBV2Client() .describeTable(new DescribeTableRequest().withTableName(tableEditorInput.getTableName())); TableDescription tableDescription = describeTable.getTable(); tableKey = convertToKeySchemaWithAttributeType(tableDescription); } scanResult = new ScanResult(); try { ScanRequest scanRequest = new ScanRequest().withTableName(tableEditorInput.getTableName()); scanRequest.setScanFilter(new HashMap<String, Condition>()); for ( ScanConditionRow row : scanConditions ) { if ( row.shouldExecute() ) { scanRequest.getScanFilter().put(row.getAttributeName(), row.getScanCondition()); } } scanResult = AwsToolkitCore.getClientFactory(DynamoDBTableEditor.this.tableEditorInput.getAccountId()) .getDynamoDBV2Client().scan(scanRequest); } catch ( Exception e ) { DynamoDBPlugin.getDefault().reportException(e.getMessage(), e); return; } final ScanResult result = scanResult; Display.getDefault().asyncExec(new Runnable() { public void run() { viewer.setInput(result.getItems()); viewer.getTable().setEnabled(true); viewer.getTable().getParent().layout(); runScanAction.setEnabled(true); nextPageResultsAction.setEnabled(scanResult.getLastEvaluatedKey() != null); exportAsCSVAction.setEnabled(true); addNewAttributeAction.setEnabled(true); } }); } }.start(); } /** * Fetches the next page of results from the scan and updates the table with them. */ private void getNextPageResults() { this.viewer.getTable().setEnabled(false); runScanAction.setEnabled(false); nextPageResultsAction.setEnabled(false); exportAsCSVAction.setEnabled(false); new Thread() { @Override public void run() { try { ScanRequest scanRequest = new ScanRequest().withTableName(tableEditorInput.getTableName()); scanRequest.setScanFilter(new HashMap<String, Condition>()); for ( ScanConditionRow row : scanConditions ) { if ( row.shouldExecute() ) { scanRequest.getScanFilter().put(row.getAttributeName(), row.getScanCondition()); } } scanRequest.setExclusiveStartKey(scanResult.getLastEvaluatedKey()); scanResult = AwsToolkitCore.getClientFactory(DynamoDBTableEditor.this.tableEditorInput.getAccountId()) .getDynamoDBV2Client().scan(scanRequest); } catch ( Exception e ) { DynamoDBPlugin.getDefault().reportException(e.getMessage(), e); return; } final ScanResult result = scanResult; Display.getDefault().asyncExec(new Runnable() { public void run() { contentProvider.addItems(result.getItems()); viewer.refresh(); DynamoDBTableEditor.this.viewer.getTable().setEnabled(true); DynamoDBTableEditor.this.viewer.getTable().getParent().layout(); runScanAction.setEnabled(true); nextPageResultsAction.setEnabled(scanResult.getLastEvaluatedKey() != null); exportAsCSVAction.setEnabled(true); } }); } }.start(); } /** * Content provider creates columns for the table and keeps track of them * for other parts of the UI. */ private class ContentProvider implements IStructuredContentProvider { private List<Map<String, AttributeValue>> input; private final List<Map<String, AttributeValue>> elementList = new ArrayList<Map<String,AttributeValue>>(); private final List<String> columns = new ArrayList<String>(); /** * Adds a single item to the table. */ void addItem(Map<String, AttributeValue> item) { elementList.set(elementList.size() - 1, item); elementList.add(new HashMap<String, AttributeValue>()); viewer.refresh(); } /** * Adds a list of new items to the table. */ public void addItems(List<Map<String, AttributeValue>> items) { // Remove the (possible) empty row and add it to the end if ( !elementList.isEmpty() && elementList.get(elementList.size() - 1).isEmpty() ) { elementList.remove(elementList.size() - 1); } elementList.addAll(items); // expand columns if necessary List<String> columns = new LinkedList<String>(); for ( Map<String, AttributeValue> item : items ) { columns.addAll(item.keySet()); } Table table = (Table) viewer.getControl(); TableColumnLayout layout = (TableColumnLayout) table.getParent().getLayout(); for ( String column : columns ) { if ( !this.columns.contains(column) ) { this.columns.add(column); createColumn(table, layout, column); } } // empty row for adding new rows elementList.add(new HashMap<String, AttributeValue>()); } @SuppressWarnings("unchecked") public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) { this.input = (List<Map<String, AttributeValue>>) newInput; this.elementList.clear(); this.columns.clear(); initializeElements(); if ( this.input != null ) { Table table = (Table) viewer.getControl(); TableColumnLayout layout = (TableColumnLayout) table.getParent().getLayout(); for ( String col : this.columns ) { createColumn(table, layout, col); } } } private void createColumn(Table table, TableColumnLayout layout, String col) { TableColumn column = new TableColumn(table, SWT.NONE); column.setText(col); layout.setColumnData(column, new ColumnWeightData(10)); } public void dispose() { } public Object[] getElements(final Object inputElement) { initializeElements(); return this.elementList.toArray(); } private synchronized void initializeElements() { if ( elementList.isEmpty() && input != null ) { Set<String> columns = new HashSet<String>(); for ( Map<String, AttributeValue> item : input ) { columns.addAll(item.keySet()); } // We add the hash and range keys back in at the beginning, so // remove them for now columns.remove(tableKey.getHashKeyAttributeName()); if ( tableKey.hasRangeKey() ) { columns.remove(tableKey.getRangeKeyAttributeName()); } List<String> sortedColumns = new ArrayList<String>(); sortedColumns.addAll(columns); Collections.sort(sortedColumns); sortedColumns.add(0, tableKey.getHashKeyAttributeName()); if ( tableKey.hasRangeKey() ) { sortedColumns.add(1, tableKey.getRangeKeyAttributeName()); } synchronized (knownAttributes) { knownAttributes.addAll(sortedColumns); } elementList.addAll(input); // empty row at the end for adding new rows elementList.add(new HashMap<String, AttributeValue>()); this.columns.addAll(sortedColumns); } } private synchronized List<String> getColumns() { return this.columns; } } private class LabelProvider extends AbstractTableLabelProvider { @Override public String getColumnText(final Object element, final int columnIndex) { @SuppressWarnings("unchecked") Map<String, AttributeValue> item = (Map<String, AttributeValue>) element; String column = DynamoDBTableEditor.this.contentProvider.getColumns().get(columnIndex); AttributeValue values = item.get(column); return format(values); } } /** * CreateNewItemDialog now extends AttributeValueInputDialog, which is a * more generic class that includes basic dialog template and value * validation. */ private final class CreateNewItemDialog extends AttributeValueInputDialog { @SuppressWarnings("serial") private CreateNewItemDialog() { super("Create new item", "Enter the key for the new item", true, new ArrayList<String>() {{ add(tableKey.getHashKeyAttributeName()); if (tableKey.getRangeKeyAttributeName() != null) { add(tableKey.getRangeKeyAttributeName()); } }}, new HashMap<String, Integer>() {{ put(tableKey.getHashKeyAttributeName(), getDataType(tableKey.getHashKeyAttributeType())); if (tableKey.getRangeKeyAttributeName() != null) { put(tableKey.getRangeKeyAttributeName(), getDataType(tableKey.getRangeKeyAttributeType())); } }}, null); } Map<String, AttributeValue> getNewItem() { String hashKey = attributeValues.get(tableKey.getHashKeyAttributeName()); String rangeKey = attributeValues.get(tableKey.getRangeKeyAttributeName()); Map<String, AttributeValue> item = new HashMap<String, AttributeValue>(); AttributeValue hashKeyAttribute = new AttributeValue(); setAttribute(hashKeyAttribute, Arrays.asList(hashKey), tableKey.getHashKeyAttributeType()); item.put(tableKey.getHashKeyAttributeName(), hashKeyAttribute); if ( rangeKey != null && rangeKey.length() > 0 ) { AttributeValue rangeKeyAttribute = new AttributeValue(); setAttribute(rangeKeyAttribute, Arrays.asList(rangeKey), tableKey.getRangeKeyAttributeType()); item.put(tableKey.getRangeKeyAttributeName(), rangeKeyAttribute); } return item; } } /** * Listener to respond to clicks in a cell, invoking a cell editor */ private final class TextCellEditorListener implements Listener { private final Table table; private final TableEditor editor; private AttributeValueEditor editorComposite; private TextCellEditorListener(final Table table, final TableEditor editor) { this.table = table; this.editor = editor; } public void handleEvent(final Event event) { if ( event.type == SWT.FocusOut && this.editorComposite != null && !this.editorComposite.isDisposed() ) { Control focus = Display.getCurrent().getFocusControl(); if ( focus != this.editorComposite && focus != this.editorComposite.editorText && focus != this.table ) { this.editorComposite.dispose(); } } else if (event.type == SWT.KeyDown && event.keyCode == SWT.DEL) { deleteItems(); return; } else if (event.type != SWT.MouseUp || event.button != 1) { return; } Rectangle clientArea = this.table.getClientArea(); Point pt = new Point(event.x, event.y); int row = table.getTopIndex(); while ( row < table.getItemCount() ) { boolean visible = false; final TableItem item = this.table.getItem(row); // We don't care about clicks in the first 1 or 2 columns since // they are read-only, except in the last row. final boolean isLastRow = row == table.getItemCount() - 1; int numKeyColumns = tableKey.hasRangeKey() ? 2 : 1; for ( int col = 0; col < table.getColumnCount(); col++ ) { Rectangle rect = item.getBounds(col); if ( rect.contains(pt) ) { if ( editorComposite != null && !editorComposite.isDisposed() ) { editorComposite.dispose(); } if ( isLastRow ) { invokeNewItemDialog(item, row); return; } else if ( col < numKeyColumns ) { // table.select(row); return; } final int column = col; final int rowNum = row; final String attributeName = item.getParent().getColumn(col).getText(); @SuppressWarnings("unchecked") Map<String, AttributeValue> dynamoDbItem = (Map<String, AttributeValue>) item.getData(); final AttributeValue attributeValue = dynamoDbItem.containsKey(attributeName) ? dynamoDbItem .get(attributeName) : new AttributeValue(); // If this is a binary value, don't allow editing if ( attributeValue != null && (attributeValue.getBS() != null || attributeValue.getB() != null) ) { invokeBinaryValueDialog(item, attributeName, column, rowNum); return; } // Don't support editing new data types if ( attributeValue != null && (attributeValue.getBOOL() != null || attributeValue.getNULL() != null || attributeValue.getM() != null || attributeValue.getL() != null) ) { Dialog dialog = new MessageDialog( Display.getDefault().getActiveShell(), "Attribute edit not supported", AwsToolkitCore.getDefault().getImageRegistry().get(AwsToolkitCore.IMAGE_AWS_ICON), "Editing BOOL/NULL/Map/List attributes is currently not supported.", MessageDialog.NONE, new String[] { "OK" }, 0); dialog.open(); return; } configureCellEditor(item, column, rowNum, attributeName, attributeValue); // If this is a multi-value item, don't allow textual editing if ( attributeValue != null && (attributeValue.getSS() != null || attributeValue.getNS() != null || attributeValue.getBS() != null) ) { editorComposite.editorText.setEditable(false); editorComposite.editorText.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { invokeMultiValueDialog(item, attributeName, column, rowNum, editorComposite.dataTypeCombo.getSelectionIndex()); editorComposite.dispose(); } }); return; } editorComposite.editorText.selectAll(); editorComposite.editorText.setFocus(); return; } if ( !visible && rect.intersects(clientArea) ) { visible = true; } } if ( !visible ) { return; } row++; } } /** * Invokes a dialog showing a binary value or set of values. */ private void invokeBinaryValueDialog(TableItem item, String attributeName, int column, int rowNum) { Dialog dialog = new MessageDialog( Display.getDefault().getActiveShell(), "Binary attribute", AwsToolkitCore.getDefault().getImageRegistry().get(AwsToolkitCore.IMAGE_AWS_ICON), "This is a binary attribute. Editing binary attributes is unsupported, " + "but you can copy the base64 encoding of this attribute to the clipboard.", MessageDialog.NONE, new String[] { "Copy to clipboard", "Cancel" }, 0); int result = dialog.open(); if ( result == 0 ) { Clipboard cb = new Clipboard(Display.getDefault()); String data = item.getText(column); TextTransfer textTransfer = TextTransfer.getInstance(); cb.setContents(new Object[] { data }, new Transfer[] { textTransfer }); } } /** * Deletes all selected items from the table. */ private void deleteItems() { List<Integer> selectionIndices = new ArrayList<Integer>(); for (int i : table.getSelectionIndices()) { selectionIndices.add(i); } // Remove all these indices from the data model of the content // provider. We go through them backwards to avoid having to // recalculate offsets caused by the list shifting to fill in the // gaps. Collections.sort(selectionIndices); for (int i = selectionIndices.size() - 1; i >= 0; i--) { Integer selectionIndex = selectionIndices.get(i); contentProvider.elementList.remove(selectionIndex.intValue()); Map<String, AttributeValue> key = getKey(table.getItem(selectionIndex)); editedItems.remove(key); // If this is a newly-added item, don't try to issue a delete // request for it. if ( addedItems.contains(key) ) { addedItems.remove(key); } else { deletedItems.add(key); } } markDirty(); viewer.refresh(); } /** * Configures the member cell editor for the table item and column * given. */ private void configureCellEditor(final TableItem item, final int column, final int rowNum, final String attributeName, final AttributeValue attributeValue) { this.editorComposite = new AttributeValueEditor(this.table, SWT.None, editor, table.getItemHeight(), attributeValue); editor.setEditor(this.editorComposite, item, column); editorComposite.editorText.setText(item.getText(column)); editorComposite.editorText.addModifyListener(new ModifyListener() { public void modifyText(final ModifyEvent e) { Text text = editorComposite.editorText; int dataType = editorComposite.getSelectedDataType(false); markModified(item, text, rowNum, column, Arrays.asList(text.getText()), dataType); } }); /* * We validate the user input of the scalar value when the text * editor is being disposed. (For set type, the validation happens in MultiValueAttributeEditorDialog.) */ editorComposite.editorText.addDisposeListener(new DisposeListener() { @SuppressWarnings({ "serial", "unchecked" }) public void widgetDisposed(DisposeEvent e) { AttributeValue updateAttributeValue = ( (Map<String, AttributeValue>)item.getData() ).get(attributeName); boolean isScalarAttribute = updateAttributeValue != null && ( updateAttributeValue.getN() != null || updateAttributeValue.getS() != null || updateAttributeValue.getB() != null); /* Only do validation when it is a scalar type. */ if ( isScalarAttribute ) { final String attributeInput = editorComposite.editorText.getText(); final int dataType = editorComposite.getSelectedDataType(false); if ( !AttributeValueUtil.validateScalarAttributeInput(attributeInput, dataType, false) ) { /* Open up a non-cancelable input dialog */ AttributeValueInputDialog attributeValueInputDialog = new AttributeValueInputDialog( "Invalid attribute value", "Please provide a valid value for the following attribute", false, Arrays.asList(attributeName), new HashMap<String, Integer>() {{ put(attributeName, dataType); }}, new HashMap<String, String>() {{ put(attributeName, attributeInput); }}); attributeValueInputDialog.open(); /* Update the attribute editor and markModified */ Text text = editorComposite.editorText; String validatedValue = attributeValueInputDialog.getInputValue(attributeName); text.setText(validatedValue); markModified(item, text, rowNum, column, Arrays.asList(text.getText()), dataType); } } } }); editorComposite.editorText.addTraverseListener(new TraverseListener() { public void keyTraversed(final TraverseEvent e) { TextCellEditorListener.this.editorComposite.dispose(); } }); editorComposite.multiValueEditorButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { invokeMultiValueDialog(item, attributeName, column, rowNum, editorComposite.dataTypeCombo.getSelectionIndex()); editorComposite.dispose(); } }); // Changing the data type must be marked as a change editorComposite.dataTypeCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Collection<String> values = getValuesFromAttribute(attributeValue); int dataType; switch (editorComposite.dataTypeCombo.getSelectionIndex()) { case STRING: if ( attributeValue.getS() != null || attributeValue.getSS() != null ) return; if ( values.size() > 1 ) { dataType = SS; } else { dataType = S; } break; case NUMBER: if ( attributeValue.getN() != null || attributeValue.getNS() != null ) return; if ( values.size() > 1 ) { dataType = NS; } else { dataType = N; } break; default: throw new RuntimeException("Unexpected selection index " + editorComposite.dataTypeCombo.getSelectionIndex()); } markModified(item, editorComposite.editorText, rowNum, column, values, dataType); } }); } private void invokeMultiValueDialog(final TableItem item, final String attributeName, final int column, final int row, final int selectedType) { @SuppressWarnings("unchecked") Map<String, AttributeValue> dynamoDbItem = (Map<String, AttributeValue>) item.getData(); MultiValueAttributeEditorDialog multiValueEditorDialog = new MultiValueAttributeEditorDialog(Display .getDefault().getActiveShell(), dynamoDbItem.get(attributeName), selectedType); int returnValue = multiValueEditorDialog.open(); /* Save set */ if ( returnValue == 0 ) { int dataType = editorComposite.getSelectedDataType(true); markModified(item, editorComposite.editorText, row, column, multiValueEditorDialog.getValues(), dataType); } /* Save single value */ else if ( returnValue == 1 ) { int dataType = editorComposite.getSelectedDataType(false); markModified(item, editorComposite.editorText, row, column, multiValueEditorDialog.getValues(), dataType); } /* Don't do anything when the user pressed Cancel */ } } /** * Invokes a new dialog to allow creation of a new item in the table. */ private void invokeNewItemDialog(TableItem tableItem, int row) { CreateNewItemDialog dialog = new CreateNewItemDialog(); int result = dialog.open(); if ( result == 0 ) { Map<String, AttributeValue> newItem = dialog.getNewItem(); contentProvider.addItem(newItem); Map<String, AttributeValue> key = getKey(tableItem); addedItems.add(key); markModified(tableItem, null, row, 0, getValuesFromAttribute(newItem.get(tableKey.getHashKeyAttributeName())), getDataType(tableKey.getHashKeyAttributeType())); if ( tableKey.hasRangeKey() ) { markModified(tableItem, null, row, 1, getValuesFromAttribute(newItem.get(tableKey.getRangeKeyAttributeName())), getDataType(tableKey.getRangeKeyAttributeType())); } } } /** * Marks the given tree item and column modified. * * TODO: type checking for numbers */ protected void markModified(final TableItem item, final Text editorControl, final int row, final int column, final Collection<String> newValue, int dataType) { final String attributeName = item.getParent().getColumn(column).getText(); @SuppressWarnings("unchecked") Map<String, AttributeValue> dynamoDbItem = (Map<String, AttributeValue>) item.getData(); AttributeValue attributeValue = dynamoDbItem.get(attributeName); if ( attributeValue == null ) { attributeValue = new AttributeValue(); dynamoDbItem.put(attributeName, attributeValue); } setAttribute(attributeValue, newValue, dataType); Map<String, AttributeValue> editedItemKey = getKey(item); if ( !editedItems.containsKey(editedItemKey) ) { editedItems.add(editedItemKey, new EditedItem(dynamoDbItem, item)); } // Don't add key attributes to the list of edited attributes if ( !attributeName.equals(tableKey.getHashKeyAttributeName()) && ( !tableKey.hasRangeKey() || !attributeName.equals(tableKey.getRangeKeyAttributeName())) ) { editedItems.get(editedItemKey).markAttributeEdited(attributeName); } // We may already have another entry here, but since we're updating the // data model as we go, we can overwrite as many times as we want. editedItems.update(editedItemKey, dynamoDbItem); item.setText(column, format(attributeValue)); // Treat the empty string as a null for easier saving if ( newValue.size() == 1 && newValue.iterator().next().length() == 0 ) { dynamoDbItem.remove(attributeName); } item.setForeground(column, Display.getDefault().getSystemColor(SWT.COLOR_RED)); if ( editorControl != null ) editorControl.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); markDirty(); } /** * Returns a key for recording a change to the item given, reusing the key * if it exists or returning a new one otherwise. */ private Map<String, AttributeValue> getKey(final TableItem item) { @SuppressWarnings("unchecked") Map<String, AttributeValue> dynamoDbItem = (Map<String, AttributeValue>) item.getData(); Map<String, AttributeValue> keyAttributes = new HashMap<String, AttributeValue>(); String hashKeyAttributeName = tableKey.getHashKeyAttributeName(); keyAttributes.put(hashKeyAttributeName, dynamoDbItem.get(hashKeyAttributeName)); if ( tableKey.hasRangeKey() ) { String rangeKeyAttributeName = tableKey.getRangeKeyAttributeName(); keyAttributes.put(rangeKeyAttributeName, dynamoDbItem.get(rangeKeyAttributeName)); } return keyAttributes; } /** * Use DynamoDB V2 to get the attribtue names and types of both hash and range keys * of the table, and then save them in a KeySchemaWithAttributeType object. */ private static KeySchemaWithAttributeType convertToKeySchemaWithAttributeType( TableDescription table) { KeySchemaWithAttributeType keySchema = new KeySchemaWithAttributeType(); String hashKeyAttributeName = null; String rangeKeyAttributeName = null; for (KeySchemaElement key : table.getKeySchema()) { if (key.getKeyType().equals(KeyType.HASH.toString())) { hashKeyAttributeName = key.getAttributeName(); } else if (key.getKeyType().equals(KeyType.RANGE.toString())) { rangeKeyAttributeName = key.getAttributeName(); } } for (AttributeDefinition attribute : table.getAttributeDefinitions()) { if (hashKeyAttributeName.equals(attribute.getAttributeName())) { keySchema.setHashKeyElement(hashKeyAttributeName, attribute.getAttributeType()); } if (rangeKeyAttributeName != null && rangeKeyAttributeName.equals(attribute .getAttributeName())) { keySchema.setRangeKeyElement(rangeKeyAttributeName, attribute.getAttributeType()); } } return keySchema; } /** * DynamoDB v2 no longer returns AttributeType as part of KeySchemaElement, * so we use this class to record the attribute names and types of both * hash key and range key. * */ private static class KeySchemaWithAttributeType { private String hashKeyAttributeName; private String hashKeyAttributeType; private String rangeKeyAttributeName; private String rangeKeyAttributeType; public void setHashKeyElement(String attributeName, String attributeType) { hashKeyAttributeName = attributeName; hashKeyAttributeType = attributeType; } public void setRangeKeyElement(String attributeName, String attributeType) { rangeKeyAttributeName = attributeName; rangeKeyAttributeType = attributeType; } public boolean hasRangeKey() { return rangeKeyAttributeName != null && rangeKeyAttributeType != null; } public String getHashKeyAttributeName() { return hashKeyAttributeName; } public String getHashKeyAttributeType() { return hashKeyAttributeType; } public String getRangeKeyAttributeName() { return rangeKeyAttributeName; } public String getRangeKeyAttributeType() { return rangeKeyAttributeType; } } }