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.api;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Optional;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentMap;
  import java.util.function.Predicate;
  import java.util.stream.Collectors;
  
  import com.google.common.collect.LinkedHashMultiset;
  import com.google.common.collect.MapMaker;
  import org.eclipse.milo.opcua.sdk.core.Reference;
  import org.eclipse.milo.opcua.sdk.core.nodes.Node;
  import org.eclipse.milo.opcua.stack.core.NamespaceTable;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
  import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
  
  public class AbstractNodeManager<T extends Node> implements NodeManager<T> {
  
      private final ConcurrentMap<NodeId, T> nodeMap;
      private final ConcurrentMap<NodeId, LinkedHashMultiset<Reference>> referenceMap;
  
      public AbstractNodeManager() {
          nodeMap = makeNodeMap(new MapMaker());
  
          referenceMap = new ConcurrentHashMap<>();
      }
  
      /**
       * Optionally customize the backing {@link ConcurrentMap} with the provided {@link MapMaker}.
       *
       * @param mapMaker the {@link MapMaker} that make the backing map with.
       * @return a {@link ConcurrentMap}.
       */
      protected ConcurrentMap<NodeId, T> makeNodeMap(MapMaker mapMaker) {
          return mapMaker.makeMap();
      }
  
      /**
       * Get the backing {@link ConcurrentMap} holding this {@link NodeManager}'s Nodes.
       *
       * @return the backing {@link ConcurrentMap} holding this {@link NodeManager}'s Nodes.
       */
      protected ConcurrentMap<NodeId, T> getNodeMap() {
          return nodeMap;
      }
  
      /**
       * Get the backing {@link ConcurrentMap} holding this {@link NodeManager}'s References.
       *
       * @return the backing {@link ConcurrentMap} holding this {@link NodeManager}'s References.
       */
      public ConcurrentMap<NodeId, LinkedHashMultiset<Reference>> getReferenceMap() {
          return referenceMap;
      }
  
      /**
       * Get a copied List of the Nodes being managed.
       *
       * @return a copied List of the Nodes being managed.
       */
      public List<T> getNodes() {
          return new ArrayList<>(nodeMap.values());
      }
  
      /**
       * Get a copied List of the {@link NodeId}s being managed.
       *
       * @return a copied List of the {@link NodeId}s being managed.
       */
      public List<NodeId> getNodeIds() {
          return new ArrayList<>(nodeMap.keySet());
      }
  
      @Override
      public boolean containsNode(NodeId nodeId) {
          return nodeMap.containsKey(nodeId);
      }
  
      @Override
      public boolean containsNode(ExpandedNodeId nodeId, NamespaceTable namespaceTable) {
          return nodeId.toNodeId(namespaceTable)
              .map(this::containsNode)
              .orElse(false);
      }
  
      @Override
      public Optional<T> addNode(T node) {
          return Optional.ofNullable(nodeMap.put(node.getNodeId(), node));
      }
  
      @Override
      public Optional<T> getNode(NodeId nodeId) {
          return Optional.ofNullable(nodeMap.get(nodeId));
      }
  
      @Override
      public Optional<T> getNode(ExpandedNodeId nodeId, NamespaceTable namespaceTable) {
          return nodeId.toNodeId(namespaceTable).flatMap(this::getNode);
      }
  
      @Override
      public Optional<T> removeNode(NodeId nodeId) {
          return Optional.ofNullable(nodeMap.remove(nodeId));
      }
  
      @Override
      public Optional<T> removeNode(ExpandedNodeId nodeId, NamespaceTable namespaceTable) {
          return nodeId.toNodeId(namespaceTable).flatMap(this::removeNode);
      }
  
      @Override
      public synchronized void addReference(Reference reference) {
          LinkedHashMultiset<Reference> references = referenceMap.computeIfAbsent(
              reference.getSourceNodeId(),
              nodeId -> LinkedHashMultiset.create()
          );
  
          references.add(reference);
      }
  
      @Override
      public synchronized void addReferences(Reference reference, NamespaceTable namespaceTable) {
          addReference(reference);
  
          reference.invert(namespaceTable).ifPresent(this::addReference);
      }
  
      @Override
      public synchronized void removeReference(Reference reference) {
          LinkedHashMultiset<Reference> references = referenceMap.get(
              reference.getSourceNodeId()
          );
  
          if (references != null) {
              references.remove(reference);
  
              if (references.isEmpty()) {
                  referenceMap.remove(reference.getSourceNodeId());
              }
          }
      }
  
      @Override
      public synchronized void removeReferences(Reference reference, NamespaceTable namespaceTable) {
          removeReference(reference);
  
          reference.invert(namespaceTable).ifPresent(this::removeReference);
      }
  
      @Override
      public synchronized List<Reference> getReferences(NodeId nodeId) {
          LinkedHashMultiset<Reference> references = referenceMap.get(nodeId);
  
          if (references != null) {
              return new ArrayList<>(references);
          } else {
              return Collections.emptyList();
          }
      }
  
      @Override
      public List<Reference> getReferences(NodeId nodeId, Predicate<Reference> filter) {
          return getReferences(nodeId)
              .stream()
              .filter(filter)
              .collect(Collectors.toList());
      }
  
  }
  