| /* 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(); |
| } |
| } |
| } |