blob: 66ec04639594cffeb2bcff9e202a9991dccd08d7 [file] [log] [blame]
/* Copyright 2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.xmlbeans.impl.config;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.type.ReferenceType;
import org.apache.xmlbeans.InterfaceExtension;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.impl.xb.xmlconfig.Extensionconfig;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
public class InterfaceExtensionImpl implements InterfaceExtension {
private NameSet _xbeanSet;
private String _interfaceClassName;
private String _delegateToClassName;
private MethodSignatureImpl[] _methods;
static InterfaceExtensionImpl newInstance(Parser loader, NameSet xbeanSet, Extensionconfig.Interface intfXO) {
InterfaceExtensionImpl result = new InterfaceExtensionImpl();
result._xbeanSet = xbeanSet;
ClassOrInterfaceDeclaration interfaceJClass = validateInterface(loader, intfXO.getName(), intfXO);
if (interfaceJClass == null)
{
BindingConfigImpl.error("Interface '" + intfXO.getStaticHandler() + "' not found.", intfXO);
return null;
}
result._interfaceClassName = interfaceJClass.getFullyQualifiedName().get();
result._delegateToClassName = intfXO.getStaticHandler();
ClassOrInterfaceDeclaration delegateJClass = validateClass(loader, result._delegateToClassName, intfXO);
if (delegateJClass == null) {
// no HandlerClass
BindingConfigImpl.warning("Handler class '" + intfXO.getStaticHandler() + "' not found on classpath, skip validation.", intfXO);
return result;
}
if (!result.validateMethods(interfaceJClass, delegateJClass, intfXO))
return null;
return result;
}
private static ClassOrInterfaceDeclaration validateInterface(Parser loader, String intfStr, XmlObject loc) {
return validateJava(loader, intfStr, true, loc);
}
static ClassOrInterfaceDeclaration validateClass(Parser loader, String clsStr, XmlObject loc) {
return validateJava(loader, clsStr, false, loc);
}
static ClassOrInterfaceDeclaration validateJava(Parser loader, String clsStr, boolean isInterface, XmlObject loc) {
if (loader==null) {
return null;
}
final String ent = isInterface ? "Interface" : "Class";
ClassOrInterfaceDeclaration cls = loader.loadSource(clsStr);
if (cls==null) {
BindingConfigImpl.error(ent + " '" + clsStr + "' not found.", loc);
return null;
}
if ( isInterface != cls.isInterface() ) {
BindingConfigImpl.error("'" + clsStr + "' must be " + (isInterface ? "an interface" : "a class") + ".", loc);
}
if (!cls.isPublic()) {
BindingConfigImpl.error(ent + " '" + clsStr + "' is not public.", loc);
}
return cls;
}
private boolean validateMethods(ClassOrInterfaceDeclaration interfaceJClass, ClassOrInterfaceDeclaration delegateJClass, XmlObject loc) {
_methods = interfaceJClass.getMethods().stream()
.map(m -> validateMethod(interfaceJClass, delegateJClass, m, loc))
.map(m -> m == null ? null : new MethodSignatureImpl(getStaticHandler(), m))
.toArray(MethodSignatureImpl[]::new);
return Stream.of(_methods).allMatch(Objects::nonNull);
}
private MethodDeclaration validateMethod(ClassOrInterfaceDeclaration interfaceJClass,
ClassOrInterfaceDeclaration delegateJClass, MethodDeclaration method, XmlObject loc) {
String methodName = method.getName().asString();
String[] delegateParams = Stream.concat(
Stream.of("org.apache.xmlbeans.XmlObject"),
Stream.of(paramStrings(method.getParameters()))
).toArray(String[]::new);
MethodDeclaration handlerMethod = getMethod(delegateJClass, methodName, delegateParams);
String delegateFQN = delegateJClass.getFullyQualifiedName().orElse("");
String methodFQN = methodName + "(" + method.getParameters().toString() + ")";
String interfaceFQN = interfaceJClass.getFullyQualifiedName().orElse("");
if (handlerMethod == null) {
BindingConfigImpl.error("Handler class '" + delegateFQN + "' does not contain method " + methodFQN, loc);
return null;
}
// check for throws exceptions
if (!Arrays.equals(exceptionStrings(method), exceptionStrings(handlerMethod))) {
BindingConfigImpl.error("Handler method '" + delegateFQN + "." + methodName + "' must declare the same " +
"exceptions as the interface method '" + interfaceFQN + "." + methodFQN, loc);
return null;
}
if (!handlerMethod.isPublic() || !handlerMethod.isStatic()) {
BindingConfigImpl.error("Method '" + delegateJClass.getFullyQualifiedName() + "." +
methodFQN + "' must be declared public and static.", loc);
return null;
}
String returnType = method.getTypeAsString();
if (!returnType.equals(handlerMethod.getTypeAsString())) {
BindingConfigImpl.error("Return type for method '" + returnType + " " + delegateFQN + "." + methodName +
"(...)' does not match the return type of the interface method :'" + returnType + "'.", loc);
return null;
}
return method;
}
static MethodDeclaration getMethod(ClassOrInterfaceDeclaration cls, String name, String[] paramTypes) {
// cls.getMethodsBySignature only checks the type name as-is ... i.e. if the type name is imported
// only the simple name is checked, otherwise the full qualified name
return cls.getMethodsByName(name).stream()
.filter(m -> parameterMatches(paramStrings(m.getParameters()), paramTypes))
.findFirst().orElse(null);
}
private static String[] paramStrings(NodeList<Parameter> params) {
return params.stream().map(Parameter::getTypeAsString).toArray(String[]::new);
}
private static String[] exceptionStrings(MethodDeclaration method) {
return method.getThrownExceptions().stream().map(ReferenceType::asString).toArray(String[]::new);
}
private static boolean parameterMatches(String[] params1, String[] params2) {
// compare all parameters type strings
// a type string can be a simple name (e.g. "XmlObject") or
// fully qualified name ("org.apache.xmlbeans.XmlObject")
// try to loosely match the names
if (params1.length != params2.length) {
return false;
}
for (int i=0; i<params1.length; i++) {
String p1 = params1[i];
String p2 = params2[i];
if (p1.contains(".")) {
String tmp = p1;
p1 = p2;
p2 = tmp;
}
if (!p2.endsWith(p1)) {
return false;
}
}
return true;
}
/* public getters */
public boolean contains(String fullJavaName) {
return _xbeanSet.contains(fullJavaName);
}
public String getStaticHandler() {
return _delegateToClassName;
}
public String getInterface() {
return _interfaceClassName;
}
public InterfaceExtension.MethodSignature[] getMethods() {
return _methods;
}
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(" static handler: ").append(_delegateToClassName).append("\n");
buf.append(" interface: ").append(_interfaceClassName).append("\n");
buf.append(" name set: ").append(_xbeanSet).append("\n");
for (int i = 0; i < _methods.length; i++)
buf.append(" method[").append(i).append("]=").append(_methods[i]).append("\n");
return buf.toString();
}
// this is used only for detecting method colisions of extending interfaces
static class MethodSignatureImpl implements InterfaceExtension.MethodSignature {
private final String _intfName;
private final int NOTINITIALIZED = -1;
private int _hashCode = NOTINITIALIZED;
private String _signature;
private final String _name;
private final String _return;
private final String[] _params;
private final String[] _exceptions;
MethodSignatureImpl(String intfName, MethodDeclaration method) {
if (intfName==null || method==null) {
throw new IllegalArgumentException("Interface: " + intfName + " method: " + method);
}
_intfName = intfName;
_signature = null;
_name = method.getName().asString();
_return = replaceInner(method.getTypeAsString());
_params = method.getParameters().stream().map(Parameter::getTypeAsString).
map(MethodSignatureImpl::replaceInner).toArray(String[]::new);
_exceptions = method.getThrownExceptions().stream().map(ReferenceType::asString).
map(MethodSignatureImpl::replaceInner).toArray(String[]::new);
}
private static String replaceInner(String classname) {
return classname.replace('$', '.');
}
String getInterfaceName() {
return _intfName;
}
public String getName() {
return _name;
}
public String getReturnType() {
return _return;
}
public String[] getParameterTypes() {
return _params;
}
public String[] getExceptionTypes() {
return _exceptions;
}
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof MethodSignatureImpl)) {
return false;
}
MethodSignatureImpl ms = (MethodSignatureImpl)o;
return ms.getName().equals(getName()) &&
_intfName.equals(ms._intfName) &&
Arrays.equals(getParameterTypes(),ms.getParameterTypes());
}
public int hashCode() {
return (_hashCode!=NOTINITIALIZED) ? _hashCode :
(_hashCode = Objects.hash(getName(), Arrays.hashCode(getParameterTypes()), _intfName));
}
String getSignature() {
return (_signature!=null) ? _signature :
(_signature = _name+"("+String.join(" ,", _params)+")");
}
public String toString() {
return getReturnType() + " " + getSignature();
}
}
}