Java tutorial
/* KDXplore provides KDDart Data Exploration and Management Copyright (C) 2015,2016,2017 Diversity Arrays Technology, Pty Ltd. KDXplore may be redistributed and may be modified under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. KDXplore is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with KDXplore. If not, see <http://www.gnu.org/licenses/>. */ package com.diversityarrays.kdxplore.design; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.CancellationException; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.apache.commons.collections15.Closure; import com.diversityarrays.kdxplore.Shared; import com.diversityarrays.kdxplore.trialdesign.TrialDesignPreferences; import com.diversityarrays.kdxplore.ui.Toast; import com.diversityarrays.util.Check; import com.diversityarrays.util.DocumentChangeListener; import com.diversityarrays.util.Either; import com.diversityarrays.util.MsgBox; import com.diversityarrays.util.Pair; import com.diversityarrays.util.UnicodeChars; import net.pearcan.dnd.DropLocationInfo; import net.pearcan.dnd.FileDrop; import net.pearcan.dnd.FileListTransferHandler; import net.pearcan.excel.ExcelUtil; import net.pearcan.excel.ExcelUtil.SheetInfo; import net.pearcan.ui.DefaultBackgroundRunner; import net.pearcan.ui.GuiUtil; import net.pearcan.ui.widget.PromptScrollPane; import net.pearcan.ui.widget.PromptTextField; import net.pearcan.util.BackgroundTask; import net.pearcan.util.GBH; /** * If Excel file: * <pre> * +---------------------------------+ * | #Rows: [nnn] Sheet: [ v] | * | --------------------------------| * | data preview shows here | * | | * | | * | - - - - - - - - - - - - ---- - -| * | Role Assignment here | * | | * | --------------------------------| * </pre> * * If CSV file: then Sheet dropdown is hidden * </pre> * * @author brianp */ @SuppressWarnings("nls") public abstract class EntryFileImportDialog<EFile, Role> extends JDialog { private static final String CARD_ERROR = "cardError"; private static final String CARD_DATA = "cardData"; public EFile entryFile; private RowDataProvider rowDataProvider; private SpinnerNumberModel previewRowCountSpinnerModel = new SpinnerNumberModel(20, 2, 100, 1); private final DefaultComboBoxModel<String> sheetNamesModel = new DefaultComboBoxModel<>(); private final JComboBox<String> sheetNamesComboBox = new JComboBox<>(sheetNamesModel); /** * To protect against looping calls to updatePreviewData() when initialising the content * of the sheetNamesModel and setting initial selection */ private boolean initialisingSheetNames; private ActionListener sheetNamesActionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Object item = sheetNamesComboBox.getSelectedItem(); if (item == null) { selectedSheetName = null; } else { selectedSheetName = sheetNamesComboBox.getSelectedItem().toString(); } // If we were called because the UpdateDataPreviewTask // is populating the combobox model then don't recurse. if (!initialisingSheetNames) { updateDataPreview(previewRowCountSpinnerModel.getNumber().intValue()); } } }; private final DataPreviewTableModel dataPreviewTableModel = new DataPreviewTableModel(); private final JTable dataPreviewTable = new JTable(dataPreviewTableModel); private final PromptScrollPane dataPreviewScrollPane = new PromptScrollPane(dataPreviewTable, ""); private final Action acceptAction = new AbstractAction("Accept") { @Override public void actionPerformed(ActionEvent e) { try { rowDataProvider.reset(); entryFile = createEntryFile(rowDataProvider, headingRoleTableModel.getRoleByHeading()); dispose(); } catch (IOException | EntryFileException e1) { MsgBox.error(EntryFileImportDialog.this, e1, getTitle() + " - Error"); } } }; private final Action cancelAction = new AbstractAction(UnicodeChars.CANCEL_CROSS) { @Override public void actionPerformed(ActionEvent e) { entryFile = null; dispose(); } }; private final HeadingRoleTableModel<Role> headingRoleTableModel; private final HeadingRoleTable<Role> headingRoleTable; private final JScrollPane headingTableScrollPane; private final JLabel headingWarning = new JLabel(); private final ChangeListener headingRoleChangeListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { Map<String, Role> roleByHeading = headingRoleTableModel.getRoleByHeading(); Either<String, Set<String>> either = checkValidity(roleByHeading); if (either.isLeft()) { headingWarning.setText(either.left()); acceptAction.setEnabled(false); } else { headingWarning.setText(""); acceptAction.setEnabled(true); } } }; private final JCheckBox useScrollBarOption = new JCheckBox("Use Scrollbar"); private final FileDrop fileDrop = new FileDrop() { @Override public void dropFiles(Component c, List<File> list, DropLocationInfo dli) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { setFile(list.get(0)); } }); } }; private FileListTransferHandler flth = new FileListTransferHandler(fileDrop); private final DefaultBackgroundRunner backgroundRunner = new DefaultBackgroundRunner(); private final PromptTextField normalEntryNameField = new PromptTextField("provide name for non-Check Entries", 40); private final JCheckBox saveForFuture = new JCheckBox("Save for future"); private final JTextArea errorMessage = new JTextArea(); private final CardLayout cardLayout = new CardLayout(); private final JPanel cardPanel = new JPanel(cardLayout); private File file; private FileType fileType; //For Excel files private String selectedSheetName; private final Predicate<Role> entryHeadingFilter; public EntryFileImportDialog(Window owner, String title, File inputFile, Predicate<Role> entryHeadingFilter) { super(owner, title, ModalityType.APPLICATION_MODAL); this.entryHeadingFilter = entryHeadingFilter; setDefaultCloseOperation(DISPOSE_ON_CLOSE); setGlassPane(backgroundRunner.getBlockingPane()); useScrollBarOption.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateDataPreviewScrolling(); } }); headingRoleTableModel = createHeadingRoleTableModel(); headingRoleTable = new HeadingRoleTable<>(headingRoleTableModel); headingTableScrollPane = new JScrollPane(headingRoleTable); GuiUtil.setVisibleRowCount(headingRoleTable, 10); JPanel roleAssignmentPanel = new JPanel(new BorderLayout()); roleAssignmentPanel.add(headingTableScrollPane, BorderLayout.CENTER); headingRoleTable.setTransferHandler(flth); headingRoleTableModel.addChangeListener(headingRoleChangeListener); GuiUtil.setVisibleRowCount(dataPreviewTable, 10); dataPreviewTable.setTransferHandler(flth); dataPreviewScrollPane.setTransferHandler(flth); updateDataPreviewScrolling(); dataPreviewScrollPane.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { // boolean useScrollBar = useScrollBarOption.isSelected(); GuiUtil.initialiseTableColumnWidths(dataPreviewTable, true); } }); Box top = Box.createHorizontalBox(); top.add(new JLabel("# rows to preview: ")); top.add(new JSpinner(previewRowCountSpinnerModel)); top.add(Box.createHorizontalGlue()); top.add(useScrollBarOption); JPanel dataPreviewPanel = new JPanel(new BorderLayout()); dataPreviewPanel.add(GuiUtil.createLabelSeparator("Data Preview", top), BorderLayout.NORTH); dataPreviewPanel.add(dataPreviewScrollPane, BorderLayout.CENTER); headingWarning.setForeground(Color.RED); JLabel instructions = new JLabel("<HTML>Please assign a <i>Role</i> for each of the headings in your data" + "<br>You must specify one as the <i>Entry Name</i>." + "<br>Click on one of the <i>Role</i> cells and select from the dropdown" + "<br>To assign multiple headings, select the rows for which you wish" + "<br>to set the <i>Role</i> then right-click and choose from the dropdown."); instructions.setHorizontalAlignment(JLabel.CENTER); instructions.setBackground(Toast.PALE_YELLOW); JScrollPane instScroll = new JScrollPane(instructions); instScroll.setBackground(Toast.PALE_YELLOW); normalEntryNameField.getDocument() .addDocumentListener(new DocumentChangeListener((e) -> updateAcceptButton())); normalEntryNameField.setText(TrialDesignPreferences.getInstance().getNormalEntryTypeName()); JPanel rolesPanel = new JPanel(); GBH gbh = new GBH(rolesPanel, 2, 2, 0, 0); int y = 0; gbh.add(0, y, 1, 1, GBH.NONE, 0, 1, GBH.EAST, "Entry Type Name for non-Checks:"); gbh.add(1, y, 1, 1, GBH.NONE, 1, 1, GBH.WEST, normalEntryNameField); gbh.add(2, y, 1, 1, GBH.NONE, 1, 1, GBH.WEST, saveForFuture); ++y; gbh.add(0, y, 3, 1, GBH.BOTH, 2, 1, GBH.CENTER, roleAssignmentPanel); ++y; JSplitPane headingsAndInstructions = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, rolesPanel, instScroll); JPanel headingPanel = new JPanel(new BorderLayout()); headingPanel.add(GuiUtil.createLabelSeparator("Assign Roles for Headings"), BorderLayout.NORTH); headingPanel.add(headingsAndInstructions, BorderLayout.CENTER); headingPanel.add(headingWarning, BorderLayout.SOUTH); errorMessage.setEditable(false); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, dataPreviewPanel, headingPanel); splitPane.setResizeWeight(0.5); cardPanel.add(new JScrollPane(errorMessage), CARD_ERROR); cardPanel.add(splitPane, CARD_DATA); Box bot = Box.createHorizontalBox(); bot.add(Box.createHorizontalGlue()); bot.add(new JButton(cancelAction)); bot.add(new JButton(acceptAction)); acceptAction.setEnabled(false); Container cp = getContentPane(); cp.add(cardPanel, BorderLayout.CENTER); cp.add(bot, BorderLayout.SOUTH); pack(); sheetNamesComboBox.addActionListener(sheetNamesActionListener); Timer timer = new Timer(true); previewRowCountSpinnerModel.addChangeListener(new ChangeListener() { int nPreview; TimerTask timerTask = null; @Override public void stateChanged(ChangeEvent e) { nPreview = previewRowCountSpinnerModel.getNumber().intValue(); if (timerTask == null) { timerTask = new TimerTask() { int lastPreviewCount = nPreview; @Override public void run() { if (lastPreviewCount == nPreview) { System.err.println("Stable at " + lastPreviewCount); // No change, do it now cancel(); try { updateDataPreview(lastPreviewCount); } finally { timerTask = null; } } else { System.err.println("Changing from " + lastPreviewCount + " to " + nPreview); lastPreviewCount = nPreview; } } }; timer.scheduleAtFixedRate(timerTask, 500, 100); } } }); sheetNamesComboBox.setVisible(false); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { removeWindowListener(this); setFile(inputFile); } }); } public String getNormalEntryName() { return normalEntryNameField.getText().trim(); } private void updateAcceptButton() { acceptAction.setEnabled(!normalEntryNameField.getText().trim().isEmpty()); } private void updateDataPreviewScrolling() { boolean useScrollBar = useScrollBarOption.isSelected(); dataPreviewTable.setAutoResizeMode(useScrollBar ? JTable.AUTO_RESIZE_OFF : JTable.AUTO_RESIZE_ALL_COLUMNS); if (!useScrollBar) { // System.out.println("initTableCOlumns: useScrollBar=" + useScrollBar); GuiUtil.initialiseTableColumnWidths(dataPreviewTable, true); } } private void setFile(File f) { setTitle(f.getName()); dataPreviewTableModel.clear(); this.file = f; if (Shared.NEW_EXCEL_FILE_FILTER.accept(file)) { fileType = FileType.EXCEL; } else if (Shared.CSV_FILE_FILTER.accept(file) || Shared.TXT_FILE_FILTER.accept(file)) { fileType = FileType.CSV_OR_TXT; } else { fileType = FileType.OTHER; } dataPreviewTableModel.setUseLettersForColumnNames(FileType.EXCEL == fileType); updateDataPreview(previewRowCountSpinnerModel.getNumber().intValue()); } private void updateDataPreview(int nPreview) { UpdateDataPreviewTask task = new UpdateDataPreviewTask(nPreview); backgroundRunner.runBackgroundTask(task); } class UpdateDataPreviewTask extends BackgroundTask<Pair<String, Exception>, Void> { private int nPreviewRows; private String[] headingRow; private List<String[]> previewRows = new ArrayList<>(); private List<String> sheetNames; UpdateDataPreviewTask(int nPreviewRows) { super("Loading...", false); this.nPreviewRows = nPreviewRows; } @Override public Pair<String, Exception> generateResult(Closure<Void> arg0) throws Exception { switch (fileType) { case CSV_OR_TXT: try { rowDataProvider = new CsvRowDataProvider(file); } catch (IOException e) { return new Pair<>("Error reading CSV/Text", e); } break; case EXCEL: try { List<String> sheetNames = loadExcelFile(); if (sheetNames.isEmpty()) { } else { selectedSheetName = sheetNames.get(0); try { rowDataProvider = new ExcelRowDataProvider(file, selectedSheetName); } catch (IOException e) { return new Pair<>("Error reading Excel", e); } } } catch (IOException e) { return new Pair<>("Unable to read Excel file", e); } break; default: throw new IllegalStateException("fileType=" + fileType); } Optional<String[]> opt = rowDataProvider.getHeadings(); if (opt.isPresent()) { headingRow = opt.get(); try { for (int i = nPreviewRows; --i >= 0;) { String[] rd = rowDataProvider.getNextRowData(); if (rd == null) { break; } previewRows.add(rd); } } catch (IOException e) { return new Pair<>("Problem reading preview data", e); } } return null; } @Override public void onCancel(CancellationException ignore) { } @Override public void onException(Throwable e) { initUIwithException("Problem Initialising", e); } @Override public void onTaskComplete(Pair<String, Exception> pair) { dataPreviewTableModel.clear(); headingRoleTableModel.setHeadingsAndData(null, null); if (pair == null) { if (previewRows.isEmpty()) { // no first data row headingRoleTableModel.setHeadingsAndData(headingRow, null); errorMessage.setText("No Data Rows"); cardLayout.show(cardPanel, CARD_ERROR); } else { if (!Check.isEmpty(sheetNames)) { initialisingSheetNames = true; try { sheetNamesModel.removeAllElements(); for (String s : sheetNames) { sheetNamesModel.addElement(s); } sheetNamesComboBox.setSelectedIndex(0); } finally { initialisingSheetNames = false; } } headingRoleTableModel.setHeadingsAndData(headingRow, previewRows.get(0)); if (entryHeadingFilter != null) { Optional<Integer> optHeadingIndex = headingRoleTableModel .getHeadingIndex(entryHeadingFilter); if (optHeadingIndex.isPresent()) { int headingIndex = optHeadingIndex.get(); List<Pair<String, Integer>> countsByValue = previewRows.stream() .filter(fields -> fields.length >= headingIndex) .map(fields -> fields[headingIndex]) .collect(Collectors.groupingBy(Function.identity())).entrySet().stream() .map(e -> new Pair<>(e.getKey(), e.getValue().size())) .collect(Collectors.toList()); // descending if (!countsByValue.isEmpty()) { Collections.sort(countsByValue, new Comparator<Pair<String, Integer>>() { @Override public int compare(Pair<String, Integer> o1, Pair<String, Integer> o2) { return o2.second.compareTo(o1.second); } }); // Assume the most common one is "non-check" String probablyNormalEntryName = countsByValue.get(0).first; if (!probablyNormalEntryName.isEmpty()) { normalEntryNameField.setText(probablyNormalEntryName); } } } } previewRows.add(0, headingRow); dataPreviewTableModel.setData(previewRows); GuiUtil.initialiseTableColumnWidths(dataPreviewTable); cardLayout.show(cardPanel, CARD_DATA); } } else if (pair.second == null) { errorMessage.setText(pair.first); cardLayout.show(cardPanel, CARD_ERROR); } else { initUIwithException(pair.first, pair.second); } } } private void initUIwithException(String hdg, Throwable e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); pw.println(hdg); e.printStackTrace(pw); pw.close(); errorMessage.setText(sw.toString()); cardLayout.show(cardPanel, CARD_ERROR); } private List<String> loadExcelFile() throws IOException { List<String> sheetNames = new ArrayList<>(); for (SheetInfo si : ExcelUtil.getSheetInfoForAllSheets(file)) { sheetNames.add(si.name); } return sheetNames; } abstract protected Either<String, Set<String>> checkValidity(Map<String, Role> rbh); abstract protected HeadingRoleTableModel<Role> createHeadingRoleTableModel(); abstract protected EFile createEntryFile(RowDataProvider rdp, Map<String, Role> roleByHeading) throws IOException, EntryFileException; public boolean isSaveForFutureChecked() { return saveForFuture.isSelected(); } }