Skip to content

Release Notes 1.2.0

Kevin Herron edited this page Oct 12, 2026 · 10 revisions

This guide covers upgrading from Milo 1.1.x to 1.2.0-RC1 on Java 17. It has two parts: the significant new features in 1.2.0, and the changes that may require code changes in your application. Changes already released in 1.1.x, through 1.1.8, are not repeated here.

The guide describes the v1.2.0-RC1 tag at 69cb250878048b8b39ce7cde036f06c5617f31b2 (2026-10-12). RC1 is a release candidate. The final 1.2.0 release may still change.

Recompile applications and extensions against one consistent Milo version. 1.2.0 includes source and binary compatibility breaks.

New features

ECC and RSA-DH security policies

The stack adds 12 ECC policies, covering NIST P-256/P-384, Brainpool P256r1/P384r1, and Curve25519/448 with AES-GCM or ChaCha20-Poly1305, plus RSA_DH_AesGcm and RSA_DH_ChaChaPoly. Milo implements their SecureChannel paths and username and X.509 Session authentication. Peer interoperability still depends on compatible certificates, token policies, and crypto providers. SecurityPolicy.getProfile() describes each policy's algorithm, certificate, and channel requirements.

Milo clients must use forward OPC TCP for ECC or RSA-DH Sessions at this baseline. ReverseTcpClientTransport does not supply the channel thumbprint required by enhanced Session authentication. See Transport extensions.

To enable an ECC policy, provision compatible ECC application identities on both client and server, configure the server endpoint, and select it on the client. The default server username token policy explicitly names Basic256Sha256 and is rejected on an ECC channel with Bad_SecurityPolicyRejected. Use a username token policy with no securityPolicyUri (inherit the channel policy), or a compatible ECC policy URI.

The server advertises an endpoint only when the runtime provider and certificate support it. Only the AEAD-suffixed ECC policy URIs are supported. Brainpool uses the Bouncy Castle dependency already supplied by Milo. Brainpool certificate-path validation registers that provider globally at lowest precedence if it is absent. The other enhanced paths can use an unregistered BC instance.

Aes256_Sha256_RsaPss now works with the built-in JDK 17+ providers, without registering BouncyCastleProvider. Milo 1.1.x required that registration for its SHA256withRSA/PSS implementation.

See Security policies, the stack security package, and the client identity, certificate group, and token policy changes before enabling them.

Reverse Connect

Clients can accept server-initiated OPC TCP connections. OpcUaClient.createReverseConnect(...) uses either a shared ReverseConnectManager plus selector or a pre-claimed connection. ReverseConnectAcceptor runs GetEndpoints discovery for each deduplicated candidate before creating a client.

The manager owns the listener sockets. The application starts and stops the manager and disconnects delivered clients. Closing an acceptor or disconnecting one client does not stop the shared manager. Disconnecting a manager-backed client stops rearming and fails pending channel acquisition. A later explicit connect can register again.

A pre-claimed connection is one-shot. After failure, create a new client from a new claim. Cancelling or exceptionally completing ReverseConnectRegistration.connectionFuture() (including orTimeout) removes its selector and closes a claim that can no longer be delivered. CLAIMED means ownership was reserved, so wait for the future before using the connection. For delivered-connection ownership and the ReverseConnectRegistration.close() race, see Select, claim, and discover.

On the server, OpcUaServer.addReverseConnectTarget(...) registers a retrying outbound target and returns its control handle. Server attempts and custom retry policies run on the server executor. A rejected attempt startup reports FAILED with Bad_ResourceUnavailable. If attempt startup or retry evaluation is rejected, the target retries after its registration period without calling the custom policy. Keep callbacks short, because rejected listener dispatch can run inline on the submitting thread.

Treat ReverseHello as an unauthenticated routing hint. Normal SecureChannel and Session checks authenticate the peer after the connection is claimed. Reverse Connect requires peer support and does not apply to HTTPS or WebSocket. See Reverse Connect for selection, timeouts, and shutdown.

ECC and RSA-DH Sessions are outside the supported baseline for Milo clients using Reverse Connect. Of the bundled client transports, only OpcTcpClientTransport defers channel-bound request construction and supplies the active channel thumbprint. ReverseTcpClientTransport inherits an empty thumbprint and eager request construction. Use forward OPC TCP for those policies. See Transport extensions.

Node instantiation

OpcUaServer.getNodeInstantiator() builds typed object and variable instances from an InstantiationRequest. It supports inherited declarations, optional members, placeholders, explicit NodeIds, staged callbacks, and Method binding. instantiate() performs the operation. describe(), plan(), and apply() expose its stages.

InstantiationResult.deleteCreated() removes recorded new nodes and references, including references added to pre-existing parents. It preserves reused nodes and later replacements. It does not undo bindMethod changes to shared or reused Methods. A failed apply rolls back and returns no result. Keep application resources and binding ownership separate from node cleanup.

For ordinary events, OpcUaServer.newEvent(typeId) returns a TransientEvent that owns temporary nodes. Close it after use. getEventInstantiator() provides explicit control over event identity and generated Java type.

apply() revalidates the model and rejects changed declarations with a MODEL_CHANGED diagnostic before constructing nodes. Call server.getTypeModelCache().invalidate(typeId) after application model edits and re-plan. Snapshots copy standard OPC UA values, but application values without a standard codec must be immutable, including such values nested inside standard structures.

The package is experimental for one minor release. See legacy node factories and Types and instantiation.

Alarms and Conditions

The server SDK adds a Condition runtime: Condition, AcknowledgeableCondition, and AlarmCondition behaviors, discrete and limit alarms, acknowledgement, confirmation, shelving, and ConditionRefresh. create builds a new instance, and attach activates a complete generated instance. adopt completes a partial instance in place, preserving identities and stored configuration unless explicitly overridden. OpcUaServer.getConditionManager() owns behavior registration and lookup. Limit alarms can evaluate values or accept application-driven state. Condition branches other than the trunk (Part 9 BranchId) are not supported.

Custom Condition types and Java behaviors use Condition.createInstance, attachInstance, and adoptInstance. The returned behavior is unregistered. These factories own structure and Method wiring, while the application supplies custom state semantics. A custom factory must wrap the supplied node. Failure after creation rolls back the new tree, but failure after adoption leaves the completed existing tree for correction and retry. See custom types and ownership.

Register the behavior, connect its event source hierarchy, and drive changes through its API. Stop the process feed before unregistering. Unregistration or replacement under the same ConditionId retires the old behavior and releases handlers and timers. Re-registering that instance does not restore those resources, so construct a fresh behavior for reuse. Retirement does not delete nodes or prevent later direct application calls from trying to emit events. Delete owned nodes afterward.

captureSnapshot() and pre-live restoreSnapshot() support recovery. The application stores snapshots, preserves ConditionIds, and restores before exposing the behavior. See Alarms and Conditions, the conditions package, and the alarm example.

Alias Names

The opt-in org.eclipse.milo.opcua.sdk.server.aliases package implements OPC UA Part 17 for the alias model already in namespace 0. AliasManager provides recursive wildcard search, category and alias mutation, reference-type filtering, and LastChange version maintenance.

The application starts AliasManager after server startup and shuts it down before the server. It owns alias NodeIds, categories, authorization, and the AliasVersionStore. The default store is in memory, but Part 17 requires LastChange to persist across restarts. Supply a durable store, restore alias definitions yourself, and retain removed category versions if NodeIds may be reused.

Network mutation methods are disabled and denied by default. Mutations support local targets only. Search does not inherit Browse visibility filtering, so enforce access through checkFind and includeResult.

See manager setup and default limits for the 1,000-result, 512-character, and 1,000-operation bounds, and persistence for version ownership. The aliases package and AliasNamesExample provide the API reference and workflow.

GDS Pull client

Add org.eclipse.milo:milo-sdk-client-gds at the same version as the SDK. GdsClient.create(connectedClient) resolves the GDS namespace and Directory and registers the generated model. It throws UaException with Bad_NotFound if that namespace is absent. The client's blocking and asynchronous methods cover application registration, certificate requests, certificate groups, and trust lists.

resolveCertificateTypeId(...) matches a desired certificate type to a GDS group's advertised types. It returns null when only compatible abstract ancestor types are offered, meaning "use the group default." Pass that null through to the request. Incompatible choices fail with Bad_NotSupported.

TrustListReader reads a remote trust list. TrustListApplier replaces each list named in SpecifiedLists in one TrustListManager.update. Local entries absent from a selected list are dropped.

The application owns connecting, scheduling enrollment, polling FinishRequest, persisting application and request IDs, and validating and installing issued certificates. GdsClient.verifyIssuedCertificate checks the issued identity before installation. The client does not retry these workflows. milo-sdk-client-gds-testing publishes FakeGdsNamespace for tests. It is not a production GDS. See the GDS client guide and GdsPullExample.

Server lifecycle participants

OpcUaServer.addLifecycleParticipant(Lifecycle) transfers startup and shutdown ownership to the server. Register before startup. Registration after startup or shutdown begins throws IllegalStateException. Do not also start or stop a participant independently.

Participants start in registration order after core facilities are ready and before binding endpoints, then stop in reverse order. Failed startup rolls back only participants whose startup completed normally. The participant that throws must clean up its own partial allocation. See Server configuration.

Sampling framework

ManagedAddressSpace owns a SamplingManager, with one SamplingGroup per supported interval. A driver supplies a group to batch device reads while retaining framework scheduling, membership, initial samples, and access refresh. SamplingManagerConfig controls bucketing, minimum interval, initial-sample debounce, read-access policy, and the executor that groups run on.

Groups run on the server's executor by default. A sampler whose device reads block can move that work with SamplingManagerConfig.withExecutor(...), such as a virtual thread per task on Java 21 or later. The executor must run tasks on its own threads. The application owns it and shuts it down after the manager stops. Timers stay on the server's scheduled executor, and a rejected turn is logged and skipped until the group's next cycle.

The default policy calls AccessController on sampling threads every cycle for each Session. A cached policy avoids repeated checks until invalidation. Controllers must now tolerate these concurrent calls as well as ActivateSession and transfer checks, not only service requests. Keep controllers and access-attribute filters fast and thread-safe, and never call the controller recursively from a filter it uses.

The SDK invalidates on Session identity or endpoint changes, reactivation, and transfers, and evicts closed Sessions. Applications invalidate after changes to access attributes, roles, policy inputs, and node removal or replacement. See Sampling and read access for migration and Sampling for configuration and batch examples.

Monitored-item read access

A data MonitoredItem denied with Bad_NotReadable or Bad_UserAccessDenied is now created and reports the denial through Publish. Framework sampling refreshes its stored decision according to the selected policy. The SDK also rechecks after Session identity or endpoint changes and transfer. Custom samplers must implement refresh for application policy changes themselves. A denied item does not recover merely because a device value changes.

For the Value attribute, DefaultAccessController distinguishes a node lacking CurrentRead or CurrentWrite from a Session lacking it. Other denials, including protected RolePermissions reads and UserWriteMask checks, can still return Bad_UserAccessDenied. Install custom controllers with setAccessControllerFactory(...). See Sampling and read access and Access control.

Server certificate rotation

On a secured endpoint, a SecureChannel failure after Hello and Acknowledge can trigger rediscovery and retry with a renewed server certificate. The trigger is not limited to certificate errors. The replacement must preserve endpoint URL, server ApplicationUri, GatewayServerUri, transport profile, policy, mode, and user-token policies. Its certificate must also pass the client's validator. Recovery requires a ChannelStateObservable transport, as implemented by TCP and reverse TCP.

OpcUaClient.create(String) installs an EndpointResolver, but selects a None endpoint with anonymous access. Use the four-argument discovery-based create overload with a secured endpoint selector for rotation recovery. create(OpcUaClientConfig) does not install a resolver. Set one with OpcUaClientConfigBuilder.setEndpointResolver(...) when assembling configuration yourself.

Session.getOriginalServerCertificate() returns a SessionServerCertificate record retaining the CreateSession certificate and key material. Retained Sessions can reactivate across rotation when their security bindings remain usable. Changing the client's leaf certificate requires a new Session, but changing only its issuer chain does not.

An OpenSecureChannel request naming an unknown receiver certificate thumbprint now fails with Bad_CertificateInvalid, replacing Bad_SecurityChecksFailed. See Client recovery.

Method handler results and per-object binding

AbstractMethodInvocationHandler.invokeResult(...) can return a complete Call result, including Uncertain with outputs or per-argument Bad_InvalidArgument results. Override getRequiredInputArgumentCount(...) to accept omitted trailing inputs.

UaMethodNode.setInvocationHandler(NodeId, ...) installs a handler for one ObjectId. MethodBindings validates and owns registrations. ManagedAddressSpace selects these handlers, after giving ConditionManager handlers precedence, and falls back to the Method's default handler. A custom AddressSpace that dispatches Calls itself must implement equivalent selection. See Method handlers and Server Methods.

Client operation limit overrides

setOperationLimitOverrides accepts either a map or Function<OperationLimit, Optional<UInteger>>. Pass null to the function overload to remove overrides. An absent or zero override leaves the advertised value unchanged. Otherwise, a nonzero override supplies a missing or zero server limit, or lowers an advertised nonzero limit. It never raises that server limit. The effective limits are cached per Session activation.

SDK consumers such as type discovery and OpcUaSubscription MonitoredItem batching use these effective values. Arbitrary bulk Read, Write, and Call inputs are not automatically partitioned. Applications must split them and preserve per-operation results.

The type-tree fallback to one operation when a limit is unavailable predates 1.2. The legacy binary dictionary reader now uses 64 nodes per Read when the effective MaxNodesPerRead is zero or unavailable. Milo 1.1.x used one when the advertised value was zero. See Recovery and limits.

Changes that may require code changes

Change Who is affected
Client certificate identity Clients that configure an application certificate or leave their application URI unset
Server certificate groups Servers, custom certificate group or manager implementations, and custom or subclassed certificate validators
Custom trust list managers Custom TrustListManager implementations, subclasses of the built-in managers, and file-store users with hidden trust material
Certificate revocation checking Applications validating CA-issued peers, including default validators with CRLs; users of --add-opens or revocation flags
Endpoints and user token policies Servers with endpoint aliases or ECC username policies; custom username choosers and X.509 identities
Security algorithm and policy APIs Code that uses SecurityAlgorithm or SecurityPolicy directly; custom identity providers and subclasses of built-in providers
Event routing Servers that emit events or manage event items
Alias Method nodes Servers with their own FindAlias handlers
Method handlers Handlers returning argument diagnostics or throwing InvalidArgumentException; callers using an ObjectType as the Method ObjectId; UaMethodNode subclasses overriding getInvocationHandler()
Legacy node factories Servers using NodeFactory or EventFactory
Server lifecycle Custom lifecycle components and code that invokes startup or shutdown repeatedly
Custom identity validators Servers with custom IdentityValidator implementations
Attribute reads and writes Servers with Variables that accept arrays but declare no array ValueRank; code that synchronizes on a node to group attribute changes
Client API behavior Clients browsing, using typed-node writers, or inspecting connect failures
Transport extensions Custom transports, direct stack users, server transport configuration
Serialization JSON and XML users; code that encodes, decodes, or persists Matrix values; custom codecs and data-type managers
Diagnostics nodes Clients browsing per-Session diagnostics with default EnabledFlag, caching diagnostic NodeIds, or interpreting disabled status
Sampling and read access Servers using SubscriptionModel, custom samplers or controllers; imports of the old access classes, getAccessController overrides, and checkReadAccess or checkWriteAccess callers; Variables without CurrentRead or CurrentWrite; clients matching access statuses or CreateMonitoredItems failures
Deprecations Users of deprecated APIs

Client certificate identity

The OpcUaClientConfig builder no longer has separate setKeyPair, setCertificate, and setCertificateChain methods. Use setCertificateIdentity(keyPair, chain), passing the leaf certificate first, followed by any issuers. Keep your existing certificate validator.

Before (Milo 1.1.x; given endpoint, keyPair, chain, validator, and an applicationUri matching the certificate):

var config =
    OpcUaClientConfig.builder()
        .setEndpoint(endpoint)
        .setApplicationUri(applicationUri)
        .setKeyPair(keyPair)
        .setCertificate(chain[0])
        .setCertificateChain(chain)
        .setCertificateValidator(validator)
        .build();

After (Milo 1.2.0-RC1):

var config =
    OpcUaClientConfig.builder()
        .setEndpoint(endpoint)
        .setApplicationUri(applicationUri)
        .setCertificateIdentity(keyPair, chain)
        .setCertificateValidator(validator)
        .build();

For managed trust or multiple certificate types, use setCertificateGroup(group) instead. The last of the two calls wins. DefaultCertificateGroup.forIdentity(...) combines one identity with application trust, quarantine, and validation.

OpcUaClientConfig.getKeyPair(), getCertificate(), and getCertificateChain() are removed. Keep the material your application supplies, or call client.getCertificateIdentity(policy.getProfile()). It takes a SecurityPolicyProfile, returns Optional<CertificateIdentity>, and can throw UaException.

OpcUaClientConfig.getApplicationUri() now returns Optional<String> holding only an explicitly configured URI. Milo 1.1.x sent urn:eclipse:milo:client:applicationUriNotConfigured when none was set. In 1.2 Milo uses the selected certificate's SAN URI, or on None connections the SAN URI shared by the group's identities. The same placeholder remains the fallback when no SAN URI is available. Call setApplicationUri(...) if server or GDS registrations depend on the prior value. client.resolveApplicationUri(identity) returns the effective URI.

CreateSession now sends the configured client certificate chain. Milo 1.1.x sent only its leaf.

On the setCertificateIdentity path, build() throws IllegalArgumentException for an empty chain, unsupported type, or key mismatch. A secured connection without a compatible identity fails with Bad_ConfigurationError. Inferred certificate types exclude SHA-1-signed RSA certificates from Basic256Sha256 and the two AES RSA policies. An explicitly typed group instead selects using the stored type, so this inference check is not a general certificate-validation substitute.

Server certificate groups

DefaultApplicationGroup is removed. Use DefaultCertificateGroup in org.eclipse.milo.opcua.stack.core.security. In 1.1.x, group initialization created missing certificates, and the application already initialized and closed its trust and store resources.

In 1.2, provisioning is explicit. Call factory.createMissingCertificates(group) before startup. This instance method returns the certificate-type NodeIds created and can throw Exception. Existing entries are retained, and unreadable entries are not silently replaced. An existing deprecated RsaSha256CertificateFactory inherits this method, so it can be retained while migrating.

Before (Milo 1.1.x; given initialized trust and store, plus factory, quarantine, and validator):

var group = DefaultApplicationGroup.createAndInitialize(trust, store, factory, validator);
var manager = new DefaultCertificateManager(quarantine, group);

After (Milo 1.2.0-RC1):

var group = new DefaultCertificateGroup(trust, store, quarantine, validator);
factory.createMissingCertificates(group);
var manager = new DefaultCertificateManager(group);

The four-argument group constructor supports RSA SHA-256 only. Use the overload with a List<NodeId> of certificate types for others. The one-group manager constructor registers the group as the DefaultApplicationGroup. For additional groups, use new DefaultCertificateManager() and addCertificateGroup(groupId, group). As in 1.1.x, close application-owned trust and persistent store resources after the server stops. KeyStoreCertificateStore is closeable. MemoryCertificateStore holds no closeable resource.

Custom implementations need these changes:

1.1.x API 1.2.0 replacement
CertificateGroup.getCertificateGroupId() Implement the new abstract CertificateManager.getCertificateGroupId(group), returning Optional<NodeId>
CertificateGroup.getCertificateFactory() Keep the factory in application code
No quarantine or presence methods on the group Implement getCertificateQuarantine() and hasCertificate(NodeId)
CertificateGroup.Entry(groupId, typeId, chain) with public fields Record Entry(typeId, chain) with certificateTypeId() and certificateChain()
CertificateManager.getCertificateQuarantine() (removed) The validating group's quarantine; getRejectedCertificates() aggregates across groups
DefaultCertificateManager(quarantine, ...) constructors (removed) Empty manager plus group registration, or DefaultCertificateManager(group)

The stack now calls the four-argument CertificateValidator.validateCertificateChain(..., profile). Its default invokes legacy validation, then checks certificate and profile compatibility. A wrapper that forwards only the three-argument method bypasses its delegate's profile-specific checks. Forward the four-argument overload too when wrapping a validator that supplies those checks.

Subclasses of DefaultClientCertificateValidator or DefaultServerCertificateValidator that override only the three-argument method still compile, but that override is bypassed by SDK calls. These built-in classes implement the four-argument overload directly. Move custom checks, such as certificate pinning, to that overload and pass the same profile when calling super.

Fixed endpoint certificates remain supported. Unlike 1.1.x, the builder permits a secured endpoint without a fixed certificate. The server selects an identity from the default group, or from an explicit EndpointCertificateConfig. As before, a secured policy requires Sign or SignAndEncrypt mode. An unsatisfied identity or provider requirement omits the endpoint with a warning, so verify bound endpoints. See CertificateGroup, DefaultCertificateGroup, and Server configuration.

KeyStoreCertificateStore now uses <prefix>ecc-* aliases for ECC identities. Applications that manage aliases directly must use the new names. The RSA SHA-256 alias is unchanged. contains() no longer treats a certificate-only entry as an application identity. It requires a cached identity or a keystore key entry.

SelfSignedCertificateBuilder chooses SHA384withECDSA for EC keys whose curve order exceeds 256 bits, and SHA256withECDSA for smaller curves, unless overridden.

Custom trust list managers

TrustListManager adds getSnapshot(), update(UnaryOperator<TrustListSnapshot>), and replaceAll(...) so a four-list change can be applied atomically. The built-in managers implement them. Direct TrustListManager implementations still compile with the interface defaults, which fall back to sequential getters and setters and are not atomic. Override them if validators or GDS updates must see a consistent trust list. Update functions can run more than once, so keep them free of side effects.

Subclasses of FileBasedTrustListManager or MemoryTrustListManager that override individual getters still compile, but the default certificate validators now read getSnapshot() instead. The built-in managers implement that method directly, so those getter overrides no longer affect validation. Move additions or filtering of certificates and CRLs to a getSnapshot() override built from super.getSnapshot(), and keep the individual getters consistent with it.

File-based atomic publication is not an on-disk transaction. A failed file write or deletion is logged, but the published snapshot still reflects the requested state. A later directory reload reconciles it with disk. Monitor persistence failures before relying on changes across restart.

TrustListSnapshot removes duplicate entries from each of its four lists, preserving the first occurrence order. Do not use duplicate entries as application metadata.

File-based trust and quarantine stores now skip dot-prefixed files and subdirectories when loading certificates or CRLs. Keep active trust material in regular, non-hidden files. Files with other names continue to be decoded regardless of extension. As in 1.1.x, decode failures are logged and skipped.

Certificate revocation checking

Revocation checking now uses the JDK's public PKIX revocation checker. Remove --add-opens java.base/sun.security.provider.certpath=ALL-UNNAMED from your JVM options. It is no longer needed. ValidationCheck.REVOCATION is deprecated for removal and has no effect. The checker uses CRLs only, with no OCSP fallback. JVM-wide ocsp.* properties do not configure it.

Configuration 1.1.x without --add-opens 1.1.x with --add-opens, and 1.2.0
No revocation flags (the default) Revoked peers and missing CRLs are accepted Revoked peers are rejected; missing CRLs are tolerated
REVOCATION only Revoked peers and missing CRLs are rejected Revoked peers are rejected; missing CRLs are tolerated
REVOCATION_LISTS only Revoked peers and missing CRLs are accepted Revoked peers and missing CRLs are rejected
Both flags, or ALL_OPTIONAL_CHECKS Revoked peers and missing CRLs are rejected Revoked peers and missing CRLs are rejected

These rows describe CA-issued peers with otherwise valid trust paths. An applicable clear CRL permits validation in each case. Applications that already ran 1.1.x with --add-opens keep the same behavior. REVOCATION becomes a no-op, and the JVM opening is no longer needed. The table was measured on Java 17, with the --add-opens column measured in a separate JVM for the 1.1.x comparison. The measurement did not include network CRL retrieval.

Check trust-list CRLs against peers you expect to accept. If an applicable CRL is unavailable locally, the JDK checker can fetch it from CRL distribution points, delaying validation and the handshake. 1.1.x without the opening did not fetch these CRLs. Milo 1.2 explicitly installs a PKIXRevocationChecker, so com.sun.security.enableCRLDP cannot disable its fetching. On isolated networks, provision current issuer CRLs in the trust list.

OpenJDK 17 defaults to a 15-second connection timeout and a 15-second read timeout per CRL fetch. The system properties com.sun.security.crl.timeout and com.sun.security.crl.readtimeout control them. Values are whole seconds unless suffixed with ms, so 5 and 5000ms both mean five seconds. Zero means an infinite timeout. These are JDK provider settings, not a total validation deadline. Confirm the syntax and behavior for your deployed JDK. See the OpenJDK 17 CRL store source.

The public classes OpcUaCertPathValidator, OpcUaCertificateRevocationChecker, and OpcUaCertificateValidityChecker in org.eclipse.milo.opcua.stack.core.util.validation were removed. Code that directly imported them must migrate to the supported certificate validators or the public JDK PKIX APIs.

DefaultServerCertificateValidator now masks Bad_CertificateRevocationUnknown and Bad_CertificateIssuerRevocationUnknown as Bad_SecurityChecksFailed. Untrusted and revoked certificates were already masked. Path-building failures retain Bad_SecurityChecksFailed, as in 1.1.x. The client validator retains the builder exception as the cause, but the server validator omits it. Update code that matches the newly masked revocation statuses.

Endpoints and user token policies

Server endpoint selection is now deterministic. Endpoints that share transport, URL path, security policy, mode, and certificate must have equivalent user token policies, even when their host names or ports differ. Otherwise, startup fails with Bad_ConfigurationError. Combine token policies on one endpoint, or distinguish the endpoints by path, policy, or mode. A certificate can also distinguish secured endpoints, but it is not part of the key for None endpoints. Endpoints sharing a selection key must also advertise identical certificates, including None endpoints.

Session reactivation decodes user tokens against the replacement channel's selected endpoint policies. Custom identity validators should use the Session's current endpoint rather than cache the endpoint from its first activation.

As in 1.1.x, an unsecured channel without a configured None endpoint can only be used for discovery. Configure a None endpoint if you intend to allow unsecured sessions. The server may leave out an endpoint whose certificate or provider is unavailable, so check GetEndpoints output.

A None endpoint offering RSA-encrypted username tokens still needs a compatible server certificate. Provision it before advertising that token policy. New in 1.2, if no fixed certificate or EndpointCertificateConfig is set and the default group has no compatible identity, startup warns and still advertises the None endpoint without a certificate. An unsatisfied explicit EndpointCertificateConfig instead omits the endpoint. As in 1.1.x, a Milo client detects an absent certificate locally and fails with Bad_ConfigurationError before sending the token.

New in 1.2, on secured channels a username token policy with an explicit non-None securityPolicyUri must use the channel's public-key family (RSA or ECC). Explicit None is exempt from the family rule.

As in 1.1.x, an omitted URI inherits the channel policy, explicit None leaves the password unencrypted at the token layer, and an omitted URI on a None endpoint has the same result. The password is exposed on a Sign-only or None channel in those cases. Choose SignAndEncrypt with certificate validation, or supply a chooser that requires a compatible encrypted username policy and fails when none is available. A chooser receives no endpoint argument, so classify inherited policies using the selected endpoint. See Match endpoint and token policies.

The default chooser still selects the first policy offered to it, but 1.2 filters that list to usable policies first. An explicit-None policy can still win over an encrypted one without a warning.

As in 1.1.x, on a None endpoint only UsernameProvider's own validator checks the certificate used to encrypt the password. Pass a validating CertificateValidator, such as DefaultClientCertificateValidator. The provider still defaults to an insecure validator. Setting only the client configuration validator does not cover this token-encryption certificate on a None endpoint. In 1.2, enhanced username secrets require a secured channel, and an enhanced certificate-token policy cannot be used over a legacy secured channel.

UsernameProvider filters usable policies before calling its custom chooser, but falls back to the full list if none is usable. The chooser may run more than once per activation and must be deterministic. X509IdentityProvider has no custom chooser. It now selects the first compatible Certificate policy rather than the first Certificate policy. If none fits, it fails with Bad_IdentityTokenRejected. It also stops sending unsigned X.509 tokens for None-resolved policies, which servers reject.

As in 1.1.x, Milo has no built-in issued-token client provider. A server application can override AbstractIdentityValidator.validateIssuedIdentityToken. Its default still rejects the token with Bad_IdentityTokenInvalid. In 1.2, that path does not apply UserTokenSecurityPolicyRules. The application must validate the issued-token policy, protection, signature, and identity. See User-token compatibility.

Security algorithm and policy APIs

If you build JCA Signature or Cipher instances from SecurityAlgorithm.getTransformation(), use SignatureFactory and CipherFactory in org.eclipse.milo.opcua.stack.core.util instead. The RsaSha256Pss and RsaOaepSha256 transformation strings changed, and those algorithms now need the parameters from SecurityAlgorithm.getAlgorithmParameterSpec(), which the factories apply.

For enhanced policies, getSymmetricSignatureAlgorithm(), getSymmetricEncryptionAlgorithm(), getKeyDerivationAlgorithm(), and getAsymmetricKeyWrapAlgorithm() return null. ECC policies also return null from getCertificateSignatureAlgorithm(). Their asymmetric signature and encryption getters return SecurityAlgorithm.None. RSA-DH also returns None from the asymmetric encryption getter.

Code iterating policies must account for these values. Prefer getProfile(), the find...Algorithm() methods, or isLegacy(). Update exhaustive switches for the new SecurityPolicy constants and the SecurityAlgorithm.HmacSha384 and Sha384 constants.

Custom identity providers for enhanced policies need the new IdentityProviderContext. The default IdentityProvider.getIdentityToken(IdentityProviderContext) calls the two-argument method. A subclass of UsernameProvider, X509IdentityProvider, or CompositeProvider that overrides only the two-argument method is bypassed when the SDK requests its token. Override getIdentityToken(IdentityProviderContext) instead and pass that context when delegating to super.

Use SelfSignedCertificateBuilder.forEccApplicationCertificate(...) to create ECC certificates.

Event routing

In 1.1.x, namespace 0 registered its event items with the event notifier, and application namespaces had to register their own. Registered items received events without source-hierarchy scoping, while unregistered application items received nothing. In 1.2, the SDK registers event items across namespaces. Items outside the Server Object receive only events from their own node or its HasNotifier or HasEventSource descendants. Server items still receive all events before filters. Application namespaces can therefore begin receiving traffic previously absent.

Set SourceNode and maintain both reference directions. UaNode.addReference and NodeManager.addReferences add the inverse automatically, but NodeManager.addReference does not. ManagedNamespace.registerEventNotifier(...) connects a notifier to Server. Connect its sources with HasEventSource. Organizes and HasComponent alone do not establish event scope. onEventItemsCreated, onEventItemsModified, and onEventItemsDeleted are notifications, so omitting old manual registration no longer suppresses delivery.

Invalid select clauses now produce null fields, while valid selections can still be delivered. In 1.1.x a failed select-clause validation suppressed the entire notification.

Event filter conversions from Float to an integer type now round through double and check the target range, as Double conversions already did. Milo 1.1.x clamped Float values above Integer.MAX_VALUE, so a Float of 3e9 cast to Int64 or UInt32 gave 2147483647. It now gives 3000000000, and a cast to Int32 out of range yields null instead of 2147483647.

ConditionRefresh markers and retained replay now target the requested Subscription. In 1.1.x the markers went to all registered items, including other Sessions. A target with no event items returns Bad_NothingToDo, and an overlapping refresh returns Bad_RefreshInProgress. See Events, Alarms and Conditions, and the scope tests.

Alias Method nodes

The standard FindAlias Method nodes now start with Executable and UserExecutable set to false until an AliasManager enables them. If you installed your own handler, either set both flags yourself or switch to AliasManager. Installing a handler does not change the flags.

Method handlers

Call argument diagnostics are returned only for operation-level ReturnDiagnostics bits (0x20 through 0x200). String indexes must come from DiagnosticsContext.addString, available through InvocationContext.getCallDiagnostics(). Handlers that invent their own string-table indexes must change.

AbstractMethodInvocationHandler maps malformed CallMethodResults to Bad_InternalError, including an InvalidArgumentException with the wrong number of per-input results. This check does not cover every raw MethodInvocationHandler. For those, the service replaces null results and statuses and discards malformed argument diagnostics, but does not apply all those result invariants.

AbstractMethodInvocationHandler.invoke is no longer abstract. Overriding neither invoke nor invokeResult compiles but returns Bad_NotImplemented at runtime.

Through ManagedAddressSpace, a modeled instance Method must be called with an instance ObjectId. Milo 1.1.x allowed its ObjectType to reach the declaration handler. Milo 1.2 returns Bad_MethodInvalid, or Bad_NodeIdInvalid for Condition instance Methods. Type-level Methods without a ModellingRule remain callable. Overriding protected getInvocationHandler(...) without delegating to super also bypasses this validation, ConditionManager dispatch, and per-ObjectId handler selection.

ManagedAddressSpace now selects a Method node's handler with UaMethodNode.getInvocationHandler(NodeId), which returns an ObjectId-specific handler or the stored default without calling getInvocationHandler(). A UaMethodNode subclass that overrides only the no-argument getter is bypassed for Calls. Install the handler with setInvocationHandler(...), or override the NodeId overload and apply the same customization to the handler returned by super.getInvocationHandler(objectId).

Legacy node factories

NodeFactory, EventFactory, and InstanceDeclarationHierarchy are deprecated but still work as before. When you migrate to the node instantiator, preserve persisted NodeIds.

Before (Milo 1.1.x; given OpcUaServer server, ManagedAddressSpace space, an existing parentId, instanceId (here a String ID File:Example in your namespace), and browseName):

UaObjectNode node =
    (UaObjectNode) space.getNodeFactory().createNode(instanceId, NodeIds.FileType);
node.setBrowseName(browseName);
node.setDisplayName(LocalizedText.english("File:Example"));
space.getNodeManager().addNode(node);
node.addReference(new Reference(instanceId, NodeIds.Organizes, parentId.expanded(), false));

After (Milo 1.2.0-RC1; import org.eclipse.milo.opcua.sdk.server.nodes.instantiation.InstantiationRequest):

var request =
    InstantiationRequest.of(UaObjectNode.class, NodeIds.FileType)
        .nodeId(instanceId)
        .browseName(browseName)
        .displayName(LocalizedText.english("File:Example"))
        .parent(parentId, NodeIds.Organizes)
        .legacyPathStrings()
        .target(space.getNodeManager())
        .build();
UaObjectNode node = server.getNodeInstantiator().instantiate(request).root();

Both versions create a mandatory Size child with the String ID File:Example/0:Size. legacyPathStrings() preserves this old formula for derived child NodeIds. Without it, IDs change for non-String roots, reserved prefixes, or String roots or member names containing \, /, or :. The String root File:Example contains :, so without the legacy strategy its child becomes File\:Example/0:Size. A numeric root 8 derives i=8/0:Size under the new default.

This example creates only the FileType model. An application must supply the file operations and attribute behavior before offering a usable file service.

Other differences to check when migrating:

  • Optional Methods are omitted unless selected with includeOptional(...).
  • The default ConflictPolicy.FAIL rejects NodeId collisions instead of silently replacing nodes. A collision found while planning fails with Bad_InvalidArgument and leaves the existing node unchanged. A collision during commit can report Bad_NodeIdExists. Choose another policy deliberately when reuse is required.
  • Supertype-declared mandatory members previously skipped by NodeFactory are now created. Mandatory descendants of an excluded optional are no longer created as orphaned nodes.
  • A subclass that overrode NodeFactory.instanceNodeId() must supply nodeIdStrategy(...) to preserve its custom identity formula.
  • Instances copy modeled permissions, AccessLevelEx, Historizing, and sampling interval from their declarations.
  • onNode callbacks see staged nodes, and Methods are bound after commit.
  • Custom NodeManager implementations get only best-effort rollback unless they implement the batch primitives. Defaults for removeNodeIfSame and removeReferenceIfSame require a single writer, so concurrent managers must make these checks and removals atomic. Cleanup relies on getReferences returning the same Reference instances that were added. Managers that rebuild references from storage must override removeReferenceIfSame.

The instantiation guide maps the old callbacks.

Server lifecycle

AbstractLifecycle.startup() now leaves the component STOPPED when onStartup() throws. In 1.1.x it stayed RUNNING. A later shutdown() does nothing, so release partially acquired resources inside onStartup() before throwing. LifecycleManager, including ManagedNamespaceWithLifecycle and ManagedAddressSpaceWithLifecycle, now shuts down the children that already started when a later child fails.

OpcUaServer could not restart in 1.1.x either, because repeated startup threw synchronously from its already-started EventFactory. In 1.2, repeated calls return the original startup future, even after failure or shutdown, and a failed startup rolls back acquired resources. A completed startup future therefore does not prove the server is still running. To restart, create a fresh server and owned resources. For a failed instance, finish shutdown.

Cancelling or manually completing the public startup future does not stop startup or let shutdown release resources early. Request shutdown through shutdown() and await its completion.

Custom identity validators

ActivateSession verification now uses the server certificate captured by the Session's CreateSession, allowing reactivation after rotation. Custom validators reading the current endpoint or channel certificate should instead obtain session.getOriginalServerCertificate(). This returns a SessionServerCertificate record. Use .certificate() for its X509Certificate or .createSessionCertificate() for the advertised ByteString. Validators using the SDK's decryptTokenData(...) already receive the corrected binding.

Attribute reads and writes

A Write now checks the value's shape for every ValueRank. Milo 1.1.x checked shape only for ValueRank 1 or more, so an array or Matrix written to a Scalar Variable returned Good. Server code that cast the stored value to its scalar type then threw ClassCastException. These writes now fail with Bad_TypeMismatch. A Variable built without setValueRank(...) is Scalar, so declare an array ValueRank, such as OneDimension or ScalarOrOneDimension, on Variables that accept arrays. Any still accepts every shape. A ValueRank below -3 is not defined by Part 3 and now rejects every value.

Writes through AttributeWriter now accept primitive arrays such as int[] wherever their boxed form is accepted. Milo 1.1.x rejected them with Bad_TypeMismatch, including client IndexRange Writes into a stored primitive array. A written Matrix.ofNull() is now treated as an empty Variant, so AllowNulls decides the result. An IndexRange Read or Write of a stored null Matrix returns Bad_IndexRangeNoData. In 1.1.x both cases threw NullPointerException, and the client request timed out without a response.

UaNode and its subclasses no longer take the node's monitor to read an attribute. Each read still sees the value most recently set for that attribute. Writes still hold the monitor while AttributeObservers run, but a concurrent reader can now see a new value before the observers have run. Code that holds a node's monitor while setting several attributes no longer keeps readers from seeing the intermediate state.

Client API behavior

AddressSpace.browse, browseAsync, browseNodes, and browseNodesAsync, and the UaNode methods that delegate to them, now fail with a UaException carrying the StatusCode of a Bad Browse or BrowseNext result. Milo 1.1.x returned the references collected so far. Browsing an unknown or deleted Node returned an empty list, and a Bad BrowseNext such as Bad_ContinuationPointInvalid returned the earlier pages as if they were the whole result. Code that treated an empty list as "nothing there" must now handle Bad_NodeIdUnknown. Uncertain pages now keep their references, and their ContinuationPoints are followed. These requests now go through OpcUaClient.sendRequestAsync, so ServiceFaultListeners see their faults.

The client type tree builders and LazyClientDataTypeTree also fail on a Bad BrowseNext result instead of building a tree without the missing subtypes or encodings. OpcUaClient.getDataTypeTree() does not cache the failure, so the next call builds again. LazyClientDataTypeTree records a failed resolution and retries it after clearFailedResolutions().

Most generated blocking model-property writers now throw UaException for a non-Good operation status where their 1.1.x counterparts discarded the returned status. The new GDS model has no 1.1.x counterpart.

At this source baseline, 23 generated methods with multiline signatures still discard status, including ServerDiagnosticsTypeNode.writeServerDiagnosticsSummary(...). Use the asynchronous property's returned StatusCode for explicit handling. UaVariableNode.writeValue(...) still has a different contract. It rejects Bad status but accepts Uncertain and updates its cached Value. The generated-writer integration test shows a writer that enforces status, and Writing describes caller choices.

SecureChannelHandshakeException is a UaException that preserves the underlying status. Blocking connect() already wrapped its cause in 1.1.x, but the new handshake wrapper can move an original UaServiceFaultException one level deeper. Code using getCause() instanceof UaServiceFaultException should traverse the cause chain or use UaException.extractStatusCode(...).

ChannelStateObservable.TransitionListener.onConnectFailure(Throwable) reports failed transport connection attempts. TCP transport disconnect() aborts an in-flight connection or handshake. SDK disconnectAsync() now waits at most five seconds for Session closure before initiating transport disconnect. This bounds the Session-close phase, not the entire cleanup operation.

OpcUaClientConfig.copy(...) now preserves the configured certificate validator. In 1.1.x a copied configuration lost it and could fall back to the insecure default validator.

UaNode.readWriteMask() now reads the WriteMask attribute. In 1.1.x it incorrectly read UserWriteMask. Use readUserWriteMask() when the user-specific value is intended.

Transport extensions

OPC TCP connection limits are now checked on every inbound connection. The defaults remain 100 per address and 10000 total. Gateways opening many connections from one address can see rejections earlier than in 1.1.x. Loopback remains exempt. Verify actual capacity after changing the limits. See Deployment.

The limits can now be set without code, through the system properties milo.stack.rateLimit.enabled, milo.stack.rateLimit.maxAttempts, milo.stack.rateLimit.windowMs, milo.stack.rateLimit.maxConnections, and milo.stack.rateLimit.maxConnectionsPerAddress. Assigning the Stack.ConnectionLimits fields before the first bind still works and takes precedence. Integer values must be positive, and enabled accepts only true or false. Any other value is logged at WARN and the default is kept. Stack.ConnectionLimits.HELLO_DEADLINE_MS is deprecated because nothing reads it. Set the Hello deadline with OpcTcpServerTransportConfigBuilder.setHelloDeadline(...).

ExecutionQueue now runs queued work inline when its executor rejects dispatch. Callers using custom executors must allow for task work on the submitting thread during rejection.

Channel failure now fails requests written to that channel. Requests waiting for a replacement channel survive only when they have a positive timeout hint, and that deadline includes the wait for a channel. Custom transports must fail channel-acquisition futures on terminal disconnect instead of leaving requests pending.

In subclasses of OpcTcpClientTransport, overrides of only sendRequestMessage(UaRequestMessageType) or getChannel() no longer see CreateSession or Session reactivation. The SDK sends those through sendRequestMessage(Callable<UaRequestMessageType>, long), which acquires its channel directly from the ChannelFsm. Override that overload as well if you customize sending or channel acquisition. Adapt custom getChannel() selection in that deferred path, because wrapping the supplier alone does not restore it. Preserve the wait for an established channel before invoking the supplier and the separate channel-wait and response timeouts.

AbstractUascClientTransport.sendRequestMessage(UaRequestMessageType) no longer calls the protected sendRequestMessage(UaRequestMessageType, Channel) overload. Subclasses that intercepted all requests in that protected method now miss ordinary sends, including initial ActivateSession. Move ordinary-send interception to the public one-argument method. For OpcTcpClientTransport, cover the public Callable overload too. When intercepting its request, wrap the supplier without evaluating it early and delegate through the matching super overload.

Custom transport, service-request, and channel code needs these changes:

1.1.x API 1.2.0 API
ClientApplicationContext.getKeyPair/getCertificate/getCertificateChain Implement Optional<CertificateIdentity> getCertificateIdentity(SecurityPolicyProfile) throws UaException; return empty when there is no identity for the profile, and throw when selection fails
ServiceRequestContext without endpoint access Implement Optional<EndpointDescription> getEndpoint(); empty means discovery-only
ServiceRequest and UascServiceRequest constructors Pass the nullable selected endpoint before the request
UascResponseHandler.handleChannelError(UaException) handleChannelError(Channel, UaException)
UascResponseHandler.handleChannelInactive() handleChannelInactive(Channel)
Protected pending-request maps and cancelRequestTimeout(long) in AbstractUascClientTransport pendingRequests is private with a new value type; pendingTimeouts and cancelRequestTimeout are removed. Use the base request and response path
ChunkEncoder.EncodedMessage(chunks, requestId) EncodedMessage(chunks, requestId, signature); pass null when there is no signature
OpcTcpServerTransportConfigBuilder.setExecutor, OpcServerTransportConfig.getExecutor(), UascServerConfig.getExecutor() Removed, as 1.1.x ignored them; use OpcUaServerConfigBuilder.setExecutor
UascServiceRequestHandler(UascServerConfig, ServerApplicationContext) UascServiceRequestHandler(ServerApplicationContext)

ServerApplicationContext.selectEndpoint(...) is a new default method. An override must return empty when matching endpoints are not Session-equivalent, rather than choose an ambiguous match.

A client transport with channel-bound requests must override OpcClientTransport.sendRequestMessage(Callable, long) to wait for the channel before building the request, and override getChannelThumbprint() to supply its binding. The default Callable overload builds immediately and preserves the existing request timeout. It does not wait for a channel. OpcTcpClientTransport supplies this override and bounds the channel wait by the supplied positive timeout, failing with Bad_Timeout. CreateSession and reactivation use the configured request timeout for that wait. The request header then bounds the response wait.

At this baseline, ReverseTcpClientTransport inherits the eager default and does not override the channel thumbprint accessor. Do not infer channel-bound enhanced Session support from its basic connection lifecycle. See ClientApplicationContext and UascResponseHandler.

Serialization

JSON now follows the OPC UA 1.05 mapping, with the differences described in Encodings. A JSON-native ExtensionObject places its fields inline beside UaTypeId. Binary and XML bodies remain opaque under UaEncoding and a base64 UaBody. XML bodies now use base64 UTF-8. JSON-native bodies already used the DataType ID in 1.1.x, while Binary and XML envelopes used their encoding ID. In 1.2 all three use the DataType ID in their JSON envelope.

A 1.1.x DataValue nests its Variant under Value, whereas 1.2 places the type and value beside the status and timestamps. The old nested DataValue fails with Bad_DecodingError. A bare scalar Int32 Variant, such as the value 42, can still decode, but compatibility depends on its payload.

An old JSON-native ExtensionObject envelope can parse before its structure-body decode fails. Legacy Binary and XML envelopes instead fail with Bad_DecodingError while 1.2 reads the envelope, because UaTypeId names an encoding rather than the DataType. A bare Variant containing one of those envelopes also fails. For XML, correcting the ID alone is not enough. The old XML text body must also become base64 UTF-8.

Keep an explicit format version, because trial decoding does not identify it. Milo supplies no automatic stored-data converter. See the two-process conversion workflow, including its model-specific limitations.

Null ByteStrings now encode as JSON null instead of empty strings. COMPACT null DataValue array entries now retain their positions instead of being dropped. VERBOSE null ExtensionObject array entries now use null instead of empty objects. Null String and LocalizedText entries already used null. Conversion alone cannot recover data lost by the old encoding, such as null versus empty ByteString.

Null DiagnosticInfo and ExtensionObject elements now encode as JSON null in Variant arrays and matrices. DiagnosticInfo now omits its default fields in both modes, including VERBOSE. It omits the index fields when they are -1, and InnerStatusCode when Good.

Configure both sides for the selected JSON mode. OpcUaJsonDecoder.setEncoding(OpcUaJsonEncoder.Encoding.VERBOSE) is required for VERBOSE input. Match the decoder mode to the encoder for structures with optional fields, unions, or enum fields. Mismatches can raise Bad_DecodingError for unexpected members or incompatible enum tokens. COMPACT writes numeric enum fields, and VERBOSE writes named enums as Name_Value strings. VERBOSE also accepts enum names with numeric suffixes. VERBOSE now writes null arrays explicitly.

Dynamic structure codecs now assign optional-field mask bits by declared optional-field position. Milo 1.1.x incorrectly packed selected fields when an earlier optional field was absent. Union headers now use SwitchField rather than SwitchValue, and JsonStructCodec optional headers use EncodingMask rather than their old SwitchField name. These names affect XML and JSON wire data. DynamicUnionType.UnionValue can hold null. Re-test persisted optional structures and unions, and exchanges with old Milo peers.

JSON structure fields may be reordered. Buffered structures, ExtensionObject envelopes, and Variant bodies reject duplicates. Unknown structure members also fail with Bad_DecodingError. Top-level DataValue and Variant members, and directly streamed built-ins such as LocalizedText, are not checked for duplicates. For those, the last occurrence wins.

The context's EncodingLimits bound JSON input. MaxMessageSize counts UTF-16 code units, including whitespace, and defaults to 2 MiB even when configured as zero or less. MaxRecursionDepth defaults to 128 and bounds buffered containers and nested ExtensionObjects. Violations report Bad_EncodingLimitsExceeded.

JSON QualifiedNames such as 2:Name now decode to namespace index 2 and name Name. Milo 1.1.x treated the whole string as a namespace 0 name. Numeric indexes above 65535 fail with Bad_DecodingError.

Custom codecs must use UaEncoder.encodeEncodingMask and encodeSwitchField, and the matching UaDecoder methods, instead of ordinary UInt32 fields. The decoder methods take declared field names for VERBOSE inference. Custom managers must implement DataTypeManager.getDataTypeId(NodeId) for JSON wrapping of Binary and XML ExtensionObjects. Unresolved identity raises Bad_EncodingError.

The client's LazyClientDataTypeManager inherits reverse lookup of registrations already present. It does not trigger discovery through getDataTypeId. Wrapping a custom Binary ExtensionObject in JSON can fail until its codec has been resolved. Resolve getCodec(encodingId) and check for null, or decode the structure before explicitly encoding JSON. See the resolution workflow.

Dynamic codecs decode subtype-enabled structure arrays and successfully decoded ExtensionObject arrays in Variant fields as UaStructuredType[]. Callers casting to a concrete array class must change. Null ExtensionObjects become Java null. If a Variant-field ExtensionObject decode fails, the dynamic codec retains the raw value or array instead. Subtype-enabled scalar, array, and Matrix fields can now encode generated Java UaStructuredType values as well as dynamic structures, using the registered codecs.

milo-encoding-xml no longer depends on JAXB. Applications that relied on that transitive dependency must declare their own JAXB dependencies. milo-dtd-core still brings jaxb-runtime.

For a Matrix inside a Binary Variant, empty values with zero or negative dimensions become empty one-dimensional arrays. Nonempty values fail with Bad_EncodingError unless they have at least two dimensions and every dimension is positive.

As in 1.1.x, Binary structure fields preserve nonnegative dimensions, but a zero dimension suppresses all elements. A nonempty Matrix with dimensions [4, 0] decodes as empty. Also as in 1.1.x, positive-dimension element-count validation in the Binary field encoder relies on a Java assertion, and a negative dimension can be written but fails field decoding with Bad_DecodingError. Enforce shape and count before encoding.

JSON preserves empty dimensions. XML normalizes an empty Variant Matrix to an empty array and an empty structure-field Matrix to a null Matrix, losing the original dimensions. See encoding limits and Matrix boundaries.

For compatible element types, server Method inputs and Variable writes now accept an empty one-dimensional array for a declared ValueRank of 2 or more. They restore an empty Matrix of that declared rank, with zero in each dimension. Milo 1.1.x rejected that shape.

Direct Binary Matrix field decoding now returns Matrix.ofNull() rather than Java null. Test with Matrix.isNull() or isNotNull(). JSON and XML decoders already returned the null Matrix object in 1.1.x. Binary Matrix field encoders now accept Java null, which previously threw. Matrix.ofNull() already wrote the null wire form. XML encoding of Matrix.ofNull() no longer throws. See Data types and Encodings for the Binary behavior.

Non-null Matrix field decoding and direct Matrix decoding with fewer than two dimensions now fail with Bad_DecodingError in Binary, JSON, and XML. A Variant with one dimension can still decode as a one-dimensional array.

XML Matrix encoding now keeps nullable element positions with the schema's nil or default representation. Null Boolean or numeric elements and incompatible element classes fail with Bad_EncodingError. XML array fields must be direct children of their field element. Invalid lexical values, integer overflow, and date-only DateTimes fail with Bad_DecodingError.

XML decoding now honors xsi:nil on String, ByteString, XmlElement, and array containers. An empty ByteString element means empty bytes, while xsi:nil means null. Null ByteString and XmlElement values now encode with xsi:nil. Do not infer an original null-versus-empty distinction from stored data whose old encoding lost it.

ULong arithmetic now allows valid subtraction across the signed-long boundary and handles negative Long.MIN_VALUE operands without recursive overflow. Its float and double conversions avoid double rounding for values above Long.MAX_VALUE. Use toBigInteger() when an exact unsigned magnitude is required. Floating-point conversion remains approximate.

ULong.valueOf(String) now throws NumberFormatException for every value above 2^64-1. Milo 1.1.x returned an unrelated number for some of them, such as 11553255926290448384 for 30000000000000000000. The JSON decoder reads UInt64 through this method, so it now fails such input with Bad_DecodingError, as the XML decoder already did. Exception messages for other invalid strings changed.

Diagnostics nodes

Per-Session diagnostics Objects now follow ServerDiagnostics EnabledFlag, which defaults to false. A default 1.2 server therefore has no per-Session diagnostics Objects. Milo 1.1.x always created them, but their values returned Bad_NotReadable while disabled. Enabling now creates Objects for existing Sessions and catches up each Session's SubscriptionDiagnosticsArray. Disabling removes the dynamic nodes. Array-element gating already existed in 1.1.x.

Static diagnostics Variables remain and report Bad_OutOfService while disabled, replacing Bad_NotReadable. SessionDiagnostics and SessionSecurityDiagnostics arrays now reference the same Variables beneath each Session Object. The old separately allocated namespace 1 array elements with BrowseName 1:SessionDiagnostics are replaced by the Object's namespace 0-named Variables.

Rediscover after toggling instead of retaining dynamic NodeIds or wrappers. Where an array element NodeId has a [n] suffix, n is an allocation counter, not its current array position. Deleting an earlier element no longer lets a new element reuse a survivor's ID.

The public SessionDiagnosticsVariableArray and SessionSecurityDiagnosticsVariableArray constructors no longer take a NodeManager. See Diagnostics and diagnostics ownership and lifecycle.

Sampling and read access

Data-item creation now succeeds for Bad_NotReadable and Bad_UserAccessDenied read denials, then publishes the denial as the DataValue status (Part 4 §5.13.2.1). In 1.1.x the default controller returned only Bad_UserAccessDenied for these access-bit checks, and Bad_NotReadable could come from a custom controller. Any other Denied status still fails data-item creation. Every Denied status fails event-item creation. NODE_UNKNOWN fails either kind of item with Bad_NodeIdUnknown. Clients must inspect Publish status, not only the create result.

Access restoration requires refreshing the stored decision. That happens every cycle under the default policy, after invalidation under cached policy, or through application refresh for custom samplers. The SDK rechecks identity or endpoint changes and transfers. A custom sampler otherwise retains its old decision even after application permissions change.

See the Access control example for cached denial and restoration, and the read-access tests for creation followed by both denial statuses.

For Value Reads, the default controller now checks AccessLevel before UserAccessLevel and returns Bad_NotReadable if CurrentRead is absent. In 1.1.x it checked only UserAccessLevel, so a Variable whose AccessLevel denied reading could still return a value when UserAccessLevel allowed it. Monitored items now publish that node-level denial. For Value Writes, AccessLevel lacking CurrentWrite now gives Bad_NotWritable before UserAccessLevel is checked. Milo 1.1.x checked the user bit first and left the node-level rejection to the node writer.

SubscriptionModel remains a working deprecated adapter. Its old forwarding overrides replace the new defaults, so retaining that wiring does not double-sample. To adopt the inherited manager:

  • Remove the SubscriptionModel field.
  • Remove its lifecycle registration.
  • Remove the four forwarding callbacks (or old no-op callbacks), preserving unrelated callback work.

ManagedNamespaceWithLifecycle, ManagedAddressSpaceWithLifecycle, and ManagedAddressSpaceFragmentWithLifecycle own manager startup and shutdown. Plain ManagedNamespace or ManagedAddressSpace subclasses must call getSamplingManager().startup() and shutdown(). Otherwise, created items remain unsampled.

The inherited manager revises intervals upward to 25 ms buckets by default, on creation and modification. A 0 ms request that reaches the namespace becomes 25 ms, where SubscriptionModel polled at least every 1 ms. This comparison requires MinimumSamplingInterval 0 (or another node that permits 0). Variables default to -1 in both releases. For them, the subscription publishing interval replaces a 0 ms request before namespace revision.

New-item sampling now uses a 100 ms debounce with a 500 ms maximum collection window. Milo 1.1.x scheduled an immediate initial sample.

A custom sampler can override protected reviseSamplingInterval(ReadValueId,double) to report its own supported intervals. Returning 0 requires routing the item to its own push implementation. Direct AddressSpaces retaining SubscriptionModel keep its unbucketed intervals. See Sampling migration and the interval tests.

AccessController, DefaultAccessController, and AccessResult moved from servicesets.impl to access, so update imports. getAccessController() and getAccessControlManager() are final. Replace a server getter override with OpcUaServerConfigBuilder.setAccessControllerFactory(...). The old override reached all 1.1.x checks. In 1.2 the manager and cache retain their own controller reference, so overriding a getter would not cover every check.

DefaultAccessController.checkWriteAccess now returns an IdentityHashMap. Code that calls it directly must look up each result with the same WriteValue instance it passed in. An equal but different instance finds nothing, and equal WriteValues each get their own entry. Custom AccessController implementations are unaffected.

Default sampling also calls the controller on sampling threads for each Session every cycle. Keep controllers and access-attribute filters fast, thread-safe, and free of recursive controller calls.

Cached-policy users must invalidate changes to AccessLevel, UserAccessLevel, RolePermissions, AccessRestrictions, role mapping, and node removal or replacement. SDK Session changes, reactivation, and transfers invalidate automatically, and Session closure evicts cached entries. Use invalidateReadAccess(ReadAccessScope) to limit invalidation to nodes, a namespace, a predicate, or a Session. Direct service checks do not use that cache.

checkReadAccess now returns NODE_UNKNOWN for an unrecognized node instead of ALLOWED. Neither isAllowed() nor isDenied() is true. Ordinary service dispatch lets the address space provide its unknown-node result. An exhaustive switch over 1.1.x's already-sealed AccessResult fails to compile until it handles the new NodeUnknown record.

DataItem.setReadAccessResult has a no-op default, and getReadAccessResult returns ALLOWED by default. A custom implementation enforces no read-access decision through these defaults, so override them to participate. The overload setReadAccessResult(result,session,accessEpoch) returns boolean and checks Session identity and Session.getAccessEpoch() before applying a result. An implementation with its own update lock must compare under that lock too.

Custom producers can use a DataItemListener (OpcUaServer.addDataItemListener and removeDataItemListener), AddressSpace.onDataItemsTransferred, and Session identity and endpoint listeners. Managed cache users also have ReadAccessListener. Refresh even while the device is quiet.

MonitoredDataItem.setValue now drops values while Disabled. Milo 1.1.x enqueued them. A new denial after a Session change clears values queued for the old Session through setReadAccessResultAfterSessionChange(...). Ordinary application invalidation does not purge already queued values.

The OpcUaServerConfigImpl canonical constructor adds both reverseConnectTargets and accessControllerFactory. Compatibility overloads remain.

Deprecations

Deprecated API Replacement
NodeFactory, its callback, InstanceDeclarationHierarchy, EventFactory, and their accessors Node instantiator; OpcUaServer.newEvent(typeId) for transient events
RsaSha256CertificateFactory AbstractCertificateFactory
ValidationCheck.REVOCATION (for removal) None needed; use REVOCATION_LISTS for strict checking
Stack.ConnectionLimits.HELLO_DEADLINE_MS OpcTcpServerTransportConfigBuilder.setHelloDeadline(...)
SubscriptionModel ManagedAddressSpace's inherited SamplingManager, or an explicitly owned SamplingManager with an AddressSpaceSamplingGroup factory for a direct AddressSpace; see Sampling and read access

Clone this wiki locally