A high-performance, enterprise-grade Java server implementation for IBM IMS Connect protocol communication. This server acts as a gateway between modern applications and IBM mainframe IMS systems, providing secure, scalable, and monitored connectivity.
- High Performance: Built on Netty NIO framework for maximum throughput
- Enterprise Security: SSL/TLS with mutual authentication, RACF integration, audit logging
- Connection Pooling: Intelligent load balancing across multiple mainframe backends
- Monitoring: Prometheus metrics, health checks, and structured logging
- Spring Boot Integration: Enterprise-ready with auto-configuration
- Kubernetes Ready: Production deployment manifests included
- IMS Connect Protocol: Full compliance with IBM IMS Connect protocol and HWSSMPL1 exit routine
- OTMA Support: Open Transaction Manager Access with conversational transactions
- System Messages: Built-in PING, NOTIFY, ECHO, and STATUS message handlers
- Legacy Compatibility: Supports both modern OTMA and legacy IMS Connect clients
- Conversational Transactions: Multi-message transaction sequences with state management
- RACF Security: Resource Access Control Facility integration with token validation
- Message Segmentation: Enhanced LL/ZZ message format support
- Protocol Routing: Automatic detection and routing between OTMA and legacy protocols
┌─────────────────┐ ┌──────────────────────────────────┐ ┌─────────────────┐
│ Client Apps │───▶│ IMS Connect Java Server │───▶│ Mainframe │
│ │ │ │ │ IMS Systems │
│ • Legacy IMS │ │ ┌─────────────────────────────┐ │ │ │
│ • OTMA Clients │ │ │ Protocol Router │ │ │ • IMS TM │
│ • System Tools │ │ │ (Legacy/OTMA Detection) │ │ │ • RACF Security │
│ │ │ └─────────────────────────────┘ │ │ • OTMA Support │
└─────────────────┘ │ │ └─────────────────┘
│ ┌─────────────────────────────┐ │
│ │ Transaction Handlers │ │
│ │ • Banking • System Messages │ │
│ │ • Security • Conversational │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Security & Monitoring │ │
│ │ • RACF Integration │ │
│ │ • Audit Logging │ │
│ │ • Health Checks (PING) │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────┘
│
▼
┌──────────────────┐
│ Monitoring │
│ (Prometheus, │
│ Grafana, Logs) │
└──────────────────┘
- Java 17 or higher
- Maven 3.6+
- Docker (optional)
- Kubernetes cluster (optional)
git clone https://github.com/gperkinscgi/ims-connect-java-server.git
cd ims-connect-java-server
# Automated development setup
./scripts/dev-setup.sh
# Edit your configuration
vi .env
# Start development server
./scripts/run-dev.shThe setup script will:
- Create
.envfile from template - Generate self-signed SSL certificates for development
- Verify Java and Maven requirements
- Build the project
# Copy environment template
cp .env.example .env
# Build the project
mvn clean package
# Run with development profile
mvn spring-boot:run -Dspring-boot.run.profiles=development
# Or using JAR
java -jar target/ims-connect-java-server-1.0.0.jar --spring.profiles.active=development# Build image
docker build -t ims-connect-server .
# Run with Docker Compose (includes monitoring)
docker-compose up -d
# View logs
docker-compose logs -f ims-connect-server# Deploy to Kubernetes
kubectl apply -f k8s/
# Check status
kubectl get pods -n ims-connect
# View logs
kubectl logs -f deployment/ims-connect-server -n ims-connectFor development, all secrets and configuration are managed through a .env file:
# SSL Certificate Passwords
SSL_KEYSTORE_PASSWORD=changeme
SSL_KEY_PASSWORD=changeme
SSL_TRUSTSTORE_PASSWORD=changeme
# Mainframe Backend Configuration
MAINFRAME_HOST_1=localhost
MAINFRAME_PORT_1=9999
MAINFRAME_HOST_2=localhost
MAINFRAME_PORT_2=9998
# Security Configuration
ADMIN_PASSWORD=admin123
MONITOR_PASSWORD=monitor123
# Application Settings
LOG_LEVEL=DEBUG
JVM_OPTS=-Xmx1g -Xms512mSecurity Features:
- ✅
.envfiles are Git-ignored - ✅ Only loaded in development profiles
- ✅ Fallback to safe defaults if missing
- ✅ Template provided (
.env.example)
The server supports multiple configuration profiles:
Development Profile (application-development.yml):
ims-connect:
server:
port: 9999
datastore-name: "DEV-IMS"
worker-threads: 4 # Reduced for development
backends:
- name: "dev-mainframe-1"
host: "${MAINFRAME_HOST_1:localhost}"
port: ${MAINFRAME_PORT_1:9999}
ssl-enabled: false # Disabled for local dev
security:
ssl:
enabled: false # SSL disabled for local development
keystore-password: "${SSL_KEYSTORE_PASSWORD:devpassword}"Production Profile (application.yml):
ims-connect:
server:
port: 9999
boss-threads: 2
worker-threads: 8
security:
enabled: true
ssl:
enabled: true
keystore-path: "config/ssl/server.p12"
keystore-password: "${SSL_KEYSTORE_PASSWORD}"| Variable | Description | Development Default |
|---|---|---|
SPRING_PROFILES_ACTIVE |
Active Spring profile | development |
SSL_KEYSTORE_PASSWORD |
SSL keystore password | devpassword |
SSL_KEY_PASSWORD |
SSL key password | devpassword |
SSL_TRUSTSTORE_PASSWORD |
SSL truststore password | devpassword |
MAINFRAME_HOST_1 |
Primary mainframe host | localhost |
MAINFRAME_PORT_1 |
Primary mainframe port | 9999 |
MAINFRAME_HOST_2 |
Secondary mainframe host | localhost |
MAINFRAME_PORT_2 |
Secondary mainframe port | 9998 |
ADMIN_PASSWORD |
Admin user password | admin |
MONITOR_PASSWORD |
Monitor user password | monitor |
LOG_LEVEL |
Application log level | DEBUG |
JVM_OPTS |
JVM options | -Xmx1g -Xms512m |
The server includes comprehensive OTMA (Open Transaction Manager Access) support for enhanced IMS messaging:
ims-connect:
otma:
enabled: true
conversations:
enabled: true
max-conversations: 1000
conversation-timeout-ms: 300000 # 5 minutes
security:
racf-enabled: true
audit-enabled: true
token-validation-enabled: trueOTMA Features:
- Conversational Transactions: Multi-message sequences with state management
- Enhanced Message Segmentation: LL/ZZ format support for complex messages
- Protocol Auto-Detection: Automatic routing between OTMA and legacy clients
- RACF Integration: Full security token validation and authorization
- Audit Logging: Comprehensive transaction audit trails
Built-in system message handlers for operational management:
ims-connect:
system-messages:
enabled: true
ping-enabled: true # Health check messages
notify-enabled: true # System notifications
echo-enabled: true # Connectivity testing
status-enabled: true # Server status queriesSystem Message Types:
| Message | Purpose | Response |
|---|---|---|
| PING | Health checks | PONG with timestamp and server status |
| NOTIFY | System notifications | Acknowledgment with appropriate log level |
| ECHO | Connectivity testing | Exact echo of sent data |
| STATUS | Server information | Runtime stats (memory, CPU, uptime) |
Usage Examples:
# Health check via PING
echo "PING TEST_DATA" | nc localhost 9999
# Server status query
echo "STATUS" | nc localhost 9999
# Connectivity test
echo "ECHO Hello World" | nc localhost 9999The server includes example transaction handlers in the examples package to demonstrate proper implementation patterns. These are for demonstration only and should not be used in production.
To enable examples in development:
ims-connect:
examples:
enabled: true// Located in com.cgi.icbc.imsconnect.examples.handlers
@Component
public class BankingTransactionHandler implements IMSTransactionHandler {
private static final Logger logger = LoggerFactory.getLogger(BankingTransactionHandler.class);
private final AuditLogger auditLogger;
private final AccountService accountService;
@Autowired
public BankingTransactionHandler(AuditLogger auditLogger, AccountService accountService) {
this.auditLogger = auditLogger;
this.accountService = accountService;
}
@Override
public boolean canHandle(String transactionCode) {
// Handle banking transaction codes
return "BALINQ".equals(transactionCode) || "TRANSFER".equals(transactionCode);
}
@Override
public IMSResponse handleTransaction(IRMHeader header, String messageData) {
String clientId = header.getClientId();
String transactionCode = header.getTransactionCode();
try {
// Convert EBCDIC message from mainframe client to ASCII
String asciiMessage = EbcdicConverter.ebcdicToAscii(messageData);
// Route to appropriate handler based on transaction code
IMSResponse response = switch (transactionCode) {
case "BALINQ" -> handleBalanceInquiry(header, asciiMessage);
case "TRANSFER" -> handleFundsTransfer(header, asciiMessage);
default -> createErrorResponse("Unknown transaction code: " + transactionCode);
};
// Audit successful transaction
auditLogger.logTransaction("TRANSACTION_PROCESSED", clientId,
transactionCode, null, true, "Message length: " + messageData.length());
return response;
} catch (Exception e) {
logger.error("Failed to process transaction {} for client {}", transactionCode, clientId, e);
auditLogger.logTransaction("TRANSACTION_FAILED", clientId,
transactionCode, null, false, "Error: " + e.getMessage());
return createErrorResponse("Transaction processing failed: " + e.getMessage());
}
}
private IMSResponse handleBalanceInquiry(IRMHeader header, String messageData) {
// Parse incoming message (fixed-format from mainframe client)
String accountNumber = messageData.substring(8, 24).trim(); // Account number
String customerNumber = messageData.substring(24, 36).trim(); // Customer number
logger.info("Processing balance inquiry for account: {}, customer: {}",
accountNumber, customerNumber);
// Call business logic
AccountBalance balance = accountService.getAccountBalance(accountNumber);
// Build response message in fixed format expected by mainframe client
StringBuilder response = new StringBuilder();
response.append(String.format("%-8s", "BALINQ")); // Echo transaction code
response.append(String.format("%-4s", "0000")); // Response code (0000 = success)
response.append(String.format("%-16s", accountNumber)); // Account number
response.append(String.format("%015d", balance.getAmountCents())); // Balance in cents (15 digits)
response.append(String.format("%-3s", balance.getCurrencyCode())); // Currency code
response.append(String.format("%-1s", balance.getAccountStatus())); // Account status
response.append(String.format("%-50s", " ")); // Reserved space
// Convert response back to EBCDIC for mainframe client
String ebcdicResponse = EbcdicConverter.asciiToEbcdic(response.toString());
return IMSResponse.success(ebcdicResponse);
}
private IMSResponse handleFundsTransfer(IRMHeader header, String messageData) {
// Parse transfer request
String fromAccount = messageData.substring(8, 24).trim();
String toAccount = messageData.substring(24, 40).trim();
long amountCents = Long.parseLong(messageData.substring(40, 55).trim());
logger.info("Processing funds transfer: {} -> {}, amount: {}",
fromAccount, toAccount, amountCents);
// Call business logic
TransferResult result = accountService.transferFunds(fromAccount, toAccount, amountCents);
// Build response
StringBuilder response = new StringBuilder();
response.append(String.format("%-8s", "TRANSFER"));
response.append(String.format("%-4s", result.isSuccess() ? "0000" : "1001"));
response.append(String.format("%-16s", fromAccount));
response.append(String.format("%-16s", toAccount));
response.append(String.format("%-20s", result.getTransactionId()));
response.append(String.format("%-50s", result.getMessage()));
String ebcdicResponse = EbcdicConverter.asciiToEbcdic(response.toString());
return result.isSuccess() ?
IMSResponse.success(ebcdicResponse) :
IMSResponse.error(ebcdicResponse);
}
private IMSResponse createErrorResponse(String errorMessage) {
StringBuilder response = new StringBuilder();
response.append(String.format("%-8s", "ERROR"));
response.append(String.format("%-4s", "9999")); // General error code
response.append(String.format("%-100s", errorMessage));
String ebcdicResponse = EbcdicConverter.asciiToEbcdic(response.toString());
return IMSResponse.error(ebcdicResponse);
}
}
### Example Business Service Layer
```java
// Located in com.cgi.icbc.imsconnect.examples.service
@Service
public class AccountService {
private static final Logger logger = LoggerFactory.getLogger(AccountService.class);
// Example mock data - in real implementation, this would call database/external services
private final Map<String, AccountBalance> accounts = new HashMap<>();
@PostConstruct
public void initializeTestData() {
accounts.put("1234567890123456", new AccountBalance(
"1234567890123456", 150000L, "CAD", "A")); // $1,500.00 CAD
accounts.put("9876543210987654", new AccountBalance(
"9876543210987654", 250000L, "USD", "A")); // $2,500.00 USD
}
public AccountBalance getAccountBalance(String accountNumber) {
AccountBalance balance = accounts.get(accountNumber);
if (balance == null) {
throw new AccountNotFoundException("Account not found: " + accountNumber);
}
return balance;
}
public TransferResult transferFunds(String fromAccount, String toAccount, long amountCents) {
try {
AccountBalance fromBalance = getAccountBalance(fromAccount);
AccountBalance toBalance = getAccountBalance(toAccount);
if (fromBalance.getAmountCents() < amountCents) {
return TransferResult.failure("Insufficient funds");
}
// Perform transfer
fromBalance.setAmountCents(fromBalance.getAmountCents() - amountCents);
toBalance.setAmountCents(toBalance.getAmountCents() + amountCents);
String transactionId = "TXN" + System.currentTimeMillis();
logger.info("Transfer completed: {} -> {}, amount: {}, txnId: {}",
fromAccount, toAccount, amountCents, transactionId);
return TransferResult.success(transactionId, "Transfer completed successfully");
} catch (Exception e) {
logger.error("Transfer failed: {} -> {}", fromAccount, toAccount, e);
return TransferResult.failure("Transfer failed: " + e.getMessage());
}
}
}@Data
@AllArgsConstructor
@NoArgsConstructor
public class AccountBalance {
private String accountNumber;
private Long amountCents; // Store money as cents to avoid decimal precision issues
private String currencyCode; // "CAD", "USD", etc.
private String accountStatus; // "A" = Active, "C" = Closed, "F" = Frozen
public BigDecimal getAmount() {
return BigDecimal.valueOf(amountCents).divide(BigDecimal.valueOf(100));
}
}
@Data
@AllArgsConstructor
public class TransferResult {
private boolean success;
private String transactionId;
private String message;
public static TransferResult success(String transactionId, String message) {
return new TransferResult(true, transactionId, message);
}
public static TransferResult failure(String message) {
return new TransferResult(false, null, message);
}
}
// Custom exceptions
public class AccountNotFoundException extends RuntimeException {
public AccountNotFoundException(String message) {
super(message);
}@Configuration
public class TransactionHandlerConfiguration {
@Bean
public IMSTransactionHandlerRegistry handlerRegistry(
List<IMSTransactionHandler> handlers,
DefaultIMSServerHandler serverHandler) {
IMSTransactionHandlerRegistry registry = new IMSTransactionHandlerRegistry();
// Register all transaction handlers
handlers.forEach(registry::registerHandler);
// Set the registry in the server handler
serverHandler.setTransactionHandlerRegistry(registry);
return registry;
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"ims-connect.backends[0].host=localhost",
"ims-connect.backends[0].port=9999",
"ims-connect.security.enabled=false"
})
class BankingTransactionHandlerTest {
@Autowired
private BankingTransactionHandler handler;
@Test
void testBalanceInquiryHandler() {
// Arrange - create IRM header
IRMHeader header = IRMHeader.builder()
.transactionCode("BALINQ")
.clientId("TEST_CLIENT")
.architecture(IRMHeader.IRMARCH1)
.flags(IRMHeader.IRMSYNCH)
.build();
// Create message in EBCDIC format (as it would come from mainframe client)
String message = String.format("%-8s%-16s%-12s%-50s",
"BALINQ", "1234567890123456", "CUST123", " ");
String ebcdicMessage = EbcdicConverter.asciiToEbcdic(message);
// Act
IMSResponse response = handler.handleTransaction(header, ebcdicMessage);
// Assert
assertThat(response.isSuccess()).isTrue();
assertThat(response.getData()).isNotEmpty();
// Verify response format
String asciiResponse = EbcdicConverter.ebcdicToAscii(response.getData());
assertThat(asciiResponse.substring(0, 8).trim()).isEqualTo("BALINQ");
assertThat(asciiResponse.substring(8, 12).trim()).isEqualTo("0000"); // Success code
}
@Test
void testCanHandleTransactionCodes() {
assertThat(handler.canHandle("BALINQ")).isTrue();
assertThat(handler.canHandle("TRANSFER")).isTrue();
assertThat(handler.canHandle("UNKNOWN")).isFalse();
}
}Here's how a mainframe COBOL program would connect to this server:
* COBOL client connecting to IMS Connect Java Server
IDENTIFICATION DIVISION.
PROGRAM-ID. ACCOUNT-INQUIRY.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-IMS-HEADER.
05 WS-IRM-LENGTH PIC 9(8) COMP.
05 WS-IRM-ARCH PIC X(4) VALUE 'IRM1'.
05 WS-IRM-FLAGS PIC 9(4) COMP VALUE 1.
05 WS-TXN-CODE PIC X(8) VALUE 'BALINQ '.
05 WS-CLIENT-ID PIC X(8) VALUE 'COBOL001'.
01 WS-BALANCE-REQUEST.
05 WS-REQ-TXN-CODE PIC X(8) VALUE 'BALINQ '.
05 WS-REQ-ACCOUNT PIC X(16).
05 WS-REQ-CUSTOMER PIC X(12).
05 FILLER PIC X(50) VALUE SPACES.
01 WS-BALANCE-RESPONSE.
05 WS-RESP-TXN-CODE PIC X(8).
05 WS-RESP-CODE PIC X(4).
05 WS-RESP-ACCOUNT PIC X(16).
05 WS-RESP-BALANCE PIC 9(15).
05 WS-RESP-CURRENCY PIC X(3).
05 WS-RESP-STATUS PIC X(1).
05 FILLER PIC X(50).
PROCEDURE DIVISION.
MAIN-LOGIC.
MOVE '1234567890123456' TO WS-REQ-ACCOUNT
MOVE 'CUST123 ' TO WS-REQ-CUSTOMER
* Connect to IMS Connect Java Server
CALL 'HWSSMPL1' USING
WS-IMS-HEADER
WS-BALANCE-REQUEST
WS-BALANCE-RESPONSE
IF WS-RESP-CODE = '0000'
DISPLAY 'Account Balance: ' WS-RESP-BALANCE
DISPLAY 'Currency: ' WS-RESP-CURRENCY
ELSE
DISPLAY 'Error: ' WS-RESP-CODE
END-IF
STOP RUN.This IMS Connect Java Server acts as a gateway that:
- Receives IMS Connect protocol messages from mainframe clients (COBOL programs, etc.)
- Parses the EBCDIC-encoded messages and IRM headers
- Routes transactions to appropriate business logic handlers based on transaction codes
- Processes the business logic (account lookups, transfers, etc.)
- Returns properly formatted EBCDIC responses back to the mainframe clients
Mainframe Client (COBOL)
↓ (EBCDIC message via IMS Connect protocol)
IMS Connect Java Server
↓ (Parse & Convert to ASCII)
Transaction Handler (Java business logic)
↓ (Call business services)
Database/External Systems
↓ (Return results)
Transaction Handler
↓ (Format & Convert to EBCDIC)
IMS Connect Java Server
↓ (IMS Connect protocol response)
Mainframe Client receives response
Spring Boot Actuator Endpoints:
# Basic health check
curl http://localhost:8080/actuator/health
# Detailed health check (includes system message validation)
curl -u admin:admin http://localhost:8080/actuator/health
# System message health check (internal PING test)
curl http://localhost:8080/actuator/health/systemMessageIMS Connect Protocol Health Checks:
# Direct PING to IMS Connect port
echo "PING HEALTH_CHECK" | nc localhost 9999
# Server status via STATUS message
echo "STATUS" | nc localhost 9999
# Echo test for connectivity
echo "ECHO CONNECTION_TEST" | nc localhost 9999Core Metrics:
ims_connect_active_connections- Current active connectionsims_connect_pool_size- Connection pool size by backendims_connect_transactions_total- Total transactions processedims_connect_transaction_duration_seconds- Transaction response timesims_connect_backend_health_status- Backend health status
OTMA Metrics:
ims_connect_otma_conversations_active- Active conversational transactionsims_connect_otma_conversations_total- Total conversations startedims_connect_otma_messages_total- OTMA messages processed by typeims_connect_protocol_routing_total- Protocol routing decisions (OTMA vs Legacy)
System Message Metrics:
ims_connect_system_messages_total- System messages by type (PING, NOTIFY, ECHO, STATUS)ims_connect_ping_response_time_seconds- PING response timesims_connect_health_check_status- System message health check results
Security Metrics:
ims_connect_racf_validations_total- RACF security validationsims_connect_security_failures_total- Security validation failuresims_connect_audit_events_total- Audit events logged
Structured JSON logs are written to multiple files:
logs/ims-connect-server.log- Application logslogs/ims-connect-audit.log- Audit trail (JSON format)logs/ims-connect-metrics.log- Performance metricslogs/ims-connect-errors.log- Error-only logs
ims-connect:
security:
ssl:
enabled: true
keystore-path: "config/ssl/server.p12"
keystore-password: "${SSL_KEYSTORE_PASSWORD}"
truststore-path: "config/ssl/truststore.p12" # For mutual auth
truststore-password: "${SSL_TRUSTSTORE_PASSWORD}"
enabled-protocols: ["TLSv1.3", "TLSv1.2"]HTTP Basic authentication for management endpoints:
- Username:
admin/ Password:admin(change in production) - Username:
monitor/ Password:monitor
-
Missing .env file
# Run setup script ./scripts/dev-setup.sh # Or manually copy template cp .env.example .env
-
Environment variables not loading
# Check if development profile is active grep "Active profiles" logs/ims-connect-server.log # Verify .env file exists and has correct values cat .env | grep -v PASSWORD # Start with explicit development profile mvn spring-boot:run -Dspring-boot.run.profiles=development
-
SSL Certificate Issues (Development)
# Regenerate development certificates rm -rf config/ssl/dev-*.p12 ./scripts/dev-setup.sh # Verify certificate keytool -list -keystore config/ssl/dev-keystore.p12 -storepass devpassword
-
Authentication Failed
# Check if passwords are loaded from .env grep ADMIN_PASSWORD .env # Test with default credentials curl -u admin:admin123 http://localhost:8080/actuator/health
-
Connection Refused
# Check if backends are reachable telnet mainframe-host 9999 # Verify configuration kubectl get configmap ims-connect-config -o yaml
-
SSL Handshake Failures
# Verify certificates openssl s_client -connect mainframe-host:9999 -cert client.pem # Check Java keystore keytool -list -keystore config/ssl/keystore.p12
-
High Memory Usage
# Check JVM settings kubectl describe pod ims-connect-server-xxx # Monitor heap usage curl http://localhost:8080/actuator/metrics/jvm.memory.used
# View audit logs
tail -f logs/ims-connect-audit.log | jq .
# Search for errors
grep "ERROR" logs/ims-connect-server.log
# Monitor transactions
grep "TRANSACTION" logs/ims-connect-audit.log | jq '.eventType, .success, .transactionCode'For development, performance settings are optimized for quick startup:
# In .env file
JVM_OPTS=-Xmx1g -Xms512m -XX:+UseG1GC
# Or via environment variable
export JAVA_OPTS="-Xmx1g -Xms512m -XX:+UseG1GC"
./scripts/run-dev.shexport JAVA_OPTS="-Xmx2g -Xms1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"ims-connect:
server:
boss-threads: 2 # Number of CPU cores
worker-threads: 16 # 2x CPU cores
backlog-size: 2048 # Increase for high loadims-connect:
pool:
min-connections-per-backend: 5
max-connections-per-backend: 50
connection-timeout-ms: 3000The project includes comprehensive examples in the com.cgi.icbc.imsconnect.examples package:
- Transaction Handlers:
examples.handlers- Sample implementations for banking transactions - Services:
examples.service- Mock business logic services - Configuration:
examples.config- Auto-configuration for examples
Important: Examples are disabled by default and should only be enabled in development environments:
ims-connect:
examples:
enabled: true # Only for development/testingExamples demonstrate:
- OTMA conversational transactions
- RACF security integration
- EBCDIC message processing
- Error handling patterns
- Transaction lifecycle management