Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Upgraded
- Keypop Card API `2.0.1` -> `2.1.0`
- Keyple Plugin API `2.3.1` -> `2.3.2`
- Keyple Distributed Remote API `3.1.0` -> `3.1.1`
- Keyple Distributed Local API `2.2.0` -> `2.2.1`
- Google Gson Library `2.10.1` -> `2.13.2`
- AssertJ `3.25.3` -> `3.27.6` (test dependency)
- Mockito `5.11.0` -> `5.21.0` (test dependency)
- Awaitility `4.2.1` -> `4.3.0` (test dependency)

## [3.4.0] - 2025-11-21
### Upgraded
Expand Down
16 changes: 8 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ plugins {

dependencies {
implementation("org.eclipse.keypop:keypop-reader-java-api:2.1.0")
implementation("org.eclipse.keypop:keypop-card-java-api:2.0.1")
implementation("org.eclipse.keypop:keypop-card-java-api:2.1.0-SNAPSHOT")
implementation("org.eclipse.keyple:keyple-common-java-api:2.0.2")
implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.1")
implementation("org.eclipse.keyple:keyple-distributed-remote-java-api:3.1.0")
implementation("org.eclipse.keyple:keyple-distributed-local-java-api:2.2.0")
implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.2")
implementation("org.eclipse.keyple:keyple-distributed-remote-java-api:3.1.1")
implementation("org.eclipse.keyple:keyple-distributed-local-java-api:2.2.1")
implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.code.gson:gson:2.13.2")
implementation("org.slf4j:slf4j-api:1.7.32")
testImplementation("org.slf4j:slf4j-simple:1.7.32")
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.vintage:junit-vintage-engine")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("org.awaitility:awaitility:4.2.1")
testImplementation("org.assertj:assertj-core:3.27.6")
testImplementation("org.mockito:mockito-core:5.21.0")
testImplementation("org.awaitility:awaitility:4.3.0")
}

///////////////////////////////////////////////////////////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
group = org.eclipse.keyple
title = Keyple Service Java Lib
description = Keyple core components
version = 3.4.0-SNAPSHOT
version = 3.5.0-SNAPSHOT

# Java Configuration
javaSourceLevel = 1.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ final List<CardSelectionResponseApi> transmitCardSelectionRequests(
List<CardSelectionRequestSpi> cardSelectionRequests,
MultiSelectionProcessing multiSelectionProcessing,
ChannelControl channelControl)
throws ReaderBrokenCommunicationException, CardBrokenCommunicationException {
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedResponseTimeException {

checkStatus();

Expand Down Expand Up @@ -201,7 +203,8 @@ abstract List<CardSelectionResponseApi> processCardSelectionRequests(
ChannelControl channelControl)
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedStatusWordException;
UnexpectedStatusWordException,
UnexpectedResponseTimeException;

/**
* Abstract method performing the actual transmission of the card request.
Expand All @@ -213,13 +216,15 @@ abstract List<CardSelectionResponseApi> processCardSelectionRequests(
* @throws CardBrokenCommunicationException if the communication with the card has failed.
* @throws UnexpectedStatusWordException If status word verification is enabled in the card
* request and the card returned an unexpected code.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected time.
* @since 2.0.0
*/
abstract CardResponseApi processCardRequest(
CardRequestSpi cardRequest, ChannelControl channelControl)
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedStatusWordException;
UnexpectedStatusWordException,
UnexpectedResponseTimeException;

/**
* {@inheritDoc}
Expand All @@ -241,7 +246,8 @@ public final CardResponseApi transmitCardRequest(
CardRequestSpi cardRequest, ChannelControl channelControl)
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedStatusWordException {
UnexpectedStatusWordException,
UnexpectedResponseTimeException {
checkStatus();

Assert.getInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class ApduResponseAdapter implements ApduResponseApi {

private final byte[] apdu;
private final int statusWord;
private final Integer responseTime;

/**
* Builds an APDU response from an array of bytes from the card, computes the status word.
Expand All @@ -32,8 +33,21 @@ final class ApduResponseAdapter implements ApduResponseApi {
* @since 2.0.0
*/
ApduResponseAdapter(byte[] apdu) {
this(apdu, null);
}

/**
* Builds an APDU response from an array of bytes from the card, computes the status word.
*
* @param apdu An array of at least 2 bytes.
* @param responseTime The response time in milliseconds (can be null).
* @since 3.4.0
*/
ApduResponseAdapter(byte[] apdu, Integer responseTime) {
this.apdu = apdu;
statusWord = ((apdu[apdu.length - 2] & 0x000000FF) << 8) + (apdu[apdu.length - 1] & 0x000000FF);
this.statusWord =
((apdu[apdu.length - 2] & 0x000000FF) << 8) + (apdu[apdu.length - 1] & 0x000000FF);
this.responseTime = responseTime;
}

/**
Expand Down Expand Up @@ -66,6 +80,16 @@ public int getStatusWord() {
return statusWord;
}

/**
* {@inheritDoc}
*
* @since 3.4.0
*/
@Override
public Integer getResponseTime() {
return responseTime;
}

/**
* Converts the APDU response into a string where the data is encoded in a json format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public CardSelectionResult processCardSelectionScenario(CardReader reader) {
cardSelectors, cardSelectionRequests, multiSelectionProcessing, channelControl);
} catch (ReaderBrokenCommunicationException e) {
throw new ReaderCommunicationException(e.getMessage(), e);
} catch (CardBrokenCommunicationException e) {
} catch (CardBrokenCommunicationException | UnexpectedResponseTimeException e) {
throw new CardCommunicationException(e.getMessage(), e);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,15 @@ private String execute() {
*
* @throws CardBrokenCommunicationException If a card communication error occurs.
* @throws ReaderBrokenCommunicationException If a reader communication error occurs.
* @throws UnexpectedStatusWordException If an unexpected status word is received.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected
* time.
*/
private void transmitCardRequest()
throws CardBrokenCommunicationException,
ReaderBrokenCommunicationException,
UnexpectedStatusWordException {
UnexpectedStatusWordException,
UnexpectedResponseTimeException {

// Extract parameters from the message
JsonObject params = input.getAsJsonObject(JsonProperty.PARAMETERS.getKey());
Expand All @@ -345,9 +349,13 @@ private void transmitCardRequest()
*
* @throws CardBrokenCommunicationException If a card communication error occurs.
* @throws ReaderBrokenCommunicationException If a reader communication error occurs.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected
* time.
*/
private void transmitCardSelectionRequests()
throws CardBrokenCommunicationException, ReaderBrokenCommunicationException {
throws CardBrokenCommunicationException,
ReaderBrokenCommunicationException,
UnexpectedResponseTimeException {

// Extract parameters from the message
JsonObject params = input.getAsJsonObject(JsonProperty.PARAMETERS.getKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ static class ApduRequest implements ApduRequestSpi {
private byte[] apdu;
private Set<Integer> successfulStatusWords;
private String info;
private Integer maxExpectedResponseTime;

/**
* Default constructor.
Expand All @@ -193,6 +194,7 @@ static class ApduRequest implements ApduRequestSpi {
this.apdu = src.getApdu().clone();
this.successfulStatusWords = new HashSet<>(src.getSuccessfulStatusWords());
this.info = src.getInfo();
this.maxExpectedResponseTime = src.getMaxExpectedResponseTime();
}

@Override
Expand All @@ -210,6 +212,11 @@ public String getInfo() {
return info;
}

@Override
public Integer getMaxExpectedResponseTime() {
return maxExpectedResponseTime;
}

@Override
public String toString() {
return "APDU_REQUEST = " + JsonUtil.toJson(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ final List<CardSelectionResponseApi> processCardSelectionRequests(
ChannelControl channelControl)
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedStatusWordException {
UnexpectedStatusWordException,
UnexpectedResponseTimeException {

checkStatus();

Expand Down Expand Up @@ -215,7 +216,8 @@ final CardResponseApi processCardRequest(
CardRequestSpi cardRequest, ChannelControl channelControl)
throws CardBrokenCommunicationException,
ReaderBrokenCommunicationException,
UnexpectedStatusWordException {
UnexpectedStatusWordException,
UnexpectedResponseTimeException {

checkStatus();

Expand Down Expand Up @@ -351,11 +353,14 @@ public final void releaseChannel() throws ReaderBrokenCommunicationException {
* @return A not null reference.
* @throws ReaderIOException if the communication with the reader has failed.
* @throws CardIOException if the communication with the card has failed.
* @throws UnexpectedResponseTimeException if the response time exceeds the maximum expected time.
*/
private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
throws CardIOException, ReaderIOException {
throws CardIOException, ReaderIOException, UnexpectedResponseTimeException {

ApduResponseAdapter apduResponse;
long startTimeNano = System.nanoTime();

if (logger.isDebugEnabled()) {
long timeStamp = System.nanoTime();
long elapsed10ms = (timeStamp - before) / 100000;
Expand All @@ -367,7 +372,9 @@ private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
elapsed10ms / 10.0);
}

apduResponse = new ApduResponseAdapter(readerSpi.transmitApdu(apduRequest.getApdu()));
byte[] responseBytes = readerSpi.transmitApdu(apduRequest.getApdu());
int responseTime = (int) ((System.nanoTime() - startTimeNano) / 1_000_000);
apduResponse = new ApduResponseAdapter(responseBytes, responseTime);

if (logger.isDebugEnabled()) {
long timeStamp = System.nanoTime();
Expand Down Expand Up @@ -415,8 +422,8 @@ private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
}

// Execute APDU directly to avoid recursive status handling
byte[] responseBytes = readerSpi.transmitApdu(getResponseApdu);
apduResponse = new ApduResponseAdapter(responseBytes);
byte[] chainedResponseBytes = readerSpi.transmitApdu(getResponseApdu);
apduResponse = new ApduResponseAdapter(chainedResponseBytes);

if (logger.isDebugEnabled()) {
long timeStamp = System.nanoTime();
Expand Down Expand Up @@ -453,7 +460,9 @@ private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
completeApdu[totalLength] = (byte) ((apduResponse.getStatusWord() >> 8) & 0xFF);
completeApdu[totalLength + 1] = (byte) (apduResponse.getStatusWord() & 0xFF);

apduResponse = new ApduResponseAdapter(completeApdu);
// Calculate total response time from the beginning of the initial APDU
int totalResponseTime = (int) ((System.nanoTime() - startTimeNano) / 1_000_000);
apduResponse = new ApduResponseAdapter(completeApdu, totalResponseTime);
}

} else if (apduResponse.getDataOut().length == 0) {
Expand Down Expand Up @@ -486,6 +495,19 @@ private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
}
}

// Validate response time against max expected time
Integer maxExpectedTime = apduRequest.getMaxExpectedResponseTime();
if (maxExpectedTime != null
&& apduResponse.getResponseTime() != null
&& apduResponse.getResponseTime() > maxExpectedTime) {
throw new UnexpectedResponseTimeException(
new CardResponseAdapter(Collections.singletonList(apduResponse), true),
true,
String.format(
"Response time %d ms exceeded maximum expected time %d ms",
apduResponse.getResponseTime(), maxExpectedTime));
}

return apduResponse;
}

Expand All @@ -499,14 +521,16 @@ private ApduResponseAdapter processApduRequest(ApduRequestSpi apduRequest)
* @throws CardBrokenCommunicationException If the communication with the card has failed.
* @throws UnexpectedStatusWordException If status word verification is enabled in the card
* request and the card returned an unexpected code.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected time.
*/
private CardSelectionResponseApi processCardSelectionRequest(
CardSelector<?> cardSelector,
CardSelectionRequestSpi cardSelectionRequest,
ChannelControl channelControl)
throws ReaderBrokenCommunicationException,
CardBrokenCommunicationException,
UnexpectedStatusWordException {
UnexpectedStatusWordException,
UnexpectedResponseTimeException {

isLogicalChannelOpen = false;

Expand Down Expand Up @@ -551,10 +575,13 @@ private CardSelectionResponseApi processCardSelectionRequest(
* @return A not null {@link SelectionStatus}.
* @throws ReaderBrokenCommunicationException If the communication with the reader has failed.
* @throws CardBrokenCommunicationException If the communication with the card has failed.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected time.
*/
private SelectionStatus processSelection(
CardSelector<?> cardSelector, CardSelectionRequestSpi cardSelectionRequest)
throws CardBrokenCommunicationException, ReaderBrokenCommunicationException {
throws CardBrokenCommunicationException,
ReaderBrokenCommunicationException,
UnexpectedResponseTimeException {
try {
// RL-CLA-CHAAUTO.1
String powerOnData;
Expand Down Expand Up @@ -644,10 +671,13 @@ private boolean checkPowerOnData(String powerOnData, InternalCardSelector cardSe
*
* @param cardSelector The card selector.
* @return A not null {@link ApduResponseApi} containing the FCI.
* @throws CardIOException if the communication with the card has failed.
* @throws ReaderIOException if the communication with the reader has failed.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected time.
* @see #processSelection(CardSelector, CardSelectionRequestSpi)
*/
private ApduResponseAdapter selectByAid(InternalIsoCardSelector cardSelector)
throws CardIOException, ReaderIOException {
throws CardIOException, ReaderIOException, UnexpectedResponseTimeException {

ApduResponseAdapter fciResponse;

Expand Down Expand Up @@ -679,9 +709,10 @@ private ApduResponseAdapter selectByAid(InternalIsoCardSelector cardSelector)
* @return A not null {@link ApduResponseApi}.
* @throws ReaderIOException if the communication with the reader has failed.
* @throws CardIOException if the communication with the card has failed.
* @throws UnexpectedResponseTimeException If the response time exceeds the maximum expected time.
*/
private ApduResponseAdapter processExplicitAidSelection(InternalIsoCardSelector cardSelector)
throws CardIOException, ReaderIOException {
throws CardIOException, ReaderIOException, UnexpectedResponseTimeException {

final byte[] aid = cardSelector.getAid();
if (logger.isDebugEnabled()) {
Expand Down Expand Up @@ -881,6 +912,12 @@ public String getInfo() {
return info;
}

@Override
public Integer getMaxExpectedResponseTime() {
// Internal GET RESPONSE commands have no specific time constraint
return null;
}

@Override
public String toString() {
return "APDU_REQUEST = " + JsonUtil.toJson(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.keypop.card.CardBrokenCommunicationException;
import org.eclipse.keypop.card.CardSelectionResponseApi;
import org.eclipse.keypop.card.ReaderBrokenCommunicationException;
import org.eclipse.keypop.card.UnexpectedResponseTimeException;
import org.eclipse.keypop.reader.CardReaderEvent;
import org.eclipse.keypop.reader.ObservableCardReader;
import org.eclipse.keypop.reader.ReaderCommunicationException;
Expand Down Expand Up @@ -289,7 +290,7 @@ final CardReaderEvent processCardInserted() {
getName(),
new ReaderCommunicationException(READER_MONITORING_ERROR, e));

} catch (CardBrokenCommunicationException e) {
} catch (CardBrokenCommunicationException | UnexpectedResponseTimeException e) {
// The last transmission failed, close the logical and physical channels.
closeLogicalAndPhysicalChannelsSilently();
// The card was removed or not read correctly, no exception raising or event notification,
Expand Down
Loading
Loading