+ private final LoadingCache<Class<?>, DataContainerStreamer<?>> streamers = CacheBuilder.newBuilder()
+ .build(new CacheLoader<>() {
+ @Override
+ public DataContainerStreamer<?> load(final Class<?> key) throws ReflectiveOperationException {
+ final var streamer = DataContainerStreamerGenerator.generateStreamer(loader, BindingCodecContext.this,
+ key);
+ final var instance = streamer.getDeclaredField(DataContainerStreamerGenerator.INSTANCE_FIELD);
+ return (DataContainerStreamer<?>) instance.get(null);
+ }
+ });
+ private final LoadingCache<Class<?>, DataContainerSerializer> serializers = CacheBuilder.newBuilder()
+ .build(new CacheLoader<>() {
+ @Override
+ public DataContainerSerializer load(final Class<?> key) throws ExecutionException {
+ return new DataContainerSerializer(BindingCodecContext.this, streamers.get(key));
+ }
+ });
+ private final LoadingCache<Class<? extends DataObject>, DataContainerCodecContext<?, ?, ?>> childrenByClass =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public DataContainerCodecContext<?, ?, ?> load(final Class<? extends DataObject> key) {
+ final var childSchema = context.getTypes().bindingChild(JavaTypeName.create(key));
+ if (childSchema instanceof ContainerLikeRuntimeType containerLike) {
+ if (childSchema instanceof ContainerRuntimeType container
+ && container.statement().findFirstEffectiveSubstatement(PresenceEffectiveStatement.class)
+ .isEmpty()) {
+ return new StructuralContainerCodecContext<>(key, container, BindingCodecContext.this);
+ }
+ return new ContainerLikeCodecContext<>(key, containerLike, BindingCodecContext.this);
+ } else if (childSchema instanceof ListRuntimeType list) {
+ return list.keyType() == null ? new ListCodecContext<>(key, list, BindingCodecContext.this)
+ : MapCodecContext.of(key, list, BindingCodecContext.this);
+ } else if (childSchema instanceof ChoiceRuntimeType choice) {
+ return new ChoiceCodecContext<>(key.asSubclass(ChoiceIn.class), choice, BindingCodecContext.this);
+ } else if (childSchema == null) {
+ throw DataContainerCodecContext.childNullException(context, key, "%s is not top-level item.", key);
+ } else {
+ throw new IncorrectNestingException("%s is not a valid data tree child of %s", key, this);
+ }
+ }
+ });
+
+ // FIXME: this could also be a leaf!
+ private final LoadingCache<QName, DataContainerCodecContext<?, ?, ?>> childrenByDomArg =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public DataContainerCodecContext<?, ?, ?> load(final QName qname) throws ClassNotFoundException {
+ final var type = context.getTypes();
+ final var child = type.schemaTreeChild(qname);
+ if (child == null) {
+ final var module = qname.getModule();
+ if (context.modelContext().findModule(module).isEmpty()) {
+ throw new MissingSchemaException(
+ "Module " + module + " is not present in current schema context.");
+ }
+ throw new IncorrectNestingException("Argument %s is not valid child of %s", qname, type);
+ }
+
+ if (!(child instanceof DataRuntimeType)) {
+ throw new IncorrectNestingException("Argument %s is not valid data tree child of %s", qname, type);
+ }
+
+ // TODO: improve this check?
+ final var childSchema = child.statement();
+ if (childSchema instanceof DataNodeContainer || childSchema instanceof ChoiceSchemaNode) {
+ return getStreamChild(context.loadClass(child.javaType()));
+ }
+
+ throw new UnsupportedOperationException("Unsupported child type " + childSchema.getClass());
+ }
+ });
+
+ private final LoadingCache<Class<? extends DataObject>, ChoiceCodecContext<?>> choicesByClass =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public ChoiceCodecContext<?> load(final Class<? extends DataObject> caseType) {
+ final var choiceClass = findCaseChoice(caseType);
+ if (choiceClass == null) {
+ throw new IllegalArgumentException(caseType + " is not a valid case representation");
+ }
+ if (context.getSchemaDefinition(choiceClass) instanceof ChoiceRuntimeType choiceType) {
+ // FIXME: accurate type!
+ return new ChoiceCodecContext(choiceClass, choiceType, BindingCodecContext.this);
+ }
+ throw new IllegalArgumentException(caseType + " does not refer to a choice");
+ }
+
+ private static Class<?> findCaseChoice(final Class<? extends DataObject> caseClass) {
+ for (var type : caseClass.getGenericInterfaces()) {
+ if (type instanceof Class<?> typeClass && ChoiceIn.class.isAssignableFrom(typeClass)) {
+ return typeClass.asSubclass(ChoiceIn.class);
+ }
+ }
+ return null;
+ }
+ });
+
+ private final LoadingCache<Class<? extends Action<?, ?, ?>>, ActionCodecContext> actionsByClass =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public ActionCodecContext load(final Class<? extends Action<?, ?, ?>> action) {
+ if (KeyedListAction.class.isAssignableFrom(action)) {
+ return prepareActionContext(2, 3, 4, action, KeyedListAction.class);
+ } else if (Action.class.isAssignableFrom(action)) {
+ return prepareActionContext(1, 2, 3, action, Action.class);
+ }
+ throw new IllegalArgumentException("The specific action type does not exist for action "
+ + action.getName());
+ }
+
+ private ActionCodecContext prepareActionContext(final int inputOffset, final int outputOffset,
+ final int expectedArgsLength, final Class<? extends Action<?, ?, ?>> action,
+ final Class<?> actionType) {
+ final var args = ClassLoaderUtils.findParameterizedType(action, actionType)
+ .orElseThrow(() -> new IllegalStateException(action + " does not specialize " + actionType))
+ .getActualTypeArguments();
+ checkArgument(args.length == expectedArgsLength, "Unexpected (%s) Action generatic arguments",
+ args.length);
+ final ActionRuntimeType schema = context.getActionDefinition(action);
+ return new ActionCodecContext(
+ new ContainerLikeCodecContext(asClass(args[inputOffset], RpcInput.class), schema.input(),
+ BindingCodecContext.this),
+ new ContainerLikeCodecContext(asClass(args[outputOffset], RpcOutput.class), schema.output(),
+ BindingCodecContext.this));
+ }
+
+ private static <T extends DataObject> Class<? extends T> asClass(final Type type, final Class<T> target) {
+ verify(type instanceof Class, "Type %s is not a class", type);
+ return ((Class<?>) type).asSubclass(target);
+ }
+ });
+
+ private final LoadingCache<Class<?>, NotificationCodecContext<?>> notificationsByClass = CacheBuilder.newBuilder()
+ .build(new CacheLoader<>() {
+ @Override
+ public NotificationCodecContext<?> load(final Class<?> key) {
+ final var runtimeType = context.getTypes().bindingChild(JavaTypeName.create(key));
+ if (runtimeType instanceof NotificationRuntimeType notification) {
+ return new NotificationCodecContext<>(key, notification, BindingCodecContext.this);
+ } if (runtimeType != null) {
+ throw new IllegalArgumentException(key + " maps to unexpected " + runtimeType);
+ }
+ throw new IllegalArgumentException(key + " is not a known class");
+ }
+ });
+ private final LoadingCache<Absolute, NotificationCodecContext<?>> notificationsByPath =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public NotificationCodecContext<?> load(final Absolute key) {
+ final var cls = context.getClassForSchema(key);
+ try {
+ return getNotificationContext(cls.asSubclass(Notification.class));
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException("Path " + key + " does not represent a notification", e);
+ }
+ }
+ });
+
+ private final LoadingCache<Class<?>, ContainerLikeCodecContext<?>> rpcDataByClass =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public ContainerLikeCodecContext<?> load(final Class<?> key) {
+ final var runtimeType = context.getTypes().findSchema(JavaTypeName.create(key))
+ .orElseThrow(() -> new IllegalArgumentException(key + " is not a known class"));
+ if (RpcInput.class.isAssignableFrom(key) && runtimeType instanceof InputRuntimeType input) {
+ // FIXME: accurate type
+ return new ContainerLikeCodecContext(key, input, BindingCodecContext.this);
+ } else if (RpcOutput.class.isAssignableFrom(key) && runtimeType instanceof OutputRuntimeType output) {
+ // FIXME: accurate type
+ return new ContainerLikeCodecContext(key, output, BindingCodecContext.this);
+ } else {
+ throw new IllegalArgumentException(key + " maps to unexpected " + runtimeType);
+ }
+ }
+ });
+ private final LoadingCache<Absolute, RpcInputCodec<?>> rpcDataByPath =
+ CacheBuilder.newBuilder().build(new CacheLoader<>() {
+ @Override
+ public RpcInputCodec<?> load(final Absolute key) {
+ final var rpcName = key.firstNodeIdentifier();
+
+ final Class<? extends DataContainer> container = switch (key.lastNodeIdentifier().getLocalName()) {
+ case "input" -> context.getRpcInput(rpcName);
+ case "output" -> context.getRpcOutput(rpcName);
+ default -> throw new IllegalArgumentException("Unhandled path " + key);
+ };
+
+ return getRpc(container);
+ }
+ });
+
+ private final @NonNull BindingClassLoader loader =
+ BindingClassLoader.create(BindingCodecContext.class, BYTECODE_DIRECTORY);
+ private final @NonNull InstanceIdentifierCodec instanceIdentifierCodec;
+ private final @NonNull IdentityCodec identityCodec;
+ private final @NonNull BindingRuntimeContext context;
+
+ public BindingCodecContext() {
+ this(ServiceLoader.load(BindingRuntimeContext.class).findFirst()
+ .orElseThrow(() -> new IllegalStateException("Failed to load BindingRuntimeContext")));
+ }
+
+ public BindingCodecContext(final BindingRuntimeContext context) {
+ this.context = requireNonNull(context, "Binding Runtime Context is required.");
+ identityCodec = new IdentityCodec(context);
+ instanceIdentifierCodec = new InstanceIdentifierCodec(this);