package com.amazonaws.eclipse.datatools.enablement.simpledb.ui.editor; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.datatools.sqltools.sqlbuilder.views.source.SQLEditorDocumentProvider; import org.eclipse.datatools.sqltools.sqlbuilder.views.source.SQLSourceEditingEnvironment; import org.eclipse.datatools.sqltools.sqlbuilder.views.source.SQLSourceViewerConfiguration; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.source.AnnotationModel; import org.eclipse.jface.text.source.CompositeRuler; import org.eclipse.jface.text.source.ISharedTextColors; import org.eclipse.jface.text.source.SourceViewer; import org.eclipse.jface.viewers.ColumnWeightData; 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.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; 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.FillLayout; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridLayout; 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.internal.editors.text.EditorsPlugin; import org.eclipse.ui.part.EditorPart; import com.amazonaws.eclipse.core.AwsToolkitCore; import com.amazonaws.eclipse.core.BrowserUtils; import com.amazonaws.eclipse.core.ui.AbstractTableContentProvider; import com.amazonaws.eclipse.core.ui.AbstractTableLabelProvider; import com.amazonaws.eclipse.datatools.enablement.simpledb.driver.SimpleDBItemName; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.BatchPutAttributesRequest; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.ReplaceableAttribute; import com.amazonaws.services.simpledb.model.ReplaceableItem; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; /** * Editor to run queries and edit results */ public class QueryEditor extends EditorPart { public static final String ID = "com.amazonaws.eclipse.datatools.enablement.simpledb.ui.editor.queryEditor"; private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+"); //$NON-NLS-1$ private static final Pattern PATTERN_FROM_CLAUSE = Pattern.compile("from\\s+[\\S&&[^,]]+"); //$NON-NLS-1$ public static final char DELIMITED_IDENTIFIER_QUOTE = '`'; private DomainEditorInput domainEditorInput; private TableViewer viewer; boolean dirty; private Map<String, Collection<EditedAttributeValue>> editedCells = new HashMap<String, Collection<EditedAttributeValue>>(); private String resultDomain; private ToolBarManager toolBarManager; private ToolBar toolBar; private SourceViewer sqlSourceViewer; private Composite sqlSourceViewerComposite; private IDocument sqlSourceDocument; private ExportAsCSVAction exportAsCSV; private ContentProvider contentProvider; @Override public void doSave(final IProgressMonitor monitor) { AmazonSimpleDB simpleDBClient = AwsToolkitCore.getClientFactory(this.domainEditorInput.getAccountId()) .getSimpleDBClient(); String domain = this.resultDomain; ArrayList<ReplaceableItem> items = new ArrayList<ReplaceableItem>(); for ( String itemName : QueryEditor.this.editedCells.keySet() ) { ReplaceableItem replaceableItem = new ReplaceableItem(); replaceableItem.setName(itemName); Collection<ReplaceableAttribute> attributes = new LinkedList<ReplaceableAttribute>(); for ( EditedAttributeValue editedValue : QueryEditor.this.editedCells.get(itemName) ) { if ( editedValue.newValue != null ) { for ( String v : editedValue.newValue ) { ReplaceableAttribute attr = new ReplaceableAttribute().withName(editedValue.name) .withReplace(true).withValue(v); attributes.add(attr); } } } if ( !attributes.isEmpty() ) { items.add(replaceableItem); replaceableItem.setAttributes(attributes); } } if ( !items.isEmpty() ) { simpleDBClient.batchPutAttributes(new BatchPutAttributesRequest(domain, items)); } for ( String itemName : QueryEditor.this.editedCells.keySet() ) { for ( EditedAttributeValue editedValue : QueryEditor.this.editedCells.get(itemName) ) { QueryEditor.this.viewer.getTable().getItem(editedValue.row) .setForeground(editedValue.col, Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); } } QueryEditor.this.editedCells.clear(); this.dirty = false; firePropertyChange(PROP_DIRTY); } @Override public void doSaveAs() { } @Override public void init(final IEditorSite site, final IEditorInput input) throws PartInitException { setSite(site); setInput(input); this.domainEditorInput = (DomainEditorInput) input; setPartName(input.getName()); } @Override public boolean isDirty() { return this.dirty; } private void markDirty() { this.dirty = true; firePropertyChange(PROP_DIRTY); } @Override public boolean isSaveAsAllowed() { return false; } @Override public void createPartControl(final 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); configureSQLSourceViewer(composite); data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(this.sqlSourceViewerComposite, 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); this.sqlSourceViewerComposite.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(); } private void configureSQLSourceViewer(final Composite composite) { int VERTICAL_RULER_WIDTH = 12; int styles = SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION; ISharedTextColors sharedColors = EditorsPlugin.getDefault().getSharedTextColors(); CompositeRuler ruler = new CompositeRuler(VERTICAL_RULER_WIDTH); try { this.sqlSourceDocument = SQLEditorDocumentProvider.createDocument(getDefaultQuery()); } catch ( CoreException e ) { e.printStackTrace(); } AnnotationModel annotationModel = new AnnotationModel(); annotationModel.connect(this.sqlSourceDocument); this.sqlSourceViewerComposite = new Composite(composite, SWT.BORDER); this.sqlSourceViewerComposite.setLayout(new FillLayout()); this.sqlSourceViewer = new SourceViewer(this.sqlSourceViewerComposite, ruler, null, true, styles); SQLSourceEditingEnvironment.connect(); SQLSourceViewerConfiguration configuration = new SQLSourceViewerConfiguration(); configuration.getCompletionProcessor().setCompletionProposalAutoActivationCharacters(new char[0]); this.sqlSourceViewer.configure(configuration); this.sqlSourceViewer.setDocument(this.sqlSourceDocument, annotationModel); } /** * Creates actions for this editor */ private void createActions() { this.toolBarManager.add(new OpenSelectSyntaxDocumentationAction()); this.toolBarManager.add(new Action() { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_START); } @Override public String getText() { return "Run query"; } @Override public String getToolTipText() { return getText(); } @Override public void run() { runQuery(QueryEditor.this.sqlSourceDocument.get()); } @Override public int getAccelerator() { return SWT.CONTROL | SWT.ALT | 'x'; } }); this.exportAsCSV = new ExportAsCSVAction(); this.exportAsCSV.setEnabled(false); this.toolBarManager.add(this.exportAsCSV); this.toolBarManager.update(true); } private static final String[] exportExtensions = new String[] { "*.csv" }; private class ExportAsCSVAction extends Action { @Override public ImageDescriptor getImageDescriptor() { return AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_EXPORT); } @Override public String getText() { return "Export as CSV"; } @Override public String getToolTipText() { return "Export as comma-separated-value"; } @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 { RandomAccessFile raf = new RandomAccessFile(new File(csvFile), "rw"); raf.setLength(0L); raf.close(); List<SimpleDBItem> items = new LinkedList<SimpleDBItem>(); Set<String> columns = new LinkedHashSet<String>(); for ( TableItem tableItem : QueryEditor.this.viewer.getTable().getItems() ) { SimpleDBItem e = (SimpleDBItem) tableItem.getData(); columns.addAll(e.columns); items.add(e); } BufferedWriter out = new BufferedWriter(new FileWriter(csvFile)); out.write(SimpleDBItemName.ITEM_HEADER); for (String col : columns) { out.write(","); out.write(col); } out.write("\n"); for ( SimpleDBItem item : items ) { out.write(item.itemName); for (String col : columns) { out.write(","); Collection<String> values = item.attributes.get(col); if (values != null) { String value = join(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); } } } private static class OpenSelectSyntaxDocumentationAction extends Action { public static final String SIMPLEDB_SELECT_SYNTAX_DOCUMENTATION_URL = "http://docs.amazonwebservices.com/AmazonSimpleDB/latest/DeveloperGuide/index.html?UsingSelect.html"; public OpenSelectSyntaxDocumentationAction() { this.setText("SimpleDB Select Syntax Documentation"); this.setToolTipText("View Amazon SimpleDB select query syntax documentation"); this.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_LCL_LINKTO_HELP)); } @Override public void run() { BrowserUtils.openExternalBrowser(SIMPLEDB_SELECT_SYNTAX_DOCUMENTATION_URL); } } private static String join(final Collection<String> values) { return join(values, ","); } /** * Joins a collection of attribute values, correctly handling single-value * attributes and empty sets. */ private static String join(final Collection<String> values, final String separator) { if ( values == null || values.isEmpty() ) { return ""; } if ( values.size() == 1 ) { return values.iterator().next(); } StringBuilder builder = new StringBuilder("["); boolean seenOne = false; for ( String s : values ) { if ( seenOne ) { builder.append(separator); } else { seenOne = true; } builder.append(s); } builder.append("]"); return builder.toString(); } /** * Updates the query results asynchronously. Must be called from the UI * thread. */ private void runQuery(final String query) { // Clear out the existing table this.viewer.getTable().setEnabled(false); this.exportAsCSV.setEnabled(false); for ( TableColumn col : this.viewer.getTable().getColumns() ) { col.dispose(); } new Thread() { @Override public void run() { SelectResult select = null; try { select = AwsToolkitCore.getClientFactory(QueryEditor.this.domainEditorInput.getAccountId()).getSimpleDBClient() .select(new SelectRequest(query)); } catch ( Exception e ) { AwsToolkitCore.getDefault().reportException(e.getMessage(), e); return; } final SelectResult result = select; QueryEditor.this.resultDomain = getDomainName(query); Display.getDefault().syncExec(new Runnable() { public void run() { QueryEditor.this.viewer.setInput(result); QueryEditor.this.viewer.getTable().setEnabled(true); QueryEditor.this.exportAsCSV.setEnabled(true); QueryEditor.this.viewer.getTable().getParent().layout(); } }); } }.start(); } private String convertSQLIdentifierToCatalogFormat(final String sqlIdentifier, final char idDelimiterQuote) { String catalogIdentifier = sqlIdentifier; if ( sqlIdentifier != null ) { String delimiter = String.valueOf(idDelimiterQuote); boolean isDelimited = sqlIdentifier.startsWith(delimiter) && sqlIdentifier.endsWith(delimiter); boolean containsQuotedDelimiters = sqlIdentifier.indexOf(delimiter + delimiter) > -1; if ( isDelimited ) { catalogIdentifier = sqlIdentifier.substring(1, sqlIdentifier.length() - 1); if ( containsQuotedDelimiters ) { catalogIdentifier = catalogIdentifier.replaceAll(delimiter + delimiter, delimiter); } } else { catalogIdentifier = sqlIdentifier; } } return catalogIdentifier; } /** * Returns the domain name from a select query. */ public String getDomainName(final String sql) { Matcher m = PATTERN_FROM_CLAUSE.matcher(sql); if ( m.find() ) { String fromExpression = sql.substring(m.start(), m.end()); m = PATTERN_WHITESPACE.matcher(fromExpression); if ( m.find() ) { String domainName = convertSQLIdentifierToCatalogFormat(fromExpression.substring(m.end()), DELIMITED_IDENTIFIER_QUOTE); return domainName; } } return null; } /** * Creates the results table */ private void createResultsTable(final 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; TextCellEditorListener listener = new TextCellEditorListener(table, editor); table.addListener(SWT.MouseUp, listener); table.addListener(SWT.FocusOut, listener); } private String getDefaultQuery() { return "select * from `" + this.domainEditorInput.getDomainName() + "`"; } @Override public void setFocus() { } /** * 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 Composite editorComposite; private Text editorText; private Button button; 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.editorText && focus != this.table ) { this.editorComposite.dispose(); } } Rectangle clientArea = this.table.getClientArea(); Point pt = new Point(event.x, event.y); int row = this.table.getTopIndex(); while ( row < this.table.getItemCount() ) { boolean visible = false; final TableItem item = this.table.getItem(row); // We don't care about clicks in the first column since they // are read-only for ( int col = 1; col < this.table.getColumnCount(); col++ ) { Rectangle rect = item.getBounds(col); if ( rect.contains(pt) ) { if ( this.editorComposite != null && !this.editorComposite.isDisposed() ) { this.editorComposite.dispose(); } // If this is a multi-value item, don't allow textual // editing SimpleDBItem simpleDBItem = (SimpleDBItem) item.getData(); final String attributeName = item.getParent().getColumn(col).getText(); if ( simpleDBItem.attributes.containsKey(attributeName) && simpleDBItem.attributes.get(attributeName).size() > 1 ) { invokeMultiValueDialog(item, attributeName, col, row); return; } createEditor(); final int column = col; final int rowNum = row; this.editor.setEditor(this.editorComposite, item, col); this.editorText.setText(item.getText(col)); this.editorText.addModifyListener(new ModifyListener() { public void modifyText(final ModifyEvent e) { markModified(item, column, rowNum, TextCellEditorListener.this.editorText); } }); this.editorText.addTraverseListener(new TraverseListener() { public void keyTraversed(final TraverseEvent e) { TextCellEditorListener.this.editorComposite.dispose(); } }); this.button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { invokeMultiValueDialog(item, attributeName, column, rowNum); TextCellEditorListener.this.editorComposite.dispose(); } }); this.editorText.selectAll(); this.editorText.setFocus(); return; } if ( !visible && rect.intersects(clientArea) ) { visible = true; } } if ( !visible ) { return; } row++; } } private void createEditor() { this.editorComposite = new Composite(this.table, SWT.None); this.editorComposite.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); GridLayout layout = new GridLayout(2, false); layout.marginHeight = 0; layout.marginWidth = 0; layout.verticalSpacing = 0; layout.horizontalSpacing = 0; this.editorComposite.setLayout(layout); this.editorText = new Text(this.editorComposite, SWT.None); GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.TOP).grab(true, true).indent(2, 2) .applyTo(this.editorText); this.button = new Button(this.editorComposite, SWT.None); this.button.setText("..."); GridDataFactory.fillDefaults().align(SWT.RIGHT, SWT.TOP).grab(false, true).applyTo(this.button); this.button.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); } private void invokeMultiValueDialog(final TableItem item, final String attributeName, final int column, final int row) { SimpleDBItem simpleDBItem = (SimpleDBItem) item.getData(); MultiValueAttributeEditorDialog multiValueEditorDialog = new MultiValueAttributeEditorDialog(Display.getDefault() .getActiveShell(), simpleDBItem, attributeName); int returnValue = multiValueEditorDialog.open(); if ( returnValue == 0 ) { markModified(item, column, row, multiValueEditorDialog.getValues()); } } } private class LabelProvider extends AbstractTableLabelProvider { @Override public String getColumnText(final Object element, final int columnIndex) { SimpleDBItem item = (SimpleDBItem) element; if ( columnIndex == 0 ) { return item.itemName; } // Column index is offset by one to make room for item name String column = QueryEditor.this.contentProvider.getColumns()[columnIndex - 1]; Collection<String> values = item.attributes.get(column); return join(values); } } private class ContentProvider extends AbstractTableContentProvider { private SelectResult input; private Object[] elements; private String[] columns; @Override public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) { this.input = (SelectResult) newInput; this.elements = null; initializeElements(); if ( this.input != null ) { Table table = (Table) viewer.getControl(); TableColumnLayout layout = (TableColumnLayout) table.getParent().getLayout(); TableColumn column = new TableColumn(table, SWT.NONE); column.setText(SimpleDBItemName.ITEM_HEADER); layout.setColumnData(column, new ColumnWeightData(10)); for ( String col : this.columns ) { column = new TableColumn(table, SWT.NONE); column.setText(col); layout.setColumnData(column, new ColumnWeightData(10)); } } } @Override public Object[] getElements(final Object inputElement) { initializeElements(); return this.elements; } private synchronized void initializeElements() { if ( this.elements == null && this.input != null ) { List<SimpleDBItem> items = new LinkedList<SimpleDBItem>(); Set<String> columns = new LinkedHashSet<String>(); for ( Item item : this.input.getItems() ) { SimpleDBItem e = new SimpleDBItem(item); columns.addAll(e.columns); items.add(e); } this.elements = items.toArray(); this.columns = columns.toArray(new String[columns.size()]); } } private synchronized String[] getColumns() { return this.columns; } } /** * Container for edited attributes */ private class EditedAttributeValue { private String name; private Collection<String> newValue; int row; int col; @Override public String toString() { return "name: " + this.name + "; new: " + this.newValue; } } /** * Marks the given tree item and column modified. */ protected void markModified(final TableItem item, final int column, final int row, final Text text) { List<String> values = new LinkedList<String>(); values.add(text.getText()); markModified(item, column, row, values); } /** * Marks the given tree item and column modified. */ protected void markModified(final TableItem item, final int column, final int row, final Collection<String> newValue) { item.setText(column, join(newValue)); SimpleDBItem simpleDBItem = (SimpleDBItem) item.getData(); String itemName = simpleDBItem.itemName; String attributeName = item.getParent().getColumn(column).getText(); // Update the data model with this new value simpleDBItem.attributes.put(attributeName, newValue); // Bootstrap new items if ( !this.editedCells.containsKey(itemName) ) { this.editedCells.put(itemName, new LinkedList<EditedAttributeValue>()); } // Find this value in the list and update it EditedAttributeValue value = null; for ( EditedAttributeValue v : this.editedCells.get(itemName) ) { if ( v.name.equals(attributeName) ) { value = v; break; } } // Create the edited value if this is the first time we've edited it if ( value == null ) { value = new EditedAttributeValue(); value.name = attributeName; value.col = column; value.row = row; this.editedCells.get(itemName).add(value); } value.newValue = newValue; // Finally update the table UI to reflect the edit item.setForeground(column, Display.getDefault().getSystemColor(SWT.COLOR_RED)); markDirty(); } }