File

src/app/shared/widgets/widget.registry.ts

Indexable

[key: string]: ChannelBridge[]
import {
  of as observableOf,
  Subject,
  ReplaySubject,
  Observable,
  Subscription,
} from "rxjs";

import { filter } from "rxjs/operators";
import { Injectable } from "@angular/core";
import { FormGroup } from "@angular/forms";
import {
  Channels,
  WidgetController,
  WidgetConfig,
  InputParameterDefinition,
  WidgetMetadata,
  ChannelDescriptors,
} from "./index";
import { NmWidgetMetadataRegistry } from "./widget.metadata.registry";
import { SimpleChannelMapping } from "./widget.configuration";
import { HalService } from "../components/hal/hal.service";
import { CurrentLocaleService } from "../components/i18n/currentLocale.service";
import { ActionForm } from "../components/hal/actionForm";

const NG_ON_INIT = "ngOnInit";

class ChannelBridge {
  private _subject: ReplaySubject<any>;
  private _sources: Observable<any>[] = [];
  private _destinations: Subject<any>[] = [];
  public subscriptions: Subscription[] = [];

  public constructor(
    public sourcePath: string,
    public destinationPath: string
  ) {
    this._subject = new ReplaySubject<any>(1);
  }

  public addSource(source: Observable<any>, path: string) {
    this._sources.push(source);

    let subscription = source.subscribe(this._subject);
    this.subscriptions.push(subscription);
  }

  public addDestination(destination: Subject<any>, path: string) {
    this._destinations.push(destination);

    let subscription = this._subject.subscribe(destination);
    this.subscriptions.push(subscription);
  }

  public dispose(): void {
    if (this.subscriptions) {
      this.subscriptions.forEach((subscription) => subscription.unsubscribe());
    }
  }
}

interface BridgeMap {
  [key: string]: ChannelBridge[];
}

export function widgetRegistryFactory(
  halService: HalService,
  currentLocaleService: CurrentLocaleService
): WidgetRegistry {
  return new WidgetRegistry(halService, currentLocaleService);
}

class DefaultWidgetController implements WidgetController {
  onInit(channels: Channels): void {}
}

@Injectable()
export class WidgetRegistry {
  private _stack: string[] = [];
  private _inputParameterDefinition: InputParameterDefinition = {};
  private _controller: WidgetController;
  private _form: FormGroup;

  private _channels: BridgeMap = {};
  private _action: ActionForm;

  // WARNING:
  // WidgetRegistry is re-created for every instance of NmPageComponent
  // using the factory injection pattern.
  // All new dependencies must be registered in the factory in page.component.ts
  // before they can be used here
  constructor(
    private _halService: HalService,
    private _currentLocaleService: CurrentLocaleService
  ) {
    this._controller = new DefaultWidgetController();
  }

  public dispose() {
    for (let channelId in this._channels) {
      this._channels[channelId].forEach((bridge) => bridge.dispose());
    }

    if (this._controller && this._controller.onDestroy) {
      this._controller.onDestroy();
    }
  }

  public parseConfiguration(configuration: WidgetConfig) {
    this.parseConfigurationStart(configuration, this.prepareChannels);
    this.parseConfigurationStart(configuration, this.connectLinks);
    this.connectInputParameters();
    this.connectActionEvents();
    this.connectLocaleEvents();
    this.connectEcho();
    this.connectController();

    console.log(this._channels);
  }

  private parseConfigurationStart(
    configuration: WidgetConfig,
    callback: (
      path: string,
      configuration: WidgetConfig,
      descriptor: WidgetMetadata
    ) => void
  ) {
    this.parseConfigurationRecursive("", "root", configuration, callback);
  }

  private parseConfigurationRecursive(
    path: string,
    name: string,
    configuration: WidgetConfig,
    callback: (
      path: string,
      configuration: WidgetConfig,
      descriptor: WidgetMetadata
    ) => void
  ) {
    let subPath = path + "/" + name;
    let descriptor = NmWidgetMetadataRegistry.getWidgetDescriptor(
      configuration.component
    );

    callback.apply(this, [subPath, configuration, descriptor]);

    if (configuration._embedded) {
      for (let child in configuration._embedded) {
        this.parseConfigurationRecursive(
          subPath,
          child,
          configuration._embedded[child],
          callback
        );
      }
    }
  }

  private connectInputParameters(): void {
    for (let parameter in this._inputParameterDefinition) {
      let key = "@page:" + parameter;
      let observable = this._inputParameterDefinition[parameter];

      if (this._channels[key]) {
        this._channels[key].forEach((bridge) =>
          bridge.addSource(observable, key)
        );
      }
    }
  }

  private connectActionEvents(): void {
    for (let channel in this._channels) {
      if (channel.indexOf("@action") < 0) {
        continue;
      }

      let action = channel.replace("@action:", "");

      let observable = this._halService
        .getActionEvents()
        .pipe(filter((event) => event.name === action));

      this._channels[channel].forEach((bridge) =>
        bridge.addSource(observable, channel)
      );
    }
  }

  private connectLocaleEvents(): void {
    for (let channel in this._channels) {
      if (channel.indexOf("@locale") < 0) {
        continue;
      }

      let observable = this._currentLocaleService.getCurrentLocale();
      this._channels[channel].forEach((bridge) =>
        bridge.addSource(observable, channel)
      );
    }
  }

  private connectEcho(): void {
    let echos = this._channels["@echo"];

    let subject = new Subject();

    if (echos) {
      echos.forEach((bridge) => {
        let subscription = subject.subscribe((data) =>
          console.log("@echo", bridge.sourcePath, data)
        );
        bridge.subscriptions.push(subscription);

        bridge.addDestination(subject, "@echo");
      });
    }
  }

  private connectController(): void {
    let channels: Channels = {};

    for (let channel in this._channels) {
      if (channel.indexOf("@controller") < 0) {
        continue;
      }

      let name = channel.replace("@controller:", "");

      this._channels[channel].forEach((bridge) => {
        let subject = new Subject();
        channels[name] = subject;

        if (bridge.sourcePath === channel) {
          bridge.addSource(subject, channel);
        } else if (bridge.destinationPath === channel) {
          bridge.addDestination(subject, channel);
        }
      });
    }

    if (this._controller) {
      this._controller.onInit(channels);
    }
  }

  private prepareChannels(
    path: string,
    configuration: WidgetConfig,
    descriptor: WidgetMetadata
  ) {
    if (configuration.channels) {
      configuration.channels.forEach((mapping) => {
        this.configureChannelMapping(path, mapping);
      });
    }
  }

  private connectLinks(
    path: string,
    configuration: WidgetConfig,
    descriptor: WidgetMetadata
  ) {
    if (!configuration._links) {
      return;
    }

    for (let name in configuration._links) {
      let key = path + ":@link:" + name;
      let link = configuration._links[name];

      let observable = observableOf(link["href"]);

      if (this._channels[key]) {
        if (this._channels[key]) {
          this._channels[key].forEach((bridge) =>
            bridge.addSource(observable, key)
          );
        }
      }
    }
  }

  private configureChannelMapping(path: string, mapping: SimpleChannelMapping) {
    // check for channels and create bridges where possible
    let sourcePath = this.resolveChannelPath(path, mapping.source);
    let destinationPath = this.resolveChannelPath(path, mapping.destination);

    let bridge = new ChannelBridge(sourcePath, destinationPath);

    this.addChannel(this._channels, sourcePath, bridge);
    this.addChannel(this._channels, destinationPath, bridge);
  }

  private addChannel(channels: BridgeMap, path: string, bridge: ChannelBridge) {
    let array = channels[path] || [];
    array.push(bridge);

    channels[path] = array;
  }

  private resolveChannelPath(currentPath: string, channel: string): string {
    // reference to something else
    if (channel.match(/^(@page|@action|@echo|@locale|@controller)/)) {
      return channel;
    }

    let parts = channel.split(":");

    if (parts.length == 2) {
      parts = [currentPath].concat(parts);
    }

    var _path = parts[0];
    var _prefix = parts[1];
    var _name = parts[2];

    let char = _path.charAt(0);

    if (char !== "/" && char !== "*") {
      _path = currentPath + "/" + _path;
    }

    return [_path, _prefix, _name].join(":");
  }

  public configureWidget(
    parent: string,
    id: string,
    configuration: WidgetConfig,
    widget: any,
    descriptor: WidgetMetadata
  ) {
    while (
      this._stack.length > 0 &&
      this._stack[this._stack.length - 1] !== parent
    ) {
      this._stack.pop();
    }

    this._stack.push(id);

    let path = "/" + this._stack.join("/");

    console.log(
      "widget path",
      configuration.id,
      configuration.component,
      parent,
      path
    );

    if (!descriptor.propertyConfiguration && !descriptor.methodConfigure) {
      throw new Error(
        'either a configuration property or a configure method is required for widget "' +
          id +
          '"'
      );
    }

    if (descriptor.propertyConfiguration) {
      widget[descriptor.propertyConfiguration] = configuration;
    }

    if (descriptor.propertyId) {
      widget[descriptor.propertyId] = id;
    }

    if (descriptor.methodConfigure) {
      widget[descriptor.methodConfigure](
        configuration,
        this._form,
        this._action
      );
    }

    let inputChannels = descriptor.inputs;
    let outputChannels = descriptor.outputs;

    WidgetRegistry.prepareChannels(widget, inputChannels);
    WidgetRegistry.prepareChannels(widget, outputChannels);

    let _service = this;
    let onInit = widget[NG_ON_INIT];
    let wrappedOnInit = function () {
      try {
        if (onInit) {
          onInit.call(this);
        }
        _service.connectInputChannels(widget, path, inputChannels);
        _service.connectOutputChannels(widget, path, outputChannels);
      } catch (err) {
        throw new Error(
          "exception during onInit or connectInput/-Output " +
            JSON.stringify(err)
        );
      }
    };

    // if the widget contains a ngOnInit-method, we initialize as part of the normal
    // angular lifecycle
    if (onInit) {
      widget[NG_ON_INIT] = wrappedOnInit;
    }
    // otherwise, we just initalize after the constructor and possibly the configure method have finished
    else {
      wrappedOnInit.call(widget);
    }
  }

  private connectOutputChannels(
    widget: any,
    path: string,
    channels: ChannelDescriptors
  ) {
    if (!channels) {
      return;
    }

    for (let channelId in channels) {
      let channelDescriptor = channels[channelId];
      let subject = widget[channelDescriptor.property];

      let channelPath = path + ":@output:" + channelId;

      if (this._channels[channelPath]) {
        this._channels[channelPath].forEach((bridge) =>
          bridge.addSource(subject, channelPath)
        );
      }

      let defaultPath = "*:@output:" + channelId;
      if (this._channels[defaultPath]) {
        this._channels[defaultPath].forEach((bridge) =>
          bridge.addSource(subject, channelPath)
        );
      }
    }
  }

  private connectInputChannels(
    widget: any,
    path: string,
    channels: ChannelDescriptors
  ) {
    if (!channels) {
      return;
    }

    for (let channelId in channels) {
      let channelDescriptor = channels[channelId];
      let subject = widget[channelDescriptor.property];

      let channelPath = path + ":@input:" + channelId;

      if (this._channels[channelPath]) {
        this._channels[channelPath].forEach((bridge) =>
          bridge.addDestination(subject, channelPath)
        );
      }

      let defaultPath = "*:@input:" + channelId;
      if (this._channels[defaultPath]) {
        this._channels[defaultPath].forEach((bridge) =>
          bridge.addDestination(subject, channelPath)
        );
      }
    }
  }

  private static prepareChannels(widget: any, channels: ChannelDescriptors) {
    if (!channels) {
      return;
    }

    for (let channelId in channels) {
      let channelDescriptor = channels[channelId];
      let subject = widget[channelDescriptor.property] || new Subject<any>();

      if (!widget[channelDescriptor.property]) {
        widget[channelDescriptor.property] = subject;
      }
    }
  }

  public registerInputParameters(inputParameters: InputParameterDefinition) {
    this._inputParameterDefinition = Object.assign(
      this._inputParameterDefinition,
      inputParameters
    );
  }

  public registerController(controller: WidgetController) {
    this._controller = controller;
  }

  public registerFormGroup(formGroup: FormGroup, action: ActionForm) {
    this._form = formGroup;
    this._action = action;
  }
}

results matching ""

    No results matching ""