/*
* Copyright 2015 Robert Bachmann
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.licel.jcardsim.smartcardio;
import com.licel.jcardsim.utils.AutoResetEvent;
import javax.smartcardio.*;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Provider;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* <p>A simulated {@link javax.smartcardio.TerminalFactory}.</p>
* <p>Example: Obtaining a Card</p>
* <pre>
* // create card simulator
* CardSimulator cardSimulator = new CardSimulator();
*
* // connect to a card
* CardTerminal terminal =
* CardTerminalSimulator.terminal(cardSimulator);
* Card card = terminal.connect("*");
* </pre>
* <p>Example: Inserting/ejecting a Card</p>
* <pre>
* // create card simulator
* CardSimulator cardSimulator = new CardSimulator();
*
* // create CardTerminal
* CardTerminals terminals = CardTerminalSimulator.terminals("my terminal")
* CardTerminal terminal = terminals.getTerminal("my terminal");
*
* // insert Card
* cardSimulator.assignToTerminal(terminal);
*
* // eject Card
* cardSimulator.assignToTerminal(null);
* </pre>
*
* @see com.licel.jcardsim.smartcardio.CardSimulator
*/
public final class CardTerminalSimulator {
private CardTerminalSimulator() {
}
/**
* Create a single CardTerminal.
*
* @param cardSimulator card to insert
* @param name the terminal name
* @return a new <code>CardTerminal</code> instance
* @throws java.lang.NullPointerException if name or cardSimulator is null
*/
public static CardTerminal terminal(CardSimulator cardSimulator, String name) {
if (name == null) {
throw new NullPointerException("name");
}
if (cardSimulator == null) {
throw new NullPointerException("cardSimulator");
}
CardTerminal cardTerminal = terminals(name).getTerminal(name);
cardSimulator.assignToTerminal(cardTerminal);
return cardTerminal;
}
/**
* Create a CardTerminal with name "jCardSim.Terminal".
*
* @param cardSimulator card to insert
* @return a new <code>CardTerminal</code> instance
* @throws java.lang.NullPointerException if name or cardSimulator is null
*/
public static CardTerminal terminal(CardSimulator cardSimulator) {
return terminal(cardSimulator, "jCardSim.Terminal");
}
/**
* <p>Create CardTerminals.</p>
* <p>Example:</p>
* <pre>
* CardTerminals terminals = CardTerminalSimulator.terminals("1","2");
* CardTerminal terminal = terminals.getTerminal("1");
*
* // assign simulator
* CardSimulator cardSimulator = new CardSimulator();
* cardSimulator.assignToTerminal(terminal);
* </pre>
*
* @param names the terminal names
* @return a new <code>CardTerminals</code> instance
* @throws java.lang.NullPointerException if names is null
* @throws java.lang.IllegalArgumentException if any name is null or duplicated
* @see javax.smartcardio.CardTerminals
*/
public static CardTerminals terminals(String... names) {
if (names == null) {
throw new NullPointerException("names");
}
Set<String> set = new HashSet<String>(names.length);
for (String name : names) {
if (set.contains(name)) {
throw new IllegalArgumentException("Duplicate name '" + name + "'");
}
set.add(name);
}
return new CardTerminalsImpl(names);
}
/**
* <p>Security provider.</p>
* <p>Register the SecurityProvider with:</p>
* <pre>
* if (Security.getProvider("CardTerminalSimulator") == null) {
* Security.addProvider(new CardTerminalSimulator.SecurityProvider());
* }
* </pre>
*/
public static final class SecurityProvider extends Provider {
public SecurityProvider() {
super("CardTerminalSimulator", 1.0d, "jCardSim Virtual Terminal Provider");
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
put("TerminalFactory." + "CardTerminalSimulator", Factory.class.getCanonicalName()
.replace(".Factory", "$Factory"));
return null;
}
});
}
}
/**
* {@link javax.smartcardio.TerminalFactorySpi} implementation.
* Applications do not access this class directly, instead see {@link javax.smartcardio.TerminalFactory}.
*/
@SuppressWarnings("unused")
public static final class Factory extends TerminalFactorySpi {
private final CardTerminals cardTerminals;
public Factory(Object params) {
String[] names;
if (params == null) {
names = new String[]{"jCardSim.Terminal"};
} else if (params instanceof String) {
names = new String[]{(String) params};
} else if (params instanceof String[]) {
names = (String[]) params;
} else {
throw new IllegalArgumentException("Illegal parameter: " + params);
}
cardTerminals = terminals(names);
}
@Override
protected CardTerminals engineTerminals() {
return cardTerminals;
}
}
static boolean waitForLatch(AutoResetEvent autoResetEvent, long timeoutMilliseconds) throws InterruptedException {
if (timeoutMilliseconds < 0) {
throw new IllegalArgumentException("timeout is negative");
}
if (timeoutMilliseconds == 0) { // wait forever
boolean success;
do {
success = autoResetEvent.await(1, TimeUnit.MINUTES);
} while (!success);
return true;
}
return autoResetEvent.await(timeoutMilliseconds, TimeUnit.MILLISECONDS);
}
static final class CardTerminalsImpl extends CardTerminals {
private final AtomicBoolean waitCalled = new AtomicBoolean(false);
private final AutoResetEvent terminalsChangeAutoResetEvent = new AutoResetEvent();
private final ArrayList<CardTerminalImpl> simulatedTerminals;
private final HashMap<CardTerminal, State> terminalStateMap;
CardTerminalsImpl(String[] names) {
simulatedTerminals = new ArrayList<CardTerminalImpl>(names.length);
terminalStateMap = new HashMap<CardTerminal, State>(names.length);
for (String name : names) {
simulatedTerminals.add(new CardTerminalImpl(name, terminalStateMap, terminalsChangeAutoResetEvent));
}
}
@Override
public synchronized List<CardTerminal> list(State state) throws CardException {
if (state == null) {
throw new NullPointerException("state");
}
synchronized (terminalStateMap) {
final ArrayList<CardTerminal> result = new ArrayList<CardTerminal>(simulatedTerminals.size());
for (CardTerminal terminal : simulatedTerminals) {
State terminalState = terminalStateMap.get(terminal);
switch (state) {
case ALL:
result.add(terminal);
break;
case CARD_ABSENT:
if (!terminal.isCardPresent() && terminalState != State.CARD_REMOVAL) {
result.add(terminal);
}
break;
case CARD_PRESENT:
if (terminal.isCardPresent() && terminalState != State.CARD_INSERTION) {
result.add(terminal);
}
break;
case CARD_INSERTION:
if (waitCalled.get()) {
if (terminalState == State.CARD_INSERTION) {
terminalStateMap.put(terminal, State.CARD_PRESENT);
result.add(terminal);
}
} else if (terminal.isCardPresent()) {
result.add(terminal);
}
break;
case CARD_REMOVAL:
if (waitCalled.get()) {
if (terminalState == State.CARD_REMOVAL) {
terminalStateMap.put(terminal, State.CARD_ABSENT);
result.add(terminal);
}
} else if (!terminal.isCardPresent()) {
result.add(terminal);
}
break;
}
}
return Collections.unmodifiableList(result);
}
}
@Override
public boolean waitForChange(long timeoutMilliseconds) throws CardException {
try {
return waitForLatch(terminalsChangeAutoResetEvent, timeoutMilliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
waitCalled.set(true);
}
}
}
static final class CardTerminalImpl extends CardTerminal {
private final String name;
private final Map<CardTerminal, CardTerminals.State> terminalStateMap;
private final AutoResetEvent terminalsChangeAutoResetEvent;
private final AutoResetEvent cardPresent = new AutoResetEvent();
private final AutoResetEvent cardAbsent = new AutoResetEvent();
private final AtomicReference<CardSimulator> cardSimulatorReference = new AtomicReference<CardSimulator>();
CardTerminalImpl(String name, Map<CardTerminal, CardTerminals.State> terminalStateMap, AutoResetEvent terminalsChangeAutoResetEvent) {
this.name = name;
this.terminalStateMap = terminalStateMap;
this.terminalsChangeAutoResetEvent = terminalsChangeAutoResetEvent;
cardAbsent.signal();
terminalStateMap.put(this, CardTerminals.State.CARD_ABSENT);
}
@Override
public String getName() {
return name;
}
@Override
public Card connect(String protocol) throws CardException {
CardSimulator cardSimulator = cardSimulatorReference.get();
if (cardSimulator == null) {
throw new CardNotPresentException("No card inserted. You need to call CardTerminalSimulator#assignToTerminal");
}
return cardSimulator.internalConnect(protocol);
}
@Override
public boolean isCardPresent() throws CardException {
return cardSimulatorReference.get() != null;
}
@Override
public boolean waitForCardPresent(long timeoutMilliseconds) throws CardException {
try {
return waitForLatch(cardPresent, timeoutMilliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
@Override
public boolean waitForCardAbsent(long timeoutMilliseconds) throws CardException {
try {
return waitForLatch(cardAbsent, timeoutMilliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
void assignSimulator(CardSimulator cardSimulator) {
synchronized (terminalStateMap) {
CardSimulator oldCardSimulator = cardSimulatorReference.getAndSet(cardSimulator);
boolean change = false;
boolean present = false;
if (oldCardSimulator != null) {
oldCardSimulator.internalEject(this);
change = true;
}
if (cardSimulator != null) {
present = true;
change = true;
}
if (change) {
if (present) {
terminalStateMap.put(this, CardTerminals.State.CARD_INSERTION);
cardPresent.signal();
cardAbsent.reset();
} else {
terminalStateMap.put(this, CardTerminals.State.CARD_REMOVAL);
cardPresent.reset();
cardAbsent.signal();
}
terminalsChangeAutoResetEvent.signal();
}
}
}
@Override
public String toString() {
return "jCardSim Terminal: " + name;
}
}
}