/*
* Commons eID Project.
* Copyright (C) 2008-2013 FedICT.
* Copyright (C) 2015 e-Contract.be BVBA.
*
* 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/.
*/
/**
* A CardAndTerminalManager connects to the CardTerminal PCSC subsystem, and maintains
* state information on any CardTerminals attached and cards inserted
* Register a CardEventsListener to get callbacks for reader attach/detach
* and card insert/removal events.
*
* @author Frank Marien
*
*/
package be.fedict.commons.eid.client;
import java.util.HashSet;
import java.util.Set;
import javax.smartcardio.Card;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.CardTerminals.State;
import javax.smartcardio.TerminalFactory;
import be.fedict.commons.eid.client.event.CardEventsListener;
import be.fedict.commons.eid.client.event.CardTerminalEventsListener;
import be.fedict.commons.eid.client.impl.LibJ2PCSCGNULinuxFix;
import be.fedict.commons.eid.client.impl.VoidLogger;
import be.fedict.commons.eid.client.spi.Logger;
/**
* A CardAndTerminalManager maintains an active state overview of all
* javax.smartcardio.CardTerminal attached to a system's pcsc subsystem, and
* notifies registered:
* <ul>
* <li>CardTerminalEventsListeners of any CardTerminals Attached or Detached
* <li>CardEventsListeners of any Cards inserted into or removed from any
* attached CardTerminals
* </ul>
* Note that at the level of CardAndTerminalManager there is no distinction
* between types of cards or terminals: They are merely reported using the
* standard javax.smartcardio classes.
*
* @author Frank Marien
*
*/
public class CardAndTerminalManager implements Runnable {
private static final int DEFAULT_DELAY = 250;
private boolean running, subSystemInitialized, autoconnect;
private Thread worker;
private Set<CardTerminal> terminalsPresent, terminalsWithCards;
private CardTerminals cardTerminals;
private final Set<String> terminalsToIgnoreCardEventsFor;
private final Set<CardTerminalEventsListener> cardTerminalEventsListeners;
private final Set<CardEventsListener> cardEventsListeners;
private int delay;
private Logger logger;
private PROTOCOL protocol;
public enum PROTOCOL {
T0("T=0"),
T1("T=1"),
TCL("T=CL"),
ANY("*");
private final String protocol;
PROTOCOL(final String protocol) {
this.protocol = protocol;
}
String getProtocol() {
return this.protocol;
}
}
// ----- various constructors ------
/**
* Instantiate a CardAndTerminalManager working on the standard smartcardio
* CardTerminals, and without any logging.
*/
public CardAndTerminalManager() {
this(new VoidLogger());
}
/**
* Instantiate a CardAndTerminalManager working on the standard smartcardio
* CardTerminals, and logging to the Logger implementation given.
*
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger that will be
* send all the logs
*/
public CardAndTerminalManager(final Logger logger) {
this(logger, null);
}
/**
* Instantiate a CardAndTerminalManager working on a specific CardTerminals
* instance and without any logging. In normal operation, you would use the
* constructor that takes no CardTerminals parameter, but using this one you
* could, for example obtain a CardTerminals instance from a different
* TerminalFactory, or from your own implementation.
*
* @param cardTerminals
* instance to obtain terminal and card events from
*/
public CardAndTerminalManager(final CardTerminals cardTerminals) {
this(new VoidLogger(), cardTerminals);
}
/**
* Instantiate a CardAndTerminalManager working on a specific CardTerminals
* instance, and that logs to the given Logger.In normal operation, you
* would use the constructor that takes no CardTerminals parameter, but
* using this one you could, for example obtain a CardTerminals instance
* from a different TerminalFactory, or from your own implementation.
*
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger that will be
* send all the logs
* @param cardTerminals
* instance to obtain terminal and card events from
*/
public CardAndTerminalManager(final Logger logger,
final CardTerminals cardTerminals) {
// work around implementation bug in some GNU/Linux JRE's that causes
// libpcsc not to be found.
LibJ2PCSCGNULinuxFix.fixNativeLibrary(logger);
this.cardTerminalEventsListeners = new HashSet<CardTerminalEventsListener>();
this.cardEventsListeners = new HashSet<CardEventsListener>();
this.terminalsToIgnoreCardEventsFor = new HashSet<String>();
this.delay = DEFAULT_DELAY;
this.logger = logger;
this.running = false;
this.subSystemInitialized = false;
this.autoconnect = true;
this.protocol = PROTOCOL.ANY;
if (cardTerminals == null) {
final TerminalFactory terminalFactory = TerminalFactory
.getDefault();
this.cardTerminals = terminalFactory.terminals();
} else {
this.cardTerminals = cardTerminals;
}
}
// --------------------------------------------------------------------------------------------------
/**
* Register a CardTerminalEventsListener instance. This will subsequently be
* called for any Terminal Attaches/Detaches on CardTerminals that we're not
* ignoring
*
* @see #ignoreCardEventsFor(String)
* @param listener
* the CardTerminalEventsListener to be registered
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager addCardTerminalListener(
final CardTerminalEventsListener listener) {
synchronized (this.cardTerminalEventsListeners) {
this.cardTerminalEventsListeners.add(listener);
}
return this;
}
/**
* Register a CardEventsListener instance. This will subsequently be called
* for any Card Inserts/Removals on CardTerminals that we're not ignoring
*
* @see #ignoreCardEventsFor(String)
* @param listener
* the CardEventsListener to be registered
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager addCardListener(
final CardEventsListener listener) {
synchronized (this.cardEventsListeners) {
this.cardEventsListeners.add(listener);
}
return this;
}
// --------------------------------------------------------------------------------------------------
/**
* Start this CardAndTerminalManager. Doing this after registering one or
* more CardTerminalEventsListener and/or CardEventsListener instances will
* cause these be be called with the initial situation: The terminals and
* cards already present. Calling start() before registering any listeners
* will cause these to not see the initial situation.
*
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager start() {
this.logger
.debug("CardAndTerminalManager worker thread start requested.");
if (null != this.worker) {
throw new IllegalStateException("already started");
}
this.worker = new Thread(this, "CardAndTerminalManager");
this.worker.setDaemon(true);
this.worker.start();
return this;
}
// --------------------------------------------------------------------------------------------------
/**
* Unregister a CardTerminalEventsListener instance.
*
* @param listener
* the CardTerminalEventsListener to be unregistered
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager removeCardTerminalListener(
final CardTerminalEventsListener listener) {
synchronized (this.cardTerminalEventsListeners) {
this.cardTerminalEventsListeners.remove(listener);
}
return this;
}
/**
* Unregister a CardEventsListener instance.
*
* @param listener
* the CardEventsListener to be unregistered
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager removeCardListener(
final CardEventsListener listener) {
synchronized (this.cardEventsListeners) {
this.cardEventsListeners.remove(listener);
}
return this;
}
// -----------------------------------------------------------------------
/**
* Start ignoring the CardTerminal with the name given. A CardTerminal's
* name is the exact String as returned by
* {@link javax.smartcardio.CardTerminal#getName() CardTerminal.getName()}
* Note that this name is neither very stable, nor portable between
* operating systems: it is constructed by the PCSC subsystem in an
* arbitrary fashion, and may change between releases.
*
* @param terminalName
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager ignoreCardEventsFor(final String terminalName) {
synchronized (this.terminalsToIgnoreCardEventsFor) {
this.terminalsToIgnoreCardEventsFor.add(terminalName);
}
return this;
}
/**
* Start accepting events for the CardTerminal with the name given, where
* these were being ignored due to a previous call to
* {@link #ignoreCardEventsFor(String)}.
*
* @param terminalName
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager acceptCardEventsFor(final String terminalName) {
synchronized (this.terminalsToIgnoreCardEventsFor) {
this.terminalsToIgnoreCardEventsFor.remove(terminalName);
}
return this;
}
// -----------------------------------------------------------------------
/**
* Stop this CardAndTerminalManager. This will may block until the worker
* thread has returned, meaning that after this call returns, no registered
* listeners will receive any more events.
*
* @return this CardAndTerminalManager to allow for method chaining.
* @throws InterruptedException
*/
public CardAndTerminalManager stop() throws InterruptedException {
this.logger
.debug("CardAndTerminalManager worker thread stop requested.");
this.running = false;
this.worker.interrupt();
this.worker.join();
this.worker = null;
return this;
}
/**
* Returns the PCSC polling delay currently in use
*
* @return the PCSC polling delay currently in use
*/
public int getDelay() {
return this.delay;
}
/**
* Set the PCSC polling delay. A CardAndTerminalsManager will wait for a
* maximum of newDelay milliseconds for new events to be received, before
* issuing a new call to the PCSC subsystem. The higher this number, the
* less CPU this CardAndTerminalsManager will take, but the greater the
* chance that terminal attach/detach events will be noticed late.
*
* @param newDelay
* the new delay to trust the PCSC subsystem for
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager setDelay(final int newDelay) {
this.delay = newDelay;
return this;
}
/**
* Return whether this CardAndTerminalsManager will automatically connect()
* to any cards inserted.
*
* @return this CardAndTerminalManager to allow for method chaining.
*/
public boolean isAutoconnect() {
return this.autoconnect;
}
/**
* Set whether this CardAndTerminalsManager will automatically connect() to
* any cards inserted.
*
* @param newAutoConnect
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager setAutoconnect(final boolean newAutoConnect) {
this.autoconnect = newAutoConnect;
return this;
}
/**
* return which card protocols this CardAndTerminalsManager will attempt to
* connect to cards with. (if autoconnect is true, see
* {@link CardAndTerminalManager#setAutoconnect(boolean)}) the default is
* PROTOCOL.ANY which allows any protocol.
*
* @return the currently attempted protocol(s)
*/
public PROTOCOL getProtocol() {
return this.protocol;
}
/**
* Determines which card protocols this CardAndTerminalsManager will attempt
* to connect to cards with. (if autoconnect is true, see
* {@link CardAndTerminalManager#setAutoconnect(boolean)}) the default is
* PROTOCOL.ANY which allows any protocol.
*
* @param newProtocol
* the card protocol(s) to attempt connection to the cards with
* @return this CardAndTerminalManager to allow for method chaining.
*/
public CardAndTerminalManager setProtocol(final PROTOCOL newProtocol) {
this.protocol = newProtocol;
return this;
}
// ---------------------------
// Private Implementation..
// ---------------------------
@Override
public void run() {
this.running = true;
this.logger.debug("CardAndTerminalManager worker thread started.");
try {
// do an initial run, making sure current status is detected
// this sends terminal attach and card insert events for this
// initial state to any listeners
handlePCSCEvents();
// advise listeners that initial state was sent, and that any
// further events are relative to this
listenersInitialized();
// keep updating
while (this.running) {
handlePCSCEvents();
}
} catch (final InterruptedException iex) {
if (this.running) {
this.logger
.error("CardAndTerminalManager worker thread unexpectedly interrupted: "
+ iex.getLocalizedMessage());
}
}
this.logger.debug("CardAndTerminalManager worker thread ended.");
}
private void handlePCSCEvents() throws InterruptedException {
if (!this.subSystemInitialized) {
this.logger.debug("subsystem not initialized");
try {
if (this.terminalsPresent == null
|| this.terminalsWithCards == null) {
this.terminalsPresent = new HashSet<CardTerminal>(
this.cardTerminals.list(State.ALL));
this.terminalsWithCards = terminalsWithCardsIn(this.terminalsPresent);
}
listenersTerminalsAttachedCardsInserted(this.terminalsPresent,
this.terminalsWithCards);
this.subSystemInitialized = true;
} catch (final CardException cex) {
logCardException(cex,
"Cannot enumerate card terminals [1] (No Card Readers Connected?)");
clear();
sleepForDelay();
return;
}
}
try {
// can't use waitForChange properly, that is in blocking mode,
// without delay argument,
// since it sometimes misses reader attach events.. (TODO: test on
// other platforms)
// this limits us to what is basically a polling strategy, with a
// small speed
// gain where waitForChange *does* detect events (because it will
// return faster than delay)
// for most events this will make reaction instantaneous, and worst
// case = delay
this.cardTerminals.waitForChange(this.delay);
} catch (final CardException cex) {
// waitForChange fails (e.g. PCSC is there but no readers)
logCardException(cex,
"Cannot wait for card terminal events [2] (No Card Readers Connected?)");
clear();
sleepForDelay();
return;
} catch (final IllegalStateException ise) {
// waitForChange fails (e.g. PCSC is not there)
this.logger
.debug("Cannot wait for card terminal changes (no PCSC subsystem?): "
+ ise.getLocalizedMessage());
clear();
sleepForDelay();
return;
}
// get here when event has occured or delay time has passed
try {
// get fresh state
final Set<CardTerminal> currentTerminals = new HashSet<CardTerminal>(
this.cardTerminals.list(State.ALL));
final Set<CardTerminal> currentTerminalsWithCards = terminalsWithCardsIn(currentTerminals);
// determine terminals that were attached since previous state
final Set<CardTerminal> terminalsAttached = new HashSet<CardTerminal>(
currentTerminals);
terminalsAttached.removeAll(this.terminalsPresent);
// determine terminals that had cards inserted since previous state
final Set<CardTerminal> terminalsWithCardsInserted = new HashSet<CardTerminal>(
currentTerminalsWithCards);
terminalsWithCardsInserted.removeAll(this.terminalsWithCards);
// determine terminals that had cards removed since previous state
final Set<CardTerminal> terminalsWithCardsRemoved = new HashSet<CardTerminal>(
this.terminalsWithCards);
terminalsWithCardsRemoved.removeAll(currentTerminalsWithCards);
// determine terminals detached since previous state
final Set<CardTerminal> terminalsDetached = new HashSet<CardTerminal>(
this.terminalsPresent);
terminalsDetached.removeAll(currentTerminals);
// keep fresh state to compare to next time (and to return to
// synchronous callers)
this.terminalsPresent = currentTerminals;
this.terminalsWithCards = currentTerminalsWithCards;
// advise the listeners where appropriate, always in the order
// attach, insert, remove, detach
listenersUpdateInSequence(terminalsAttached,
terminalsWithCardsInserted, terminalsWithCardsRemoved,
terminalsDetached);
} catch (final CardException cex) {
// if a CardException occurs, assume we're out of readers (only
// CardTerminals.list throws that here)
// CardTerminal fails in that case, instead of simply seeing zero
// CardTerminals.
logCardException(cex,
"Cannot wait for card terminal changes (no PCSC subsystem?)");
clear();
sleepForDelay();
}
}
// ---------------------------------------------------------------------------------------------------
private boolean areCardEventsIgnoredFor(final CardTerminal cardTerminal) {
synchronized (this.terminalsToIgnoreCardEventsFor) {
for (String prefixToMatch : this.terminalsToIgnoreCardEventsFor) {
if (cardTerminal.getName().startsWith(prefixToMatch)) {
return true;
}
}
}
return false;
}
private Set<CardTerminal> terminalsWithCardsIn(
final Set<CardTerminal> terminals) {
final Set<CardTerminal> terminalsWithCards = new HashSet<CardTerminal>();
synchronized (this.terminalsToIgnoreCardEventsFor) {
for (CardTerminal terminal : terminals) {
try {
if (terminal.isCardPresent()
&& !this.areCardEventsIgnoredFor(terminal)) {
terminalsWithCards.add(terminal);
}
} catch (final CardException cex) {
this.logger
.error("Problem determining card presence in terminal ["
+ terminal.getName() + "]");
}
}
}
return terminalsWithCards;
}
// -------------------------------------------------
// --------- private convenience methods -----------
// -------------------------------------------------
// return to the uninitialized state
private void clear() {
// if we were already initialized, we may have sent attached and insert
// events we now pretend to remove and detach all that we know of, for
// consistency
if (this.subSystemInitialized) {
listenersCardsRemovedTerminalsDetached(this.terminalsWithCards,
this.terminalsPresent);
}
this.terminalsPresent = null;
this.terminalsWithCards = null;
this.subSystemInitialized = false;
this.logger.debug("cleared");
}
private void listenersTerminalsAttachedCardsInserted(
final Set<CardTerminal> attached, final Set<CardTerminal> inserted)
throws CardException {
listenersTerminalsAttached(attached);
listenersTerminalsWithCardsInserted(inserted);
}
private void listenersCardsRemovedTerminalsDetached(
final Set<CardTerminal> removed, final Set<CardTerminal> detached) {
listenersTerminalsWithCardsRemoved(removed);
listenersTerminalsDetached(detached);
}
private void listenersUpdateInSequence(final Set<CardTerminal> attached,
final Set<CardTerminal> inserted, final Set<CardTerminal> removed,
final Set<CardTerminal> detached) throws CardException {
listenersTerminalsAttached(attached);
listenersTerminalsWithCardsInserted(inserted);
listenersTerminalsWithCardsRemoved(removed);
listenersTerminalsDetached(detached);
}
private void listenersInitialized() {
listenersTerminalEventsInitialized();
listenersCardEventsInitialized();
}
private void listenersCardEventsInitialized() {
Set<CardEventsListener> copyOfListeners;
synchronized (this.cardEventsListeners) {
copyOfListeners = new HashSet<CardEventsListener>(
this.cardEventsListeners);
}
for (CardEventsListener listener : copyOfListeners) {
try {
listener.cardEventsInitialized();
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardEventsListener.cardRemoved:"
+ thrownInListener.getMessage());
}
}
}
private void listenersTerminalEventsInitialized() {
Set<CardTerminalEventsListener> copyOfListeners;
synchronized (this.cardTerminalEventsListeners) {
copyOfListeners = new HashSet<CardTerminalEventsListener>(
this.cardTerminalEventsListeners);
}
for (CardTerminalEventsListener listener : copyOfListeners) {
try {
listener.terminalEventsInitialized();
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardTerminalEventsListener.terminalAttached:"
+ thrownInListener.getMessage());
}
}
}
// Tell listeners about attached readers
private void listenersTerminalsAttached(final Set<CardTerminal> attached) {
if (!attached.isEmpty()) {
Set<CardTerminalEventsListener> copyOfListeners;
synchronized (this.cardTerminalEventsListeners) {
copyOfListeners = new HashSet<CardTerminalEventsListener>(
this.cardTerminalEventsListeners);
}
for (CardTerminal terminal : attached) {
for (CardTerminalEventsListener listener : copyOfListeners) {
try {
listener.terminalAttached(terminal);
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardTerminalEventsListener.terminalAttached:"
+ thrownInListener.getMessage());
}
}
}
}
}
// Tell listeners about detached readers
private void listenersTerminalsDetached(final Set<CardTerminal> detached) {
if (!detached.isEmpty()) {
Set<CardTerminalEventsListener> copyOfListeners;
synchronized (this.cardTerminalEventsListeners) {
copyOfListeners = new HashSet<CardTerminalEventsListener>(
this.cardTerminalEventsListeners);
}
for (CardTerminal terminal : detached) {
for (CardTerminalEventsListener listener : copyOfListeners) {
try {
listener.terminalDetached(terminal);
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardTerminalEventsListener.terminalDetached:"
+ thrownInListener.getMessage());
}
}
}
}
}
// Tell listeners about removed cards
private void listenersTerminalsWithCardsRemoved(
final Set<CardTerminal> removed) {
if (!removed.isEmpty()) {
Set<CardEventsListener> copyOfListeners;
synchronized (this.cardEventsListeners) {
copyOfListeners = new HashSet<CardEventsListener>(
this.cardEventsListeners);
}
for (CardTerminal terminal : removed) {
for (CardEventsListener listener : copyOfListeners) {
try {
listener.cardRemoved(terminal);
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardEventsListener.cardRemoved:"
+ thrownInListener.getMessage());
}
}
}
}
}
// Tell listeners about inserted cards. giving them the CardTerminal and a
// Card object
// if this.autoconnect is enabled (the default), the card argument may be
// automatically
// filled out, but it may still be null, if the connect failed.
private void listenersTerminalsWithCardsInserted(
final Set<CardTerminal> inserted) {
if (!inserted.isEmpty()) {
Set<CardEventsListener> copyOfListeners;
synchronized (this.cardEventsListeners) {
copyOfListeners = new HashSet<CardEventsListener>(
this.cardEventsListeners);
}
for (CardTerminal terminal : inserted) {
Card card = null;
if (this.autoconnect) {
try {
card = terminal.connect(this.protocol.getProtocol());
} catch (final CardException cex) {
this.logger.debug("terminal.connect("
+ this.protocol.getProtocol() + ") failed. "
+ cex.getMessage());
}
}
for (CardEventsListener listener : copyOfListeners) {
try {
listener.cardInserted(terminal, card);
} catch (final Exception thrownInListener) {
this.logger
.error("Exception thrown in CardEventsListener.cardInserted:"
+ thrownInListener.getMessage());
}
}
}
}
}
private void sleepForDelay() throws InterruptedException {
Thread.sleep(this.delay);
}
private void logCardException(final CardException cex, final String where) {
this.logger.debug(where + ": " + cex.getMessage());
this.logger.debug("no card readers connected?");
final Throwable cause = cex.getCause();
if (cause == null) {
return;
}
this.logger.debug("cause: " + cause.getMessage());
this.logger.debug("cause type: " + cause.getClass().getName());
}
}