code: /*
   * Copyright (c) 2019 the Eclipse Milo Authors
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  
  package org.eclipse.milo.opcua.sdk.server.nodes.factories;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Objects;
  import java.util.stream.Collectors;
  
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
  import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
  
  class ReferenceTable {
  
      private final List<RefRow> references = new ArrayList<>();
  
      void addReference(
          BrowsePath sourcePath,
          NodeId referenceTypeId,
          ExpandedNodeId targetNodeId) {
  
          references.add(new RefRow(sourcePath, referenceTypeId, new RefTarget(targetNodeId)));
      }
  
      void addReference(
          BrowsePath sourcePath,
          NodeId referenceTypeId,
          BrowsePath targetPath) {
  
          references.add(new RefRow(sourcePath, referenceTypeId, new RefTarget(targetPath)));
      }
  
      List<RefRow> getReferences(BrowsePath sourcePath) {
          return references.stream().filter(t -> t.browsePath.equals(sourcePath)).collect(Collectors.toList());
      }
  
      static ReferenceTable merge(ReferenceTable table1, ReferenceTable table2) {
          ReferenceTable mergedTable = new ReferenceTable();
  
          mergedTable.references.addAll(table1.references);
  
          table2.references.forEach(row -> {
              BrowsePath browsePath = row.browsePath;
              NodeId referenceTypeId = row.nodeId;
              RefTarget target = row.target;
  
              if (Identifiers.HasProperty.equals(referenceTypeId)) {
  
                  boolean hasPropertyReference = mergedTable.references.stream().anyMatch(
                      r ->
                          r.browsePath.equals(browsePath) &&
                              r.nodeId.equals(Identifiers.HasProperty) &&
                              r.target.equals(target)
                  );
  
                  if (!hasPropertyReference) {
                      mergedTable.references.add(row);
                  }
              } else if (Identifiers.HasTypeDefinition.equals(referenceTypeId)) {
                  // Don't merge a HasTypeDefinition reference if there's already one present
                  // for a given BrowsePath.
                  // This logic may need to be extended to include other Reference types for
                  // which there should only be one.
  
                  boolean hasTypeDefinitionReference = mergedTable.references.stream().anyMatch(
                      r ->
                          r.browsePath.equals(browsePath) &&
                              r.nodeId.equals(Identifiers.HasTypeDefinition)
                  );
  
                  if (!hasTypeDefinitionReference) {
                      mergedTable.references.add(row);
                  }
              } else if (!mergedTable.references.contains(row)) {
                  mergedTable.references.add(row);
              }
          });
  
          return mergedTable;
      }
  
      static class RefRow {
          final BrowsePath browsePath;
          final NodeId nodeId;
          final RefTarget target;
  
          RefRow(BrowsePath browsePath, NodeId nodeId, RefTarget target) {
              this.browsePath = browsePath;
              this.nodeId = nodeId;
              this.target = target;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              RefRow row = (RefRow) o;
              return Objects.equals(browsePath, row.browsePath) &&
                  Objects.equals(nodeId, row.nodeId) &&
                  Objects.equals(target, row.target);
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(browsePath, nodeId, target);
          }
      }
  
      static class RefTarget {
          final ExpandedNodeId targetNodeId;
          final BrowsePath targetPath;
  
          RefTarget(ExpandedNodeId targetNodeId) {
              this.targetNodeId = targetNodeId;
              this.targetPath = null;
          }
  
          RefTarget(BrowsePath targetPath) {
              this.targetPath = targetPath;
              this.targetNodeId = null;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              RefTarget target = (RefTarget) o;
              return Objects.equals(targetNodeId, target.targetNodeId) &&
                  Objects.equals(targetPath, target.targetPath);
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(targetNodeId, targetPath);
          }
      }
  
  }
  