private static ClassHierarchyProto.Node serializeNode(Node n) {
    List<ClassHierarchyProto.Node> children = new ArrayList<>();
    for (Node child : n.getChildren()) {
      children.add(serializeNode(child));
    }
    if (n instanceof ClassNode) {
      ClassNode<?> cn = (ClassNode<?>) n;
      ConstructorDef<?>[] injectable = cn.getInjectableConstructors();
      ConstructorDef<?>[] all = cn.getAllConstructors();
      List<ConstructorDef<?>> others = new ArrayList<>(Arrays.asList(all));
      others.removeAll(Arrays.asList(injectable));

      List<ClassHierarchyProto.ConstructorDef> injectableConstructors = new ArrayList<>();
      for (ConstructorDef<?> inj : injectable) {
        injectableConstructors.add(serializeConstructorDef(inj));
      }
      List<ClassHierarchyProto.ConstructorDef> otherConstructors = new ArrayList<>();
      for (ConstructorDef<?> other : others) {
        otherConstructors.add(serializeConstructorDef(other));
      }
      List<String> implFullNames = new ArrayList<>();
      for (ClassNode<?> impl : cn.getKnownImplementations()) {
        implFullNames.add(impl.getFullName());
      }
      return newClassNode(
          cn.getName(),
          cn.getFullName(),
          cn.isInjectionCandidate(),
          cn.isExternalConstructor(),
          cn.isUnit(),
          injectableConstructors,
          otherConstructors,
          implFullNames,
          children);
    } else if (n instanceof NamedParameterNode) {
      NamedParameterNode<?> np = (NamedParameterNode<?>) n;
      return newNamedParameterNode(
          np.getName(),
          np.getFullName(),
          np.getSimpleArgName(),
          np.getFullArgName(),
          np.isSet(),
          np.isList(),
          np.getDocumentation(),
          np.getShortName(),
          np.getDefaultInstanceAsStrings(),
          children);
    } else if (n instanceof PackageNode) {
      return newPackageNode(n.getName(), n.getFullName(), children);
    } else {
      throw new IllegalStateException("Encountered unknown type of Node: " + n);
    }
  }
  @Override
  public ClassHierarchy merge(ClassHierarchy ch) {
    if (this == ch) {
      return this;
    }
    if (!(ch instanceof ProtocolBufferClassHierarchy)) {
      throw new UnsupportedOperationException(
          "Cannot merge with class hierarchies of type: " + ch.getClass().getName());
    }

    final ProtocolBufferClassHierarchy pch = (ProtocolBufferClassHierarchy) ch;
    for (final String key : pch.lookupTable.keySet()) {
      if (!this.lookupTable.containsKey(key)) {
        this.lookupTable.put(key, pch.lookupTable.get(key));
      }

      for (final Node n : ch.getNamespace().getChildren()) {
        if (!this.namespace.contains(n.getFullName())) {
          if (n instanceof NamedParameter) {
            final NamedParameterNode np = (NamedParameterNode) n;
            new NamedParameterNodeImpl<>(
                this.namespace,
                np.getName(),
                np.getFullName(),
                np.getFullArgName(),
                np.getSimpleArgName(),
                np.isSet(),
                np.isList(),
                np.getDocumentation(),
                np.getShortName(),
                np.getDefaultInstanceAsStrings());
          } else if (n instanceof ClassNode) {
            final ClassNode cn = (ClassNode) n;
            new ClassNodeImpl(
                namespace,
                cn.getName(),
                cn.getFullName(),
                cn.isUnit(),
                cn.isInjectionCandidate(),
                cn.isExternalConstructor(),
                cn.getInjectableConstructors(),
                cn.getAllConstructors(),
                cn.getDefaultImplementation());
          }
        }
      }
    }
    return this;
  }
  private Node register(final String s) {
    final Class<?> c;
    try {
      c = classForName(s);
    } catch (final ClassNotFoundException e1) {
      return null;
    }
    try {
      final Node n = getAlreadyBoundNode(c);
      return n;
    } catch (final NameResolutionException e) {
      // node not bound yet
    }
    // First, walk up the class hierarchy, registering all out parents. This
    // can't be loopy.
    if (c.getSuperclass() != null) {
      register(ReflectionUtilities.getFullName(c.getSuperclass()));
    }
    for (final Class<?> i : c.getInterfaces()) {
      register(ReflectionUtilities.getFullName(i));
    }
    // Now, we'd like to register our enclosing classes. This turns out to be
    // safe.
    // Thankfully, Java doesn't allow:
    // class A implements A.B { class B { } }

    // It also doesn't allow cycles such as:
    // class A implements B.BB { interface AA { } }
    // class B implements A.AA { interface BB { } }

    // So, even though grafting arbitrary DAGs together can give us cycles, Java
    // seems
    // to have our back on this one.
    final Class<?> enclosing = c.getEnclosingClass();
    if (enclosing != null) {
      register(ReflectionUtilities.getFullName(enclosing));
    }

    // Now register the class. This has to be after the above so we know our
    // parents (superclasses and enclosing packages) are already registered.
    final Node n = registerClass(c);

    // Finally, do things that might introduce cycles that invlove c.
    // This has to be below registerClass, which ensures that any cycles
    // this stuff introduces are broken.
    for (final Class<?> innerClass : c.getDeclaredClasses()) {
      register(ReflectionUtilities.getFullName(innerClass));
    }
    if (n instanceof ClassNode) {
      final ClassNode<?> cls = (ClassNode<?>) n;
      for (final ConstructorDef<?> def : cls.getInjectableConstructors()) {
        for (final ConstructorArg arg : def.getArgs()) {
          register(arg.getType());
          if (arg.getNamedParameterName() != null) {
            final NamedParameterNode<?> np =
                (NamedParameterNode<?>) register(arg.getNamedParameterName());
            try {
              // TODO: When handling sets, need to track target of generic parameter, and check the
              // type here!
              if (!np.isSet()
                  && !np.isList()
                  && !ReflectionUtilities.isCoercable(
                      classForName(arg.getType()), classForName(np.getFullArgName()))) {
                throw new ClassHierarchyException(
                    "Named parameter type mismatch in "
                        + cls.getFullName()
                        + ".  Constructor expects a "
                        + arg.getType()
                        + " but "
                        + np.getName()
                        + " is a "
                        + np.getFullArgName());
              }
            } catch (final ClassNotFoundException e) {
              throw new ClassHierarchyException(
                  "Constructor refers to unknown class " + arg.getType(), e);
            }
          }
        }
      }
    } else if (n instanceof NamedParameterNode) {
      final NamedParameterNode<?> np = (NamedParameterNode<?>) n;
      register(np.getFullArgName());
    }
    return n;
  }