/* * Commons eID Project. * Copyright (C) 2008-2013 FedICT. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version * 3.0 as published by the Free Software Foundation. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, see * http://www.gnu.org/licenses/. */ package be.fedict.commons.eid.dialogs; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Toolkit; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.lang.reflect.InvocationTargetException; import java.text.DateFormat; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.ImageIcon; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.ListCellRenderer; import javax.swing.SwingUtilities; import be.fedict.commons.eid.client.BeIDCard; import be.fedict.commons.eid.client.FileType; import be.fedict.commons.eid.client.OutOfCardsException; import be.fedict.commons.eid.client.CancelledException; import be.fedict.commons.eid.client.event.BeIDCardListener; import be.fedict.commons.eid.consumer.Identity; import be.fedict.commons.eid.consumer.text.Format; import be.fedict.commons.eid.consumer.tlv.TlvParser; /** * Dynamically changing dialog listing BeIDCards by photo and main identity data * part of the DefaultBeIDCardsUI. Based on the original, static BeID selector * dialog from eid-applet. * * @author Frank Marien * */ public class BeIDSelector { private JDialog dialog; private JPanel masterPanel; private DefaultListModel listModel; private JList list; private final Component parentComponent; private final ListData selectedListData; private final Map<BeIDCard, ListDataUpdater> updaters; private int identitiesbeingRead; private boolean outOfCards; public BeIDSelector(final Component parentComponent, final String title, final Collection<BeIDCard> initialCards) { this.parentComponent = parentComponent; this.selectedListData = new ListData(null); this.updaters = new HashMap<BeIDCard, ListDataUpdater>(); this.identitiesbeingRead = 0; this.outOfCards = false; initComponents(title, initialCards); for (BeIDCard card : initialCards) { addEIDCard(card); } MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(final MouseEvent mouseEvent) { final JList theList = (JList) mouseEvent.getSource(); if (mouseEvent.getClickCount() == 2) { final int index = theList.locationToIndex(mouseEvent .getPoint()); if (index >= 0) { stop(); final Object object = theList.getModel().getElementAt( index); final ListData listData = (ListData) object; BeIDSelector.this.selectedListData.card = listData .getCard(); BeIDSelector.this.dialog.dispose(); } } } }; this.list.addMouseListener(mouseListener); } public void addEIDCard(final BeIDCard card) { final ListData listData = new ListData(card); this.addToList(listData); final ListDataUpdater listDataUpdater = new ListDataUpdater(this, listData); this.updaters.put(card, listDataUpdater); listDataUpdater.start(); } public void removeEIDCard(final BeIDCard card) { final ListDataUpdater listDataUpdater = this.updaters.get(card); listDataUpdater.stop(); this.updaters.remove(card); this.removeFromList(listDataUpdater.getListData()); } public synchronized void startReadingIdentity() { this.identitiesbeingRead++; notifyAll(); } public synchronized void endReadingIdentity() { this.identitiesbeingRead--; this.repack(); notifyAll(); } public synchronized void waitUntilIdentitiesRead() { try { while (this.identitiesbeingRead > 0) { this.wait(); } } catch (final InterruptedException iex) { return; } } public void stop() { for (ListDataUpdater updater : this.updaters.values()) { updater.stop(); } for (ListDataUpdater updater : this.updaters.values()) { try { updater.join(); } catch (final InterruptedException iex) { return; } } } public BeIDCard choose() throws OutOfCardsException, CancelledException { this.waitUntilIdentitiesRead(); if (this.parentComponent != null) { this.dialog.setLocationRelativeTo(this.parentComponent); } else { final Dimension screen = Toolkit.getDefaultToolkit() .getScreenSize(); this.dialog.setLocation( (screen.width - this.dialog.getSize().width) / 2, (screen.height - this.dialog.getSize().height) / 2); } this.dialog.setResizable(false); this.dialog.setVisible(true); // dialog is modal so setVisible will block until dispose is called. // mouseListener calls dispose after setting selection, on double-click // removeFromList calls dispose after setting outOfCards when last card // removed // user closing dialog will have no selection and outOfCards not set // indicating cancel if (this.outOfCards) { throw new OutOfCardsException(); } if (this.selectedListData.getCard() == null) { throw new CancelledException(); } return this.selectedListData.getCard(); } // ---------------------------------------------------------------------------------------------------- // methods to alter the dialog in a Swing-Thread safe way // ---------------------------------------------------------------------------------------------------- private void initComponents(final String title, final Collection<BeIDCard> initialCards) { try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { BeIDSelector.this.dialog = new JDialog((Frame) null, title, true); BeIDSelector.this.masterPanel = new JPanel( new BorderLayout()); BeIDSelector.this.masterPanel.setBorder(BorderFactory .createEmptyBorder(16, 16, 16, 16)); BeIDSelector.this.listModel = new DefaultListModel(); BeIDSelector.this.list = new JList( BeIDSelector.this.listModel); BeIDSelector.this.list .setCellRenderer(new EidListCellRenderer()); BeIDSelector.this.masterPanel.add(BeIDSelector.this.list); BeIDSelector.this.dialog.add(BeIDSelector.this.masterPanel); } }); } catch (final InterruptedException e) { } catch (final InvocationTargetException e) { } } private synchronized void updateListData( final ListDataUpdater listDataUpdater, final ListData listData) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final int index = BeIDSelector.this.listModel.indexOf(listData); if (index != -1) { BeIDSelector.this.listModel.set(index, listData); } } }); } private void addToList(final ListData listData) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { BeIDSelector.this.listModel.addElement(listData); } }); } private void removeFromList(final ListData listData) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { BeIDSelector.this.listModel.removeElement(listData); if (BeIDSelector.this.listModel.isEmpty()) { BeIDSelector.this.selectedListData.card = null; BeIDSelector.this.outOfCards = true; BeIDSelector.this.dialog.dispose(); } else { BeIDSelector.this.dialog.pack(); } } }); } private void repack() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { BeIDSelector.this.dialog.pack(); } }); } /* * ********************************************************************************************************************** */ private static class ListData { private BeIDCard card; private Identity identity; private ImageIcon photo; private int photoProgress, photoSizeEstimate; private boolean error; public ListData(final BeIDCard card) { super(); this.card = card; } public BeIDCard getCard() { return this.card; } public ImageIcon getPhoto() { return this.photo; } public Identity getIdentity() { return this.identity; } public void setIdentity(final Identity identity) { this.identity = identity; } public void setPhoto(final ImageIcon photo) { this.photo = photo; } public int getPhotoProgress() { return this.photoProgress; } public void setPhotoProgress(final int photoProgress) { this.photoProgress = photoProgress; } public void setPhotoSizeEstimate(final int photoSizeEstimate) { this.photoSizeEstimate = photoSizeEstimate; } public int getPhotoSizeEstimate() { return this.photoSizeEstimate; } public boolean hasError() { return this.error; } public void setError() { this.error = true; } } private static class EidListCellRenderer extends JPanel implements ListCellRenderer { private static final long serialVersionUID = -6914001662919942232L; @Override public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { final JPanel panel = new JPanel(); final ListData listData = (ListData) value; panel.setLayout(new FlowLayout(FlowLayout.LEFT)); if (listData.hasError()) { panel.setBackground(redden(isSelected ? list .getSelectionBackground() : list.getBackground())); } else { panel.setBackground(isSelected ? list.getSelectionBackground() : list.getBackground()); } panel.add(new PhotoPanel(listData.getPhoto(), listData .getPhotoProgress(), listData.getPhotoSizeEstimate())); panel.add(new IdentityPanel(listData.getIdentity())); return panel; } private Color redden(final Color originalColor) { final Color less = originalColor.darker().darker(); final Color more = originalColor.brighter().brighter(); return new Color(more.getRed(), less.getGreen(), less.getBlue()); } } private static class PhotoPanel extends JPanel { private static final long serialVersionUID = -8779658857811406077L; private JProgressBar progressBar; public PhotoPanel(final ImageIcon photo, final int progress, final int max) { super(new GridBagLayout()); setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); Dimension fixedSize = new Dimension(140, 200); setPreferredSize(fixedSize); setMinimumSize(fixedSize); setMaximumSize(fixedSize); if (photo == null) { this.progressBar = new JProgressBar(0, max); this.progressBar.setIndeterminate(false); this.progressBar.setValue(progress); fixedSize = new Dimension(100, 16); this.progressBar.setPreferredSize(fixedSize); this.add(this.progressBar); } else { this.add(new JLabel(photo)); } } } private static class IdentityPanel extends JPanel { private static final long serialVersionUID = 1293396834578252226L; public IdentityPanel(final Identity identity) { super(new GridBagLayout()); setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); setMinimumSize(new Dimension(140, 200)); setOpaque(false); if (identity == null) { this.add(new JLabel("-")); } else { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridy = 0; gbc.anchor = GridBagConstraints.LINE_START; gbc.ipady = 4; add(new JLabel(identity.getName()), gbc); gbc = new GridBagConstraints(); gbc.gridy = 1; gbc.anchor = GridBagConstraints.LINE_START; gbc.ipady = 4; add(new JLabel(identity.getFirstName() + " " + identity.getMiddleName()), gbc); gbc = new GridBagConstraints(); gbc.gridy = 2; gbc.ipady = 8; add(Box.createVerticalGlue(), gbc); gbc = new GridBagConstraints(); gbc.gridy = 3; gbc.anchor = GridBagConstraints.LINE_START; gbc.ipady = 4; final DateFormat dateFormat = DateFormat.getDateInstance( DateFormat.DEFAULT, Locale.getDefault()); add(new JLabel( identity.getPlaceOfBirth() + " " + dateFormat.format(identity.getDateOfBirth() .getTime())), gbc); gbc = new GridBagConstraints(); gbc.gridy = 4; gbc.ipady = 8; add(Box.createVerticalGlue(), gbc); gbc = new GridBagConstraints(); gbc.gridy = 5; gbc.anchor = GridBagConstraints.LINE_START; gbc.ipady = 4; add(new JLabel(identity.getNationality().toUpperCase()), gbc); gbc = new GridBagConstraints(); gbc.gridy = 6; gbc.ipady = 8; add(Box.createVerticalGlue(), gbc); gbc = new GridBagConstraints(); gbc.gridy = 7; gbc.anchor = GridBagConstraints.LINE_START; gbc.ipady = 4; add(new JLabel( Format.formatCardNumber(identity.getCardNumber())), gbc); } } } private static class ListDataUpdater implements Runnable { final private BeIDSelector selectionDialog; final private ListData listData; final private Thread worker; public ListDataUpdater(final BeIDSelector selectionDialog, final ListData listData) { super(); this.selectionDialog = selectionDialog; this.listData = listData; this.worker = new Thread(this, "ListDataUpdater"); this.worker.setDaemon(true); setWorkerName(null, null); this.selectionDialog.startReadingIdentity(); } public void stop() { this.worker.interrupt(); } public void start() { this.worker.start(); } public void join() throws InterruptedException { this.worker.join(); } @Override public void run() { Identity identity = null; setWorkerName(null, "Reading Identity"); try { identity = TlvParser.parse( this.listData.getCard().readFile(FileType.Identity), Identity.class); this.listData.setIdentity(identity); this.selectionDialog.updateListData(this, this.listData); setWorkerName(identity, "Identity Read"); } catch (final Exception ex) { this.listData.setError(); this.selectionDialog.updateListData(this, this.listData); setWorkerName(identity, "Error Reading Identity"); } finally { this.selectionDialog.endReadingIdentity(); } setWorkerName(identity, "Reading Photo"); try { this.listData.setPhotoSizeEstimate(FileType.Photo .getEstimatedMaxSize()); this.selectionDialog.updateListData(this, this.listData); this.listData.getCard().addCardListener(new BeIDCardListener() { @Override public void notifyReadProgress(final FileType fileType, final int offset, final int estimatedMaxSize) { ListDataUpdater.this.listData.setPhotoProgress(offset); ListDataUpdater.this.selectionDialog.updateListData( ListDataUpdater.this, ListDataUpdater.this.listData); } @Override public void notifySigningBegin(final FileType keyType) { // can safely ignore this here } @Override public void notifySigningEnd(final FileType keyType) { // can safely ignore this here } }); final byte[] photoFile = this.listData.getCard().readFile( FileType.Photo); final BufferedImage photoImage = ImageIO .read(new ByteArrayInputStream(photoFile)); this.listData.setPhoto(new ImageIcon(photoImage)); this.selectionDialog.updateListData(this, this.listData); setWorkerName(identity, "All Done"); } catch (final Exception ex) { this.listData.setError(); this.selectionDialog.updateListData(this, this.listData); setWorkerName(identity, "Error Reading Photo"); } } private void setWorkerName(final Identity identity, final String activity) { final StringBuilder builder = new StringBuilder("ListDataUpdater"); if (identity != null) { builder.append(" ["); if (identity.getFirstName() != null) { builder.append(identity.getFirstName()); builder.append(" "); } if (identity.getName() != null) { builder.append(identity.getName()); } builder.append("]"); } if (activity != null) { builder.append(" ["); builder.append(activity); builder.append("]"); } this.worker.setName(builder.toString()); } public ListData getListData() { return this.listData; } } }