package org.solrmarc.index.extractor.methodcall; import org.marc4j.marc.Record; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class MethodCallManager { /* a static singleton manager */ private static MethodCallManager theManager = new MethodCallManager(); private final Map<Object, String> perRecordInitMap = new ConcurrentHashMap<>(); private final Map<String, AbstractExtractorMethodCall<?>> extractorMethodCalls = new HashMap<>(); private final Map<String, AbstractMappingMethodCall<?>> mappingMethodCalls = new HashMap<>(); public static MethodCallManager instance() { return (theManager); } private Set<Class<?>> classes = new HashSet<>(); /* private to prevent multiple instances from being created */ private MethodCallManager() { } public void doneWithRecord(Record record) { String recId = record.getControlNumber(); synchronized (perRecordInitMap) { Iterator<String> deleteIterator = perRecordInitMap.values().iterator(); while (deleteIterator.hasNext()) { String value = deleteIterator.next(); if (value.equals(recId)) deleteIterator.remove(); } } } private boolean isPerRecordInitMethod(Method method) { final Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1 || !parameterTypes[0].equals(Record.class) || !void.class.isAssignableFrom(method.getReturnType()) || !Modifier.isPublic(method.getModifiers())) return(false); return(true); } private boolean isValidExtractorMethod(Method method) { final Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 0 || !parameterTypes[0].equals(Record.class) || (!Collection.class.isAssignableFrom(method.getReturnType()) && !String.class.isAssignableFrom(method.getReturnType())) || !Modifier.isPublic(method.getModifiers())) { return false; } for (int i = 1; i < parameterTypes.length; i++) { if (parameterTypes[i] != String.class) { return false; } } return true; } private boolean isValidMappingMethod(Method method) { final Class<?>[] parameterTypes = method.getParameterTypes(); if (!(parameterTypes.length > 0 && Modifier.isPublic(method.getModifiers())&& ((Collection.class.isAssignableFrom(parameterTypes[0]) // its first parameter is a collction && Collection.class.isAssignableFrom(method.getReturnType())) || // and it returns a collection (parameterTypes[0].equals(String.class) // OR its first parameter is a String && method.getReturnType().equals(String.class)) // and it returns a String ))) { return false; } for (int i = 1; i < parameterTypes.length; i++) { if (parameterTypes[i] != String.class) { return false; } } return true; } public void add(Object mixin) { final Class<?> addedParentClass = getAddedParentClass(mixin); Class<?> currentClass = mixin.getClass(); boolean addAsDefault = true; while (currentClass != null && currentClass != Object.class) { addAsDefault &= (currentClass != addedParentClass); add(mixin, currentClass, addAsDefault); currentClass = currentClass.getSuperclass(); } } private void add(Object mixin, Class<?> clazz, boolean addMethodsAsDefault) { classes.add(clazz); Method hasPerRecordInit = null; for (final Method method : clazz.getDeclaredMethods()) { if (isPerRecordInitMethod(method)) { hasPerRecordInit = method; if (!perRecordInitMap.containsKey(method)) { perRecordInitMap.put(method, ""); } } } for (final Method method : clazz.getDeclaredMethods()) { if (isValidExtractorMethod(method)) { final Class<?>[] parameterTypes = method.getParameterTypes(); AbstractExtractorMethodCall<?> methodCall = null; if (Collection.class.isAssignableFrom(method.getReturnType())) { methodCall = createMultiValueExtractorMethodCall(mixin, method, hasPerRecordInit, parameterTypes.length); } else if (String.class.isAssignableFrom(method.getReturnType())) { methodCall = createSingleValueExtractorMethodCall(mixin, method, hasPerRecordInit, parameterTypes.length); } if (addMethodsAsDefault) { // If there is already a custom method with the same name as this one replace // the default method definition with a placeholder. if (extractorMethodCalls.containsKey(toCacheKey(method, parameterTypes))) { extractorMethodCalls.put(toCacheKey(method, parameterTypes), null); } else { extractorMethodCalls.put(toCacheKey(method, parameterTypes), methodCall); } } extractorMethodCalls.put(toCacheKey(mixin, method, parameterTypes), methodCall); } else if (isValidMappingMethod(method)) { final Class<?>[] parameterTypes = method.getParameterTypes(); AbstractMappingMethodCall<?> methodCall = null; if (Collection.class.isAssignableFrom(method.getReturnType())) { methodCall = createMultiValueMappingMethodCall(mixin, method); } else if (method.getReturnType().equals(String.class)) { methodCall = createSingleValueMappingMethodCall(mixin, method); } if (addMethodsAsDefault) { mappingMethodCalls.put(toCacheKey(method, parameterTypes), methodCall); } mappingMethodCalls.put(toCacheKey(mixin, method, parameterTypes), methodCall); } } } protected AbstractMappingMethodCall<?> createMultiValueMappingMethodCall(Object object, Method method) { return new MultiValueMappingMethodCall(object, method); } protected AbstractMappingMethodCall<?> createSingleValueMappingMethodCall(Object object, Method method) { return new SingleValueMappingMethodCall(object, method); } protected SingleValueExtractorMethodCall createSingleValueExtractorMethodCall(Object object, Method method, Method perRecordInit, int numParameters) { return new SingleValueExtractorMethodCall(object, method, perRecordInit, numParameters); } protected MultiValueExtractorMethodCall createMultiValueExtractorMethodCall(Object object, Method method, Method perRecordInit, int numParameters) { return new MultiValueExtractorMethodCall(object, method, perRecordInit, numParameters); } /** * Given a mixin, this method finds a parent class of the mixin which was * added before. * * @param mixin * the object which should be added. * @return previous added parent class or null, if none was added before. */ private Class<?> getAddedParentClass(Object mixin) { Class<?> mixinClass = mixin.getClass().getSuperclass(); while (mixinClass != null) { if (classes.contains(mixinClass)) { return mixinClass; } mixinClass = mixinClass.getSuperclass(); } return null; } public AbstractExtractorMethodCall<?> getExtractorMethodCallForContext(MethodCallContext context) { final String key = toCacheKey(context.getObjectName(), context.getMethodName(), context.getParameterTypes()); return extractorMethodCalls.get(key); } public AbstractMappingMethodCall<?> getMappingMethodCallForContext(MethodCallContext context) { final String key = toCacheKey(context.getObjectName(), context.getMethodName(), context.getParameterTypes()); return mappingMethodCalls.get(key); } private String toCacheKey(Object mixin, Method method, Class<?>... pameterTypes) { return toCacheKey(mixin.getClass().getName(), method.getName(), pameterTypes); } private String toCacheKey(Method method, Class<?>... parameterTypes) { return toCacheKey(null, method.getName(), parameterTypes); } private String toCacheKey(String className, String methodName, Class<?>... parameterTypes) { return className + ';' + methodName + ';' + parameterTypes.length; } public String loadedExtractorMixinsToString() { // get all Extractor Mixins and return them in a printable string return loadedExtractorMixinsToString(getLoadedExtractorMixinsMatches(null, null, -1)); } public List<AbstractExtractorMethodCall<?>> getLoadedExtractorMixinsMatches(String classNameToMatch, String methodNameToMatch, int numParameters) { List<AbstractExtractorMethodCall<?>> result = new ArrayList<>(); for (final String key : extractorMethodCalls.keySet()) { // Method calls are added twice. Once with a class name, once with // 'null' as class name. // It doesn't matter which one we take, but we don't want to show // both entries. if (!key.startsWith("null")) { final AbstractExtractorMethodCall<?> call = extractorMethodCalls.get(key); if (classNameToMatch == null || classNameToMatch.equals(call.getObjectName())) { if (methodNameToMatch == null || methodNameToMatch.equals(call.getMethodName())) { if (numParameters == -1 || numParameters == call.getNumParameters()) { result.add(call); } } } } } return(result); } public String loadedExtractorMixinsToString(List<AbstractExtractorMethodCall<?>> matches) { List<String> lines = new ArrayList<>(matches.size()); for (final AbstractExtractorMethodCall<?> call : matches) { lines.add("- " + call.getObjectName() + "::" + call.getMethodName()); } Collections.sort(lines); final StringBuilder buffer = new StringBuilder(); for (final String line : lines) { buffer.append(line).append('\n'); } return buffer.toString(); } // public String loadedExtractorMixinsToString(String methodNameToMatch) // { // List<String> lines = new ArrayList<>(extractorMethodCalls.size()); // for (final String key : extractorMethodCalls.keySet()) // { // // Method calls are added twice. Once with a class name, once with // // 'null' as class name. // // It doesn't matter which one we take, but we don't want to show // // both entries. // if (!key.startsWith("null")) // { // final AbstractExtractorMethodCall<?> call = extractorMethodCalls.get(key); // if (methodNameToMatch == null || methodNameToMatch.equals(call.getMethodName())) // { // lines.add("- " + call.getObjectName() + "::" + call.getMethodName()); // } // } // } // Collections.sort(lines); // final StringBuilder buffer = new StringBuilder(); // for (final String line : lines) // { // buffer.append(line).append('\n'); // } // return buffer.toString(); // } public String loadedMappingMixinsToString() { List<String> lines = new ArrayList<>(mappingMethodCalls.size()); for (final String key : mappingMethodCalls.keySet()) { // Method calls are added twice. Once with a class name, once with // 'null' as class name. // It doesn't matter which one we take, but we don't want to show // both entries. if (!key.startsWith("null")) { final AbstractMappingMethodCall<?> call = mappingMethodCalls.get(key); lines.add("- " + call.getObjectName() + "::" + call.getMethodName()); } } Collections.sort(lines); final StringBuilder buffer = new StringBuilder(); for (final String line : lines) { buffer.append(line).append('\n'); } return buffer.toString().trim(); } // public final String getRecordLastCalledFor(Method perRecordInit) // { // final String result = perRecordInitMap.get(perRecordInit); // return(result); // } public final boolean alreadyCalledFor(Object objectWithPerRecordInit, Object record) { String recId = ""; if (record instanceof Record) { Record rec = (Record)record; recId = rec.getControlNumber(); } boolean returnValue = true; synchronized (perRecordInitMap) { final String result = perRecordInitMap.get(objectWithPerRecordInit); if (result == null || !result.equals(recId)) { perRecordInitMap.put(objectWithPerRecordInit, recId); returnValue = false; } } return(returnValue); } // public final void setRecordLastCalledFor(Method perRecordInit, String recordID) // { // perRecordInitMap.put(perRecordInit, recordID); // } }