ReadInvocationHandler(
        final Node node,
        final Method method,
        final String annotationValue,
        final XBProjector projector,
        final boolean absentIsEmpty) {
      super(node, method, annotationValue, projector);
      wrappedInOptional = ReflectionHelper.isOptional(method.getGenericReturnType());
      returnType =
          wrappedInOptional
              ? ReflectionHelper.getParameterType(method.getGenericReturnType())
              : method.getReturnType();
      Class<?>[] exceptionTypes = method.getExceptionTypes();
      exceptionType = exceptionTypes.length > 0 ? exceptionTypes[0] : null;
      this.isConvertable = projector.config().getTypeConverter().isConvertable(returnType);
      this.isReturnAsNode = Node.class.isAssignableFrom(returnType);
      this.isEvaluateAsList =
          List.class.equals(returnType) || ReflectionHelper.isStreamClass(returnType);
      this.isReturnAsStream = ReflectionHelper.isStreamClass(returnType);
      this.isEvaluateAsArray = returnType.isArray();
      if (wrappedInOptional && (isEvaluateAsArray || isEvaluateAsList)) {
        throw new IllegalArgumentException(
            "Method "
                + method
                + " must not declare an optional return type of list or array. Lists and arrays may be empty but will never be null.");
      }
      this.isEvaluateAsSubProjection = returnType.isInterface();
      this.isThrowIfAbsent = exceptionType != null;

      // Throwing exception overrides empty default value.
      this.absentIsEmpty = absentIsEmpty && (!isThrowIfAbsent);
    }
 ProjectionMethodInvocationHandler(
     final Node node,
     final Method method,
     final String annotationValue,
     final XBProjector projector) {
   this.method = method;
   this.annotationValue = annotationValue;
   this.projector = projector;
   this.node = node;
   final XBDocURL annotation = method.getAnnotation(XBDocURL.class);
   this.docAnnotationValue = annotation == null ? null : annotation.value();
   this.isVoidMethod = !ReflectionHelper.hasReturnType(method);
   methodParameterIndexes = ReflectionHelper.getMethodParameterIndexes(method);
 }
 /**
  * When reading collections, determine the collection component type.
  *
  * @param method
  * @return
  */
 private static Class<?> findTargetComponentType(final Method method) {
   if (method.getReturnType().isArray()) {
     return method.getReturnType().getComponentType();
   }
   if (!(ReflectionHelper.isStreamClass(method.getReturnType())
       || List.class.equals(method.getReturnType()))) {
     return null;
   }
   final Type type = method.getGenericReturnType();
   if (!(type instanceof ParameterizedType)
       || (((ParameterizedType) type).getActualTypeArguments() == null)
       || (((ParameterizedType) type).getActualTypeArguments().length < 1)) {
     throw new IllegalArgumentException(
         "When using List as return type for method "
             + method
             + ", please specify a generic type for the List. Otherwise I do not know which type I should fill the List with.");
   }
   assert ((ParameterizedType) type).getActualTypeArguments().length == 1 : "";
   Type componentType = ((ParameterizedType) type).getActualTypeArguments()[0];
   if (!(componentType instanceof Class)) {
     throw new IllegalArgumentException(
         "I don't know how to instantiate the generic type for the return type of method "
             + method);
   }
   return (Class<?>) componentType;
 }
 /**
  * If parameter is instance of Callable or Supplier then resolve its value.
  *
  * @param args
  * @param args2
  */
 private static void unwrapArgs(final Class<?>[] types, final Object[] args) {
   if (args == null) {
     return;
   }
   try {
     for (int i = 0; i < args.length; ++i) {
       args[i] = ReflectionHelper.unwrap(types[i], args[i]);
     }
   } catch (Exception e) {
     throw new IllegalArgumentException(e);
   }
 }
 @Override
 public Object invokeXpathProjection(
     final InvocationContext invocationContext, final Object proxy, final Object[] args)
     throws Throwable {
   final Object result = invokeReadProjection(invocationContext, proxy, args);
   if ((result == null) && (isThrowIfAbsent)) {
     XBDataNotFoundException dataNotFoundException =
         new XBDataNotFoundException(invocationContext.getResolvedXPath());
     if (XBDataNotFoundException.class.equals(exceptionType)) {
       throw dataNotFoundException;
     }
     ReflectionHelper.throwThrowable(exceptionType, args, dataNotFoundException);
   }
   return result;
 }
    @Override
    public Object invokeXpathProjection(
        final InvocationContext invocationContext, final Object proxy, final Object[] args)
        throws Throwable {
      assert ReflectionHelper.hasParameters(method);
      final Node node = getNodeForMethod(method, args);
      //            final Document document = DOMHelper.getOwnerDocumentFor(node);
      //            final XPath xPath = projector.config().createXPath(document);
      final XPathExpression expression = invocationContext.getxPathExpression();

      final Object valueToSet = args[findIndexOfValue];
      //      final Class<?> typeToSet = method.getParameterTypes()[findIndexOfValue];
      //     final boolean isMultiValue = isMultiValue(typeToSet);
      NodeList nodes = (NodeList) expression.evaluate(node, XPathConstants.NODESET);
      final int count = nodes.getLength();
      for (int i = 0; i < count; ++i) {
        final Node n = nodes.item(i);
        if (n == null) {
          continue;
        }
        if (Node.ATTRIBUTE_NODE == n.getNodeType()) {
          Element e = ((Attr) n).getOwnerElement();
          if (e == null) {
            continue;
          }
          DOMHelper.setOrRemoveAttribute(
              e, n.getNodeName(), valueToSet == null ? null : valueToSet.toString());
          continue;
        }
        if (valueToSet instanceof Element) {
          if (!(n instanceof Element)) {
            throw new IllegalArgumentException(
                "XPath for element update need to select elements only");
          }
          DOMHelper.replaceElement((Element) n, (Element) ((Element) valueToSet).cloneNode(true));
          continue;
        }
        n.setTextContent(valueToSet == null ? null : valueToSet.toString());
      }
      return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
    }
 protected Node getNodeForMethod(final Method method, final Object[] args)
     throws SAXException, IOException, ParserConfigurationException {
   if (docAnnotationValue != null) {
     String uri =
         projector.config().getExternalizer().resolveURL(docAnnotationValue, method, args);
     final Map<String, String> requestParams =
         ((IOBuilder) projector.io()).filterRequestParamsFromParams(uri, args);
     uri = Preprocessor.applyParams(uri, methodParameterIndexes, args);
     Class<?> callerClass = null;
     if (IOHelper.isResourceProtocol(uri)) {
       callerClass = ReflectionHelper.getCallerClass(8);
     }
     return IOHelper.getDocumentFromURL(
         projector.config().createDocumentBuilder(),
         uri,
         requestParams,
         method.getDeclaringClass(),
         callerClass);
   }
   return node;
 }
  ProjectionInvocationHandler(
      final XBProjector projector,
      final Node node,
      final Class<?> projectionInterface,
      final Map<Class<?>, Object> mixins,
      final boolean toStringRendersXML,
      final boolean absentIsEmpty) {
    final Object defaultInvokerObject =
        DefaultDOMAccessInvoker.create(projectionInterface, node, projector, toStringRendersXML);
    final Map<MethodSignature, InvocationHandler> defaultInvocationHandlers =
        getDefaultInvokers(defaultInvokerObject);

    for (Entry<Class<?>, Object> e : mixins.entrySet()) {
      for (Method m : e.getKey().getMethods()) {
        mixinHandlers.put(
            MethodSignature.forMethod(m), new MixinInvoker(e.getValue(), projectionInterface));
      }
    }

    handlers.putAll(defaultInvocationHandlers);

    List<Class<?>> allSuperInterfaces =
        ReflectionHelper.findAllSuperInterfaces(projectionInterface);
    for (Class<?> i7e : allSuperInterfaces) {
      for (Method m : i7e.getDeclaredMethods()) {
        if (Modifier.isPrivate(m.getModifiers())) {
          // ignore private methods
          continue;
        }
        final MethodSignature methodSignature = MethodSignature.forMethod(m);
        if (ReflectionHelper.isDefaultMethod(m)) {
          handlers.put(methodSignature, DEFAULT_METHOD_INVOCATION_HANDLER);
          final XBOverride xbOverride = m.getAnnotation(XBOverride.class);
          if (xbOverride != null) {
            handlers.put(
                methodSignature.overridenBy(xbOverride.value()),
                new OverrideByDefaultMethodInvocationHandler(m));
          }
          continue;
        }
        if (defaultInvocationHandlers.containsKey(methodSignature)) {
          continue;
        }
        {
          final XBRead readAnnotation = m.getAnnotation(XBRead.class);
          if (readAnnotation != null) {
            handlers.put(
                methodSignature,
                new ReadInvocationHandler(
                    node, m, readAnnotation.value(), projector, absentIsEmpty));
            continue;
          }
        }
        {
          final XBUpdate updateAnnotation = m.getAnnotation(XBUpdate.class);
          if (updateAnnotation != null) {
            handlers.put(
                methodSignature,
                new UpdateInvocationHandler(node, m, updateAnnotation.value(), projector));
            continue;
          }
        }
        {
          final XBWrite writeAnnotation = m.getAnnotation(XBWrite.class);
          if (writeAnnotation != null) {
            handlers.put(
                methodSignature,
                new WriteInvocationHandler(node, m, writeAnnotation.value(), projector));
            continue;
          }
        }
        {
          final XBDelete delAnnotation = m.getAnnotation(XBDelete.class);
          if (delAnnotation != null) {
            handlers.put(
                methodSignature,
                new DeleteInvocationHandler(node, m, delAnnotation.value(), projector));
            continue;
          }
        }

        if (mixinHandlers.containsKey(methodSignature)) {
          continue;
        }

        throw new IllegalArgumentException(
            "I don't known how to handle method "
                + m
                + ". Did you forget to add a XB*-annotation or to register a mixin?");
      }
    }
  }
 @Override
 public Object invoke(final Object proxy, final Method method, final Object[] args)
     throws Throwable {
   return ReflectionHelper.invokeDefaultMethod(method, args, proxy);
 }
    @Override
    public Object invokeProjection(
        final String resolvedXpath, final Object proxy, final Object[] args) throws Throwable {
      //   final String pathToElement = resolvedXpath.replaceAll("\\[@",
      // "[attribute::").replaceAll("/?@.*", "").replaceAll("\\[attribute::", "[@");
      lastInvocationContext.updateMethodArgs(args);
      final Document document = DOMHelper.getOwnerDocumentFor(node);
      assert document != null;
      final Object valueToSet = args[findIndexOfValue];
      final boolean isMultiValue = isMultiValue(method.getParameterTypes()[findIndexOfValue]);
      // ROOT element update
      if ("/*".equals(resolvedXpath)) { // Setting a new root element.
        if (isMultiValue) {
          throw new IllegalArgumentException(
              "Method "
                  + method
                  + " was invoked as setter changing the document root element, but tries to set multiple values.");
        }
        return handeRootElementReplacement(proxy, method, document, valueToSet);
      }
      final boolean wildCardTarget = resolvedXpath.endsWith("/*");
      try {
        if (!lastInvocationContext.isStillValid(resolvedXpath)) {
          final DuplexExpression duplexExpression =
              wildCardTarget
                  ? new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping())
                      .compile(resolvedXpath.substring(0, resolvedXpath.length() - 2))
                  : new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping())
                      .compile(resolvedXpath);
          MethodParamVariableResolver resolver = null;
          if (duplexExpression.isUsingVariables()) {
            resolver =
                new MethodParamVariableResolver(
                    method, args, duplexExpression, projector.config().getStringRenderer(), null);
            duplexExpression.setXPathVariableResolver(resolver);
          }
          Class<?> targetComponentType = findTargetComponentType(method);
          lastInvocationContext =
              new InvocationContext(
                  resolvedXpath,
                  null,
                  null,
                  duplexExpression,
                  resolver,
                  targetComponentType,
                  projector);
        }
        final DuplexExpression duplexExpression = lastInvocationContext.getDuplexExpression();
        if (duplexExpression.getExpressionType().isMustEvalAsString()) {
          throw new XBPathException("Unwriteable xpath selector used ", method, resolvedXpath);
        }
        // MULTIVALUE
        if (isMultiValue) {
          if (duplexExpression.getExpressionType().equals(ExpressionType.ATTRIBUTE)) {
            throw new IllegalArgumentException(
                "Method "
                    + method
                    + " was invoked as setter changing some attribute, but was declared to set multiple values. I can not create multiple attributes for one path.");
          }
          final Iterable<?> iterable2Set =
              valueToSet == null
                  ? Collections.emptyList()
                  : (valueToSet.getClass().isArray())
                      ? ReflectionHelper.array2ObjectList(valueToSet)
                      : (Iterable<?>) valueToSet;
          if (wildCardTarget) {
            // TODO: check support of ParameterizedType e.g. Supplier
            final Element parentElement = (Element) duplexExpression.ensureExistence(node);
            DOMHelper.removeAllChildren(parentElement);
            int count = 0;
            for (Object o : iterable2Set) {
              if (o == null) {
                continue;
              }
              ++count;
              if (o instanceof Node) {
                DOMHelper.appendClone(parentElement, (Node) o);
                continue;
              }
              if (o instanceof DOMAccess) {
                DOMHelper.appendClone(parentElement, ((DOMAccess) o).getDOMBaseElement());
                continue;
              }
              throw new XBPathException(
                  "When using a wildcard target, the type to set must be a DOM Node or another projection. Otherwise I can not determine the element name.",
                  method,
                  resolvedXpath);
            }
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
          }
          final Element parentElement = duplexExpression.ensureParentExistence(node);
          duplexExpression.deleteAllMatchingChildren(parentElement);
          int count = applyIterableSetOnElement(iterable2Set, parentElement, duplexExpression);
          return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
        }

        // ATTRIBUTES
        if (duplexExpression.getExpressionType().equals(ExpressionType.ATTRIBUTE)) {
          if (wildCardTarget) {
            // TODO: This may never happen, right?
            throw new XBPathException(
                "Wildcards are not allowed when writing to an attribute. I need to know to which Element I should set the attribute",
                method,
                resolvedXpath);
          }
          Attr attribute = (Attr) duplexExpression.ensureExistence(node);
          if (valueToSet == null) {
            attribute.getOwnerElement().removeAttributeNode(attribute);
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
          }

          DOMHelper.setStringValue(attribute, valueToSet.toString());
          return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
        }

        if ((valueToSet instanceof Node) || (valueToSet instanceof DOMAccess)) {
          if (valueToSet instanceof Attr) {
            if (wildCardTarget) {
              throw new XBPathException(
                  "Wildcards are not allowed when writing an attribute. I need to know to which Element I should set the attribute",
                  method,
                  resolvedXpath);
            }
            Element parentNode = duplexExpression.ensureParentExistence(node);
            if (((Attr) valueToSet).getNamespaceURI() != null) {
              parentNode.setAttributeNodeNS((Attr) valueToSet);
              return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
            }
            parentNode.setAttributeNode((Attr) valueToSet);
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
          }
          final Element newNodeOrigin =
              valueToSet instanceof DOMAccess
                  ? ((DOMAccess) valueToSet).getDOMBaseElement()
                  : (Element) valueToSet;
          final Element newNode = (Element) newNodeOrigin.cloneNode(true);
          DOMHelper.ensureOwnership(document, newNode);
          if (wildCardTarget) {
            Element parentElement = (Element) duplexExpression.ensureExistence(node);
            DOMHelper.removeAllChildren(parentElement);
            parentElement.appendChild(newNode);
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
          }
          Element previousElement = (Element) duplexExpression.ensureExistence(node);

          DOMHelper.replaceElement(previousElement, newNode);
          return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
        }

        final Element elementToChange = (Element) duplexExpression.ensureExistence(node);
        if (valueToSet == null) {
          // TODO: This should depend on the parameter type?
          // If param type == String, no structural change might be expected.
          DOMHelper.removeAllChildren(elementToChange);
        } else {
          final String asString =
              projector
                  .config()
                  .getStringRenderer()
                  .render(
                      valueToSet.getClass(),
                      valueToSet,
                      duplexExpression.getExpressionFormatPattern());
          elementToChange.setTextContent(asString);
        }
        return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
      } catch (XBPathParsingException e) {
        throw new XBPathException(e, method, resolvedXpath);
      }
    }
    private Object invokeReadProjection(
        final InvocationContext invocationContext, final Object proxy, final Object[] args)
        throws Throwable {
      final Node node = getNodeForMethod(method, args);
      final ExpressionType expressionType =
          invocationContext.getDuplexExpression().getExpressionType();
      final XPathExpression expression = invocationContext.getxPathExpression();

      if (isConvertable) {
        String data;
        if (expressionType.isMustEvalAsString()) {
          data = (String) expression.evaluate(node, XPathConstants.STRING);
        } else {
          Node dataNode = (Node) expression.evaluate(node, XPathConstants.NODE);
          data = dataNode == null ? null : dataNode.getTextContent();
        }
        if ((data == null) && (absentIsEmpty)) {
          data = "";
        }

        try {
          final Object result =
              projector
                  .config()
                  .getTypeConverter()
                  .convertTo(returnType, data, invocationContext.getExpressionFormatPattern());
          return wrappedInOptional ? ReflectionHelper.createOptional(result) : result;
        } catch (NumberFormatException e) {
          throw new NumberFormatException(
              e.getMessage() + " XPath was:" + invocationContext.getResolvedXPath());
        }
      }
      if (isReturnAsNode) {
        // Try to evaluate as node
        // if evaluated type does not match return type, ClassCastException will follow
        final Object result = expression.evaluate(node, XPathConstants.NODE);
        return wrappedInOptional ? ReflectionHelper.createOptional(result) : result;
      }
      if (isEvaluateAsList) {
        assert !wrappedInOptional : "Projection methods returning list will never return null";
        final List<?> result =
            DefaultXPathEvaluator.evaluateAsList(expression, node, method, invocationContext);
        return isReturnAsStream ? ReflectionHelper.toStream(result) : result;
      }
      if (isEvaluateAsArray) {
        assert !wrappedInOptional : "Projection methods returning array will never return null";
        final List<?> list =
            DefaultXPathEvaluator.evaluateAsList(expression, node, method, invocationContext);
        return list.toArray(
            (Object[])
                java.lang.reflect.Array.newInstance(returnType.getComponentType(), list.size()));
      }
      if (isEvaluateAsSubProjection) {
        final Node newNode = (Node) expression.evaluate(node, XPathConstants.NODE);
        if (newNode == null) {
          return wrappedInOptional ? ReflectionHelper.createOptional(null) : null;
        }
        final DOMAccess subprojection = (DOMAccess) projector.projectDOMNode(newNode, returnType);
        return wrappedInOptional ? ReflectionHelper.createOptional(subprojection) : subprojection;
      }
      throw new IllegalArgumentException(
          "Return type "
              + returnType
              + " of method "
              + method
              + " is not supported. Please change to an projection interface, a List, an Array or one of current type converters types:"
              + projector.config().getTypeConverter());
    }