/*
* 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.client;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.smartcardio.CardTerminal;
import be.fedict.commons.eid.client.CardAndTerminalManager.PROTOCOL;
import be.fedict.commons.eid.client.event.BeIDCardEventsListener;
import be.fedict.commons.eid.client.event.CardTerminalEventsListener;
import be.fedict.commons.eid.client.impl.LocaleManager;
import be.fedict.commons.eid.client.impl.VoidLogger;
import be.fedict.commons.eid.client.spi.BeIDCardsUI;
import be.fedict.commons.eid.client.spi.Logger;
import be.fedict.commons.eid.client.spi.Sleeper;
/**
* BeIDCards is a synchronous approach to Belgian Identity Cards and their
* presence in the user's system, as opposed to the asynchronous, event-driven
* approach of {@link BeIDCardManager} (but BeIDCards uses an underlying
* {@link BeIDCardManager} to achieve it's goals). It's main purpose is to have
* a very simple way to get a user's BeIDCard instance, abstracting away and
* delegating issues such as terminal connection, card insertion, and handling
* multiple eligible cards.
* <p>
* BeIDCards handle user interaction (if any) through an instance of
* BeIDCardsUI, which can be supplied at construction, or left to the supplied
* default, which will instantiate a
* be.fedict.commons.eid.dialogs.DefaultBeIDCardsUI (which needs to be available
* on the class path)
*
* @author Frank Marien
*
*/
public class BeIDCards {
private static final String UI_MISSING_LOG_MESSAGE = "No BeIDCardsUI set and can't load DefaultBeIDCardsUI";
private static final String DEFAULT_UI_IMPLEMENTATION = "be.fedict.commons.eid.dialogs.DefaultBeIDCardsUI";
private final Logger logger;
private CardAndTerminalManager cardAndTerminalManager;
private BeIDCardManager cardManager;
private boolean terminalsInitialized, cardsInitialized, uiSelectingCard;
private final Map<CardTerminal, BeIDCard> beIDTerminalsAndCards;
private Sleeper terminalManagerInitSleeper, cardTerminalSleeper;
private Sleeper cardManagerInitSleeper, beIDSleeper;
private BeIDCardsUI ui;
private int cardTerminalsAttached;
/**
* a BeIDCards without logging, using the default BeIDCardsUI
*/
public BeIDCards() {
this(new VoidLogger(), null);
}
/**
* a BeIDCards without logging, using the supplied BeIDCardsUI
*
* @param ui
* an instance of be.fedict.commons.eid.client.spi.BeIDCardsUI
* that will be called upon for any user interaction required to
* handle other calls. The UI's Locale will be used globally for
* subsequent UI actions, as if setLocale() was called, except
* where the Locale is explicity set for individual BeIDCard
* instances.
*/
public BeIDCards(final BeIDCardsUI ui) {
this(new VoidLogger(), ui);
}
/**
* a BeIDCards logging to supplied logger, using the default BeIDCardsUI
*
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger that will be
* send all the logs
*/
public BeIDCards(Logger logger) {
this(logger, null);
}
/**
* a BeIDCards logging to logger, using the supplied BeIDCardsUI and locale
*
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger that will be
* send all the logs
* @param ui
* an instance of be.fedict.commons.eid.client.spi.BeIDCardsUI
* that will be called upon for any user interaction required to
* handle other calls. The UI's Locale will be used globally for
* subsequent UI actions, as if setLocale() was called, except
* where the Locale is explicity set for individual BeIDCard
* instances.
*/
public BeIDCards(final Logger logger, final BeIDCardsUI ui) {
this.logger = logger;
this.cardAndTerminalManager = new CardAndTerminalManager(logger);
this.cardAndTerminalManager.setProtocol(PROTOCOL.T0);
this.cardManager = new BeIDCardManager(logger,
this.cardAndTerminalManager);
this.terminalManagerInitSleeper = new Sleeper();
this.cardManagerInitSleeper = new Sleeper();
this.cardTerminalSleeper = new Sleeper();
this.beIDSleeper = new Sleeper();
this.beIDTerminalsAndCards = new HashMap<CardTerminal, BeIDCard>();
this.terminalsInitialized = false;
this.cardsInitialized = false;
this.uiSelectingCard = false;
setUI(ui);
this.cardAndTerminalManager
.addCardTerminalListener(new CardTerminalEventsListener() {
@Override
public void terminalEventsInitialized() {
BeIDCards.this.terminalsInitialized = true;
BeIDCards.this.terminalManagerInitSleeper.awaken();
}
@Override
public void terminalDetached(CardTerminal cardTerminal) {
BeIDCards.this.cardTerminalsAttached--;
BeIDCards.this.cardTerminalSleeper.awaken();
}
@Override
public void terminalAttached(CardTerminal cardTerminal) {
BeIDCards.this.cardTerminalsAttached++;
BeIDCards.this.cardTerminalSleeper.awaken();
}
});
this.cardManager.addBeIDCardEventListener(new BeIDCardEventsListener() {
@Override
public void eIDCardInserted(final CardTerminal cardTerminal,
final BeIDCard card) {
BeIDCards.this.logger.debug("eID Card Insertion Reported");
if (BeIDCards.this.uiSelectingCard) {
try {
BeIDCards.this.getUI().eIDCardInsertedDuringSelection(
card);
} catch (final Exception ex) {
BeIDCards.this.logger
.error("Exception in UI:eIDCardInserted"
+ ex.getMessage());
}
}
synchronized (BeIDCards.this.beIDTerminalsAndCards) {
BeIDCards.this.beIDTerminalsAndCards
.put(cardTerminal, card);
BeIDCards.this.beIDSleeper.awaken();
}
}
@Override
public void eIDCardRemoved(final CardTerminal cardTerminal,
final BeIDCard card) {
BeIDCards.this.logger.debug("eID Card Removal Reported");
if (BeIDCards.this.uiSelectingCard) {
try {
BeIDCards.this.getUI().eIDCardRemovedDuringSelection(
card);
} catch (final Exception ex) {
BeIDCards.this.logger
.error("Exception in UI:eIDCardRemoved"
+ ex.getMessage());
}
}
synchronized (BeIDCards.this.beIDTerminalsAndCards) {
BeIDCards.this.beIDTerminalsAndCards.remove(cardTerminal);
BeIDCards.this.beIDSleeper.awaken();
}
}
@Override
public void eIDCardEventsInitialized() {
BeIDCards.this.logger.debug("eIDCardEventsInitialized");
BeIDCards.this.cardsInitialized = true;
BeIDCards.this.cardManagerInitSleeper.awaken();
}
});
this.cardAndTerminalManager.start();
}
/**
* Return whether any BeID Cards are currently present.
*
* @return true if one or more BeID Cards are inserted in one or more
* connected CardTerminals, false if zero BeID Cards are present
*/
public boolean hasBeIDCards() {
return this.hasBeIDCards(null);
}
/**
* Return whether any BeID Cards are currently present.
*
* @param terminal
* if not null, only this terminal will be considered in
* determining whether beID Cards are present.
*
* @return true if one or more BeID Cards are inserted in one or more
* connected CardTerminals, false if zero BeID Cards are present
*/
public boolean hasBeIDCards(CardTerminal terminal) {
waitUntilCardsInitialized();
boolean has;
synchronized (this.beIDTerminalsAndCards) {
if (terminal != null) {
has = this.beIDTerminalsAndCards.containsKey(terminal);
} else {
has = !this.beIDTerminalsAndCards.isEmpty();
}
}
this.logger.debug("hasBeIDCards returns " + has);
return has;
}
/**
* return Set of all BeID Cards present. Will return empty Set if no BeID
* cards are present at time of call
*
* @return a (possibly empty) set of all BeID Cards inserted at time of call
*/
public Set<BeIDCard> getAllBeIDCards() {
waitUntilCardsInitialized();
synchronized (this.beIDTerminalsAndCards) {
return new HashSet<BeIDCard>(this.beIDTerminalsAndCards.values());
}
}
/**
* return exactly one BeID Card.
*
* This may block when called when no BeID Cards are present, until at least
* one BeID card is inserted, at which point this will be returned. If, at
* time of call, more than one BeID card is present, will request the UI to
* select between those, and return the selected card. If the UI is called
* upon to request the user to select between different cards, or to insert
* one card, and the user declines, CancelledException is thrown.
*
* @return a BeIDCard instance. The only one present, or one chosen out of
* several by the user
* @throws CancelledException
*/
public BeIDCard getOneBeIDCard() throws CancelledException {
return this.getOneBeIDCard(null);
}
/**
* return a BeID Card inserted into a given CardTerminal
*
* @param terminal
* if not null, only BeID Cards in this particular CardTerminal
* will be considered.
*
* May block when called when no BeID Cards are present, until at
* least one BeID card is inserted, at which point this will be
* returned. If, at time of call, more than one BeID card is
* present, will request the UI to select between those, and
* return the selected card. If the UI is called upon to request
* the user to select between different cards, or to insert one
* card, and the user declines, CancelledException is thrown.
*
* @return a BeIDCard instance. The only one present, or one chosen out of
* several by the user
* @throws CancelledException
*/
public BeIDCard getOneBeIDCard(CardTerminal terminal)
throws CancelledException {
BeIDCard selectedCard = null;
do {
waitForAtLeastOneCardTerminal();
waitForAtLeastOneBeIDCard(terminal);
// copy current list of BeID Cards to avoid holding a lock on it
// during possible selectBeIDCard dialog.
// (because we'd deadlock when user inserts/removes a card while
// selectBeIDCard has not returned)
Map<CardTerminal, BeIDCard> currentBeIDCards;
synchronized (this.beIDTerminalsAndCards) {
currentBeIDCards = new HashMap<CardTerminal, BeIDCard>(
this.beIDTerminalsAndCards);
}
if (terminal != null) {
// if selecting by terminal and we have a card in the requested
// one,
// return that immediately. (this will return null if the
// terminal we want doesn't
// have a card, and continue the loop.
selectedCard = currentBeIDCards.get(terminal);
} else if (currentBeIDCards.size() == 1) {
// we have only one BeID card. return it.
selectedCard = currentBeIDCards.values().iterator().next();
} else {
// more than one, call upon the UI to obtain a selection
try {
this.logger.debug("selecting");
this.uiSelectingCard = true;
selectedCard = getUI().selectBeIDCard(
currentBeIDCards.values());
} catch (final OutOfCardsException oocex) {
// if we run out of cards, waitForAtLeastOneBeIDCard will
// ask for one in the next loop
} finally {
this.uiSelectingCard = false;
this.logger.debug("no longer selecting");
}
}
} while (selectedCard == null);
return selectedCard;
}
/**
* wait for a particular BeID card to be removed. Note that this only works
* with BeID objects that were acquired using either the
* {@link #getOneBeIDCard()} or {@link #getAllBeIDCards()} methods from the
* same BeIDCards instance. If, at time of call, that particular card is
* present, the UI is called upon to prompt the user to remove that card.
*
* @param card
* @return this BeIDCards instance to allow for method chaining
*/
public BeIDCards waitUntilCardRemoved(final BeIDCard card) {
if (this.getAllBeIDCards().contains(card)) {
try {
this.logger
.debug("waitUntilCardRemoved blocking until card removed");
this.getUI().adviseBeIDCardRemovalRequired();
while (this.getAllBeIDCards().contains(card)) {
this.beIDSleeper.sleepUntilAwakened();
}
} finally {
this.getUI().adviseEnd();
}
}
this.logger.debug("waitUntilCardRemoved returning");
return this;
}
public boolean hasCardTerminals() {
waitUntilTerminalsInitialized();
return this.cardTerminalsAttached > 0;
}
/**
* call close() if you no longer need this BeIDCards instance.
*
* @return this
* @throws InterruptedException
*/
public BeIDCards close() throws InterruptedException {
this.cardManager.stop();
this.cardAndTerminalManager.stop();
return this;
}
/**
* Set the Locale to use for subsequent UI operations. BeIDCards and
* BeIDCardManager share the same global Locale, so this will impact and and
* all instances of either. BeIDCard instances may have individual,
* per-instance Locale settings, however.
*
* @param newLocale
* will be used globally for subsequent UI actions, as if
* setLocale() was called, except where the Locale is explicity
* set for individual BeIDCard instances.
* @return this BeIDCards, to allow method chaining
*/
public BeIDCards setLocale(Locale newLocale) {
LocaleManager.setLocale(newLocale);
synchronized (this.beIDTerminalsAndCards) {
for (BeIDCard card : this.beIDTerminalsAndCards.values()) {
card.setLocale(newLocale);
}
}
return this;
}
/**
*
* @return the currently set Locale
*/
public Locale getLocale() {
return LocaleManager.getLocale();
}
/*
* Private, supporting methods
* **********************************************
*/
private void setUI(BeIDCardsUI ui) {
this.ui = ui;
if (this.ui != null) {
setLocale(ui.getLocale());
}
}
private BeIDCardsUI getUI() {
if (this.ui == null) {
try {
final ClassLoader classLoader = BeIDCard.class.getClassLoader();
final Class<?> uiClass = classLoader
.loadClass(DEFAULT_UI_IMPLEMENTATION);
setUI((BeIDCardsUI) uiClass.newInstance());
} catch (final Exception e) {
this.logger.error(UI_MISSING_LOG_MESSAGE);
throw new UnsupportedOperationException(UI_MISSING_LOG_MESSAGE,
e);
}
}
return this.ui;
}
private void waitUntilCardsInitialized() {
while (!this.cardsInitialized) {
this.logger
.debug("Waiting for CardAndTerminalManager Cards initialisation");
this.cardManagerInitSleeper.sleepUntilAwakened();
this.logger
.debug("CardAndTerminalManager now has cards initialized");
}
}
private void waitUntilTerminalsInitialized() {
while (!this.terminalsInitialized) {
this.logger
.debug("Waiting for CardAndTerminalManager Terminals initialisation");
this.terminalManagerInitSleeper.sleepUntilAwakened();
this.logger
.debug("CardAndTerminalManager now has terminals initialized");
}
}
private void waitForAtLeastOneBeIDCard(CardTerminal terminal)
throws CancelledException {
if (!this.hasBeIDCards(terminal)) {
try {
this.getUI().adviseBeIDCardRequired();
while (!this.hasBeIDCards(terminal)) {
this.beIDSleeper.sleepUntilAwakened();
}
} finally {
this.getUI().adviseEnd();
}
}
}
private void waitForAtLeastOneCardTerminal() {
if (!this.hasCardTerminals()) {
try {
this.getUI().adviseCardTerminalRequired();
while (!this.hasCardTerminals()) {
this.cardTerminalSleeper.sleepUntilAwakened();
}
} finally {
this.getUI().adviseEnd();
}
// if we just found our first CardTerminal, give us 100ms
// to get notified about any eID cards that may already present in
// that CardTerminal
// we'll get notified about any cards much faster than 100ms,
// and worst case, 100ms is not noticeable. Better than calling
// adviseBeIDCardRequired and adviseEnd
// with a few seconds in between.
if (!this.hasBeIDCards()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// intentionally empty
}
}
}
}
}