package name.abuchen.portfolio.ui.wizards.datatransfer; import static name.abuchen.portfolio.ui.util.SWTHelper.widest; import java.io.IOException; import java.nio.charset.Charset; import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableColorProvider; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.wizard.IWizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.RGB; 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.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Spinner; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import name.abuchen.portfolio.datatransfer.Extractor; import name.abuchen.portfolio.datatransfer.csv.CSVExtractor; import name.abuchen.portfolio.datatransfer.csv.CSVImporter; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.AmountField; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.Column; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.DateField; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.EnumField; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.EnumMapFormat; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.Field; import name.abuchen.portfolio.datatransfer.csv.CSVImporter.FieldFormat; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.FormDataFactory; import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport; import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupportWrapper; import name.abuchen.portfolio.ui.util.viewers.StringEditingSupport; import name.abuchen.portfolio.ui.wizards.AbstractWizardPage; public class CSVImportDefinitionPage extends AbstractWizardPage implements ISelectionChangedListener { private static final class Delimiter { private final char delimiter; private final String label; private Delimiter(char delimiter, String label) { this.delimiter = delimiter; this.label = label; } public char getDelimiter() { return delimiter; } public String getLabel() { return label; } @Override public String toString() { return getLabel(); } } private final CSVImporter importer; private final boolean onlySecurityPrices; private TableViewer tableViewer; public CSVImportDefinitionPage(CSVImporter importer, boolean onlySecurityPrices) { super("importdefinition"); //$NON-NLS-1$ setTitle(Messages.CSVImportWizardTitle); setDescription(Messages.CSVImportWizardDescription); this.importer = importer; this.onlySecurityPrices = onlySecurityPrices; if (onlySecurityPrices) importer.setExtractor(importer.getSecurityPriceExtractor()); } public CSVImporter getImporter() { return importer; } @Override public IWizardPage getNextPage() { if (onlySecurityPrices) return null; if (importer.getExtractor() == importer.getSecurityPriceExtractor()) return getWizard().getPage(SelectSecurityPage.PAGE_ID); else return getWizard().getPage(ReviewExtractedItemsPage.PAGE_ID); } @Override public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); setControl(container); container.setLayout(new FormLayout()); Label lblTarget = new Label(container, SWT.RIGHT); lblTarget.setText(Messages.CSVImportLabelTarget); Combo cmbTarget = new Combo(container, SWT.READ_ONLY); ComboViewer target = new ComboViewer(cmbTarget); target.setContentProvider(ArrayContentProvider.getInstance()); target.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { return ((Extractor) element).getLabel(); } }); target.getCombo().setEnabled(!onlySecurityPrices); target.addSelectionChangedListener(this); Label lblDelimiter = new Label(container, SWT.NONE); lblDelimiter.setText(Messages.CSVImportLabelDelimiter); Combo cmbDelimiter = new Combo(container, SWT.READ_ONLY); ComboViewer delimiter = new ComboViewer(cmbDelimiter); delimiter.setContentProvider(ArrayContentProvider.getInstance()); delimiter.setInput(new Delimiter[] { new Delimiter(',', Messages.CSVImportSeparatorComma), // new Delimiter(';', Messages.CSVImportSeparatorSemicolon), // new Delimiter('\t', Messages.CSVImportSeparatorTab) }); cmbDelimiter.select(1); delimiter.addSelectionChangedListener(this); Label lblSkipLines = new Label(container, SWT.NONE); lblSkipLines.setText(Messages.CSVImportLabelSkipLines); final Spinner skipLines = new Spinner(container, SWT.BORDER); skipLines.setMinimum(0); skipLines.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent event) { onSkipLinesChanged(skipLines.getSelection()); } }); Label lblEncoding = new Label(container, SWT.NONE); lblEncoding.setText(Messages.CSVImportLabelEncoding); Combo cmbEncoding = new Combo(container, SWT.READ_ONLY); ComboViewer encoding = new ComboViewer(cmbEncoding); encoding.setContentProvider(ArrayContentProvider.getInstance()); encoding.setInput(Charset.availableCharsets().values().toArray()); encoding.setSelection(new StructuredSelection(Charset.defaultCharset())); encoding.addSelectionChangedListener(this); final Button firstLineIsHeader = new Button(container, SWT.CHECK); firstLineIsHeader.setText(Messages.CSVImportLabelFirstLineIsHeader); firstLineIsHeader.setSelection(true); firstLineIsHeader.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent event) { onFirstLineIsHeaderChanged(firstLineIsHeader.getSelection()); } @Override public void widgetDefaultSelected(SelectionEvent event) {} }); Composite compositeTable = new Composite(container, SWT.NONE); // // form layout // int width = widest(lblTarget, lblDelimiter, lblEncoding); FormDataFactory.startingWith(lblTarget).width(width).top(new FormAttachment(0, 5)).thenRight(cmbTarget) .right(new FormAttachment(50, -5)).thenBelow(cmbDelimiter).label(lblDelimiter) .right(new FormAttachment(50, -5)).thenBelow(cmbEncoding).label(lblEncoding) .right(new FormAttachment(50, -5)); FormDataFactory.startingWith(cmbDelimiter).thenRight(lblSkipLines).suffix(skipLines); FormDataFactory.startingWith(cmbEncoding).thenRight(firstLineIsHeader); FormData data = new FormData(); data.top = new FormAttachment(cmbEncoding, 10); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); data.bottom = new FormAttachment(100, 0); data.width = 100; data.height = 100; compositeTable.setLayoutData(data); // // table & columns // TableColumnLayout layout = new TableColumnLayout(); compositeTable.setLayout(layout); tableViewer = new TableViewer(compositeTable, SWT.BORDER | SWT.FULL_SELECTION); final Table table = tableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); tableViewer.setLabelProvider(new ImportLabelProvider(importer)); tableViewer.setContentProvider(ArrayContentProvider.getInstance()); table.addMouseListener(new MouseListener() { @Override public void mouseUp(MouseEvent e) {} @Override public void mouseDown(MouseEvent e) {} @Override public void mouseDoubleClick(MouseEvent e) { TableItem item = table.getItem(0); if (item == null) return; int columnIndex = -1; for (int ii = 0; ii < table.getColumnCount(); ii++) { Rectangle bounds = item.getBounds(ii); int width = table.getColumn(ii).getWidth(); if (e.x >= bounds.x && e.x <= bounds.x + width) columnIndex = ii; } if (columnIndex >= 0) onColumnSelected(columnIndex); } }); // // setup form elements // target.setInput(importer.getExtractors()); target.getCombo().select(importer.getExtractors().indexOf(importer.getExtractor())); doProcessFile(); } @Override public void selectionChanged(SelectionChangedEvent event) { Object element = ((IStructuredSelection) event.getSelectionProvider().getSelection()).getFirstElement(); if (element instanceof CSVExtractor) { onTargetChanged((CSVExtractor) element); } else if (element instanceof Delimiter) { importer.setDelimiter(((Delimiter) element).getDelimiter()); doProcessFile(); } else if (element instanceof Charset) { importer.setEncoding((Charset) element); doProcessFile(); } } private void onTargetChanged(CSVExtractor def) { if (!def.equals(importer.getExtractor())) { importer.setExtractor(def); doProcessFile(); } } private void onSkipLinesChanged(int linesToSkip) { importer.setSkipLines(linesToSkip); doProcessFile(); } private void onFirstLineIsHeaderChanged(boolean isFirstLineHeader) { importer.setFirstLineHeader(isFirstLineHeader); doProcessFile(); } private void onColumnSelected(int columnIndex) { ColumnConfigDialog dialog = new ColumnConfigDialog(getShell(), importer.getExtractor(), importer.getColumns()[columnIndex]); dialog.open(); doUpdateTable(); } private void doProcessFile() { try { importer.processFile(); tableViewer.getTable().setRedraw(false); for (TableColumn column : tableViewer.getTable().getColumns()) column.dispose(); TableColumnLayout layout = (TableColumnLayout) tableViewer.getTable().getParent().getLayout(); for (Column column : importer.getColumns()) { TableColumn tableColumn = new TableColumn(tableViewer.getTable(), SWT.None); layout.setColumnData(tableColumn, new ColumnPixelData(80, true)); setColumnLabel(tableColumn, column); } List<Object> input = new ArrayList<Object>(); input.add(importer); input.addAll(importer.getRawValues()); tableViewer.setInput(input); tableViewer.refresh(); tableViewer.getTable().pack(); for (TableColumn column : tableViewer.getTable().getColumns()) column.pack(); doUpdateErrorMessages(); } catch (IOException e) { PortfolioPlugin.log(e); ErrorDialog.openError(getShell(), Messages.LabelError, e.getMessage(), new Status(Status.ERROR, PortfolioPlugin.PLUGIN_ID, e.getMessage(), e)); } finally { tableViewer.getTable().setRedraw(true); } } private void setColumnLabel(TableColumn tableColumn, Column column) { tableColumn.setText(column.getLabel()); tableColumn.setAlignment(column.getField() instanceof AmountField ? SWT.RIGHT : SWT.LEFT); } private void doUpdateTable() { Table table = tableViewer.getTable(); table.setRedraw(false); try { for (int ii = 0; ii < table.getColumnCount(); ii++) setColumnLabel(table.getColumn(ii), importer.getColumns()[ii]); tableViewer.refresh(); doUpdateErrorMessages(); } finally { table.setRedraw(true); } } private void doUpdateErrorMessages() { Set<Field> fieldsToMap = new HashSet<Field>(importer.getExtractor().getFields()); for (Column column : importer.getColumns()) fieldsToMap.remove(column.getField()); if (fieldsToMap.isEmpty()) { setMessage(null); setPageComplete(true); } else { String required = fieldsToMap.stream().filter(f -> !f.isOptional()).map(Field::getName) .collect(Collectors.joining(", ")); //$NON-NLS-1$ String optional = fieldsToMap.stream().filter(Field::isOptional).map(Field::getName) .collect(Collectors.joining(", ")); //$NON-NLS-1$ boolean onlyOptional = required.length() == 0; setPageComplete(onlyOptional); StringBuilder message = new StringBuilder(); if (required.length() > 0) message.append(MessageFormat.format(Messages.CSVImportErrorMissingFields, required)).append("\n"); //$NON-NLS-1$ if (optional.length() > 0) message.append(MessageFormat.format(Messages.CSVImportInformationOptionalFields, optional)); setMessage(message.toString(), onlyOptional ? IMessageProvider.INFORMATION : IMessageProvider.ERROR); } } private static final class ImportLabelProvider extends LabelProvider implements ITableLabelProvider, ITableColorProvider { private static final RGB GREEN = new RGB(152, 251, 152); private static final RGB RED = new RGB(255, 127, 80); private CSVImporter importer; private final LocalResourceManager resources; private ImportLabelProvider(CSVImporter importer) { this.importer = importer; this.resources = new LocalResourceManager(JFaceResources.getResources()); } @Override public void dispose() { this.resources.dispose(); super.dispose(); } @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof CSVImporter) { Column column = importer.getColumns()[columnIndex]; if (column.getField() == null) return Messages.CSVImportLabelDoubleClickHere; else return MessageFormat.format(Messages.CSVImportLabelMappedToField, column.getField().getName()); } else { String[] line = (String[]) element; if (line != null && columnIndex < line.length) return line[columnIndex]; } return null; } @Override public Color getForeground(Object element, int columnIndex) { return element instanceof CSVImporter ? Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY) : null; } @Override public Color getBackground(Object element, int columnIndex) { if (element instanceof CSVImporter) return null; Column column = importer.getColumns()[columnIndex]; if (column.getField() == null) return null; try { if (column.getFormat() != null) { String text = getColumnText(element, columnIndex); if (text != null) column.getFormat().getFormat().parseObject(text); } return resources.createColor(GREEN); } catch (ParseException e) { return resources.createColor(RED); } } } private static class ColumnConfigDialog extends Dialog implements ISelectionChangedListener { private static final Field EMPTY = new Field("---"); //$NON-NLS-1$ private CSVExtractor definition; private Column column; protected ColumnConfigDialog(Shell parentShell, CSVExtractor definition, Column column) { super(parentShell); setShellStyle(getShellStyle() | SWT.SHEET); this.definition = definition; this.column = column; } @Override protected void configureShell(Shell shell) { super.configureShell(shell); shell.setText(Messages.CSVImportLabelEditMapping); } @Override protected Control createDialogArea(Composite parent) { Composite composite = (Composite) super.createDialogArea(parent); Label label = new Label(composite, SWT.NONE); label.setText(Messages.CSVImportLabelEditMapping); ComboViewer mappedTo = new ComboViewer(composite, SWT.READ_ONLY); mappedTo.setContentProvider(ArrayContentProvider.getInstance()); List<Field> fields = new ArrayList<Field>(); fields.add(EMPTY); fields.addAll(definition.getFields()); mappedTo.setInput(fields); final Composite details = new Composite(composite, SWT.NONE); final StackLayout layout = new StackLayout(); details.setLayout(layout); final Composite emptyArea = new Composite(details, SWT.NONE); GridLayoutFactory glf = GridLayoutFactory.fillDefaults().margins(0, 0); final Composite dateArea = new Composite(details, SWT.NONE); glf.applyTo(dateArea); label = new Label(dateArea, SWT.NONE); label.setText(Messages.CSVImportLabelFormat); final ComboViewer dateFormats = new ComboViewer(dateArea, SWT.READ_ONLY); dateFormats.setContentProvider(ArrayContentProvider.getInstance()); dateFormats.setInput(DateField.FORMATS); dateFormats.getCombo().select(0); dateFormats.addSelectionChangedListener(this); final Composite valueArea = new Composite(details, SWT.NONE); glf.applyTo(valueArea); label = new Label(valueArea, SWT.NONE); label.setText(Messages.CSVImportLabelFormat); final ComboViewer valueFormats = new ComboViewer(valueArea, SWT.READ_ONLY); valueFormats.setContentProvider(ArrayContentProvider.getInstance()); valueFormats.setInput(AmountField.FORMATS); valueFormats.getCombo().select(0); valueFormats.addSelectionChangedListener(this); final Composite keyArea = new Composite(details, SWT.NONE); glf.applyTo(keyArea); final TableViewer tableViewer = new TableViewer(keyArea, SWT.FULL_SELECTION); tableViewer.setContentProvider(new KeyMappingContentProvider()); tableViewer.getTable().setLinesVisible(true); tableViewer.getTable().setHeaderVisible(true); GridDataFactory.fillDefaults().grab(false, true).applyTo(tableViewer.getTable()); TableViewerColumn col = new TableViewerColumn(tableViewer, SWT.NONE); col.getColumn().setText(Messages.CSVImportLabelExpectedValue); col.getColumn().setWidth(100); col.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { return ((KeyMappingContentProvider.Entry<?>) element).getKey(); } }); col = new TableViewerColumn(tableViewer, SWT.NONE); col.getColumn().setText(Messages.CSVImportLabelProvidedValue); col.getColumn().setWidth(100); col.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { return ((KeyMappingContentProvider.Entry<?>) element).getValue(); } }); ColumnEditingSupport.prepare(tableViewer); col.setEditingSupport(new ColumnEditingSupportWrapper(tableViewer, new StringEditingSupport(KeyMappingContentProvider.Entry.class, "value"))); //$NON-NLS-1$ layout.topControl = emptyArea; mappedTo.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { Field field = (Field) ((IStructuredSelection) event.getSelection()).getFirstElement(); if (field != column.getField()) column.setField(field != EMPTY ? field : null); if (field instanceof DateField) { layout.topControl = dateArea; if (column.getFormat() != null) dateFormats.setSelection(new StructuredSelection(column.getFormat())); else dateFormats.setSelection(new StructuredSelection(dateFormats.getElementAt(0))); } else if (field instanceof AmountField) { layout.topControl = valueArea; if (column.getFormat() != null) valueFormats.setSelection(new StructuredSelection(column.getFormat())); else valueFormats.setSelection(new StructuredSelection(valueFormats.getElementAt(0))); } else if (field instanceof EnumField) { layout.topControl = keyArea; EnumField<?> ef = (EnumField<?>) field; FieldFormat f = column.getFormat(); if (f == null || !(f.getFormat() instanceof EnumMapFormat)) { f = new FieldFormat(null, ef.createFormat()); column.setFormat(f); } tableViewer.setInput((EnumMapFormat<?>) f.getFormat()); } else { layout.topControl = emptyArea; } details.layout(); } }); if (this.column.getField() != null) { mappedTo.setSelection(new StructuredSelection(this.column.getField())); } else { mappedTo.getCombo().select(0); } return composite; } @Override public void selectionChanged(SelectionChangedEvent event) { FieldFormat format = (FieldFormat) ((IStructuredSelection) event.getSelectionProvider().getSelection()) .getFirstElement(); column.setFormat(format != null ? format : null); } } private static class KeyMappingContentProvider implements IStructuredContentProvider { /* Map.Entry#setValue is not backed by EnumMap :-( */ public static final class Entry<M extends Enum<M>> { private EnumMap<M, String> map; private M key; private Entry(EnumMap<M, String> map, M key) { this.map = map; this.key = key; } public String getKey() { return key.toString(); } public String getValue() { return map.get(key); } @SuppressWarnings("unused") public void setValue(String value) { map.put(key, value); } } private EnumMapFormat<?> mapFormat; @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { this.mapFormat = (EnumMapFormat<?>) newInput; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Object[] getElements(Object inputElement) { if (mapFormat == null) return new Object[0]; List<Entry<?>> elements = new ArrayList<Entry<?>>(); for (Enum<?> entry : mapFormat.map().keySet()) elements.add(new Entry(mapFormat.map(), entry)); Collections.sort(elements, (e1, e2) -> e1.key.name().compareToIgnoreCase(e2.key.name())); return elements.toArray(); } @Override public void dispose() { // nothing to do } } }