/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.camel.component.dozer;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.camel.spi.ClassResolver;
/**
* Allows a user to customize a field mapping using a POJO that is not
* required to extend/implement Dozer-specific classes.
*/
public class CustomMapper extends BaseConverter {
private ClassResolver resolver;
public CustomMapper(ClassResolver resolver) {
this.resolver = resolver;
}
@Override
public Object convert(Object existingDestinationFieldValue,
Object sourceFieldValue,
Class<?> destinationClass,
Class<?> sourceClass) {
try {
return mapCustom(sourceFieldValue, sourceClass);
} finally {
done();
}
}
private Object invokeFunction(Method method,
Object customObj,
Object source,
String[][] parameters) throws Exception {
Class<?>[] prmTypes = method.getParameterTypes();
Object[] methodPrms = new Object[prmTypes.length];
methodPrms[0] = source;
for (int parameterNdx = 0, methodPrmNdx = 1; parameterNdx < parameters.length; parameterNdx++, methodPrmNdx++) {
if (method.isVarArgs() && methodPrmNdx == prmTypes.length - 1) {
Object array = Array.newInstance(prmTypes[methodPrmNdx].getComponentType(), parameters.length - parameterNdx);
for (int arrayNdx = 0; parameterNdx < parameters.length; parameterNdx++, arrayNdx++) {
String[] parts = parameters[parameterNdx];
Array.set(array, arrayNdx, resolver.resolveClass(parts[0]).getConstructor(String.class).newInstance(parts[1]));
}
methodPrms[methodPrmNdx] = array;
} else {
String[] parts = parameters[parameterNdx];
methodPrms[methodPrmNdx] = resolver.resolveClass(parts[0]).getConstructor(String.class).newInstance(parts[1]);
}
}
return method.invoke(customObj, methodPrms);
}
Object mapCustom(Object source, Class<?> sourceClass) {
// The converter parameter is stored in a thread local variable, so
// we need to parse the parameter on each invocation
// ex: custom-converter-param="org.example.MyMapping,map"
// className = org.example.MyMapping
// operation = map
String[] prms = getParameter().split(",");
String className = prms[0];
String operation = prms.length > 1 ? prms[1] : null;
// now attempt to process any additional parameters passed along
// ex: custom-converter-param="org.example.MyMapping,substring,java.lang.Integer=3,java.lang.Integer=10"
// className = org.example.MyMapping
// operation = substring
// parameters = ["java.lang.Integer=3","java.lang.Integer=10"]
String[][] prmTypesAndValues;
if (prms.length > 2) {
// Break parameters down into types and values
prmTypesAndValues = new String[prms.length - 2][2];
for (int ndx = 0; ndx < prmTypesAndValues.length; ndx++) {
String prm = prms[ndx + 2];
String[] parts = prm.split("=");
if (parts.length != 2) {
throw new RuntimeException("Value missing for parameter " + prm);
}
prmTypesAndValues[ndx][0] = parts[0];
prmTypesAndValues[ndx][1] = parts[1];
}
} else {
prmTypesAndValues = null;
}
Object customObj;
Method method;
try {
Class<?> customClass = resolver.resolveMandatoryClass(className);
customObj = customClass.newInstance();
// If a specific mapping operation has been supplied use that
if (operation != null && prmTypesAndValues != null) {
method = selectMethod(customClass, operation, sourceClass, prmTypesAndValues);
} else if (operation != null) {
method = customClass.getMethod(operation, sourceClass);
} else {
method = selectMethod(customClass, sourceClass);
}
} catch (Exception e) {
throw new RuntimeException("Failed to load custom function", e);
}
// Verify that we found a matching method
if (method == null) {
throw new RuntimeException("No eligible custom function methods in " + className);
}
// Invoke the custom mapping method
try {
if (prmTypesAndValues != null) {
return invokeFunction(method, customObj, source, prmTypesAndValues);
} else {
return method.invoke(customObj, source);
}
} catch (Exception e) {
throw new RuntimeException("Error while invoking custom function", e);
}
}
private boolean parametersMatchParameterList(Class<?>[] prmTypes,
String[][] parameters) {
int ndx = 0;
while (ndx < prmTypes.length) {
Class<?> prmType = prmTypes[ndx];
if (ndx >= parameters.length) {
return ndx == prmTypes.length - 1 && prmType.isArray();
}
if (ndx == prmTypes.length - 1 && prmType.isArray()) { // Assume this only occurs for functions with var args
Class<?> varArgClass = prmType.getComponentType();
while (ndx < parameters.length) {
Class<?> prmClass = resolver.resolveClass(parameters[ndx][0]);
if (!varArgClass.isAssignableFrom(prmClass)) {
return false;
}
ndx++;
}
} else {
Class<?> prmClass = resolver.resolveClass(parameters[ndx][0]);
if (!prmTypes[ndx].isAssignableFrom(prmClass)) {
return false;
}
}
ndx++;
}
return true;
}
Method selectMethod(Class<?> customClass,
Class<?> sourceClass) {
Method method = null;
for (Method m : customClass.getDeclaredMethods()) {
if (m.getReturnType() != null
&& m.getParameterTypes().length == 1
&& m.getParameterTypes()[0].isAssignableFrom(sourceClass)) {
method = m;
break;
}
}
return method;
}
// Assumes source is a separate parameter in method even if it has var args and that there are no
// ambiguous calls based upon number and types of parameters
private Method selectMethod(Class<?> customClass,
String operation,
Class<?> sourceClass,
String[][] parameters) {
// Create list of potential methods
List<Method> methods = new ArrayList<>();
for (Method method : customClass.getDeclaredMethods()) {
methods.add(method);
}
// Remove methods that are not applicable
for (Iterator<Method> iter = methods.iterator(); iter.hasNext();) {
Method method = iter.next();
Class<?>[] prmTypes = method.getParameterTypes();
if (!method.getName().equals(operation)
|| method.getReturnType() == null
|| !prmTypes[0].isAssignableFrom(sourceClass)) {
iter.remove();
continue;
}
prmTypes = Arrays.copyOfRange(prmTypes, 1, prmTypes.length); // Remove source from type list
if (!method.isVarArgs() && prmTypes.length != parameters.length) {
iter.remove();
continue;
}
if (!parametersMatchParameterList(prmTypes, parameters)) {
iter.remove();
continue;
}
}
// If more than one method is applicable, return the method whose prm list exactly matches the parameters
// if possible
if (methods.size() > 1) {
for (Method method : methods) {
if (!method.isVarArgs()) {
return method;
}
}
}
return methods.size() > 0 ? methods.get(0) : null;
}
}