package org.solrmarc.index.extractor.methodcall;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.solrmarc.index.extractor.AbstractValueExtractor;
import org.solrmarc.index.extractor.AbstractValueExtractorFactory;
import org.solrmarc.index.indexer.IndexerSpecException;
import org.solrmarc.index.indexer.ValueIndexerFactory;
public abstract class AbstractMethodCallFactory extends AbstractValueExtractorFactory
{
protected final MethodCallManager methodCallManager;
protected boolean haveShownKnownMethods = false;
public AbstractMethodCallFactory()
{
this(MethodCallManager.instance());
}
public AbstractMethodCallFactory(final MethodCallManager methodCallManager)
{
this.methodCallManager = methodCallManager;
}
public void addMethodsFromClasses(Collection<Class<?>> classes)
{
for (final Class<?> aClass : classes)
{
Object instance = createThreadLocalObjectForSpecifiedClass(aClass);
if (instance != null)
methodCallManager.add(instance);
}
}
/*
* Warning here there be Dragons!
*
* This Map and the subsequent method creates object instances for potentially non-thread-safe
* externally loaded objects containing custom methods. Each indexing Thread will have a separate instance of
* of the object to avoid the problem of ensuring the external methods are thread safe.
*
*/
private final static ThreadLocal<Map<Class<?>, Object>> threadLocalObjectMap = new ThreadLocal<Map<Class<?>, Object>>()
{
@Override
protected Map<Class<?>, Object> initialValue()
{
return new LinkedHashMap<>();
}
};
static public Object createThreadLocalObjectForSpecifiedClass(Class<?> aClass)
{
Map<Class<?>, Object> instanceMap = threadLocalObjectMap.get();
if (instanceMap.containsKey(aClass))
{
return(instanceMap.get(aClass));
}
Object toReturn;
try
{
Constructor<?> ctor = aClass.getConstructor();
toReturn = ctor.newInstance();
}
catch (NoSuchMethodException | SecurityException e)
{
// can't call no-args constructor, check whether class extends org.solrmarc.index.SolrIndexer
// for backwards compatibility sake.
Class<?> solrIndexerClass = org.solrmarc.index.SolrIndexer.class;
if (solrIndexerClass.isAssignableFrom(aClass))
{
try
{
Constructor<?> ctor2 = aClass.getConstructor(String.class, String[].class);
// the placeholder stub implementation for org.solrmarc.index.SolrIndexer
// doesn't look at use the parameters that the Constructor requires, so fake values are used
toReturn = ctor2.newInstance("", ValueIndexerFactory.instance().getHomeDirs());
}
catch (NoSuchMethodException | SecurityException e1)
{
throw new RuntimeException("Cannot call constructor for legacy class derived from old SolrIndexer, you'll need to edit your source code", e);
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e2)
{
throw new RuntimeException("Cannot call constructor for legacy class derived from old SolrIndexer, you'll need to edit your source code", e);
}
}
else
{
/* dynamically loaded class that has no default constructor, but also is not a legacy class that extends SolrIndexer,
* therefore don't look for custom extractor methods or custom mapping methods in the class
*/
toReturn = null;
}
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
throw new RuntimeException(e);
}
instanceMap.put(aClass, toReturn);
return(toReturn);
}
public AbstractValueExtractor<?> createExtractor(final String solrFieldName, MethodCallContext context)
{
final AbstractExtractorMethodCall<?> methodCall = methodCallManager.getExtractorMethodCallForContext(context);
if (methodCall != null)
{
return createExtractorForMethodCall(methodCall, context);
}
// most of the rest of this method is figuring out how to describe what went wrong to the user.
else if (methodCall == null && context.getObjectName() == null)
{
List<AbstractExtractorMethodCall<?>> matches = methodCallManager.getLoadedExtractorMixinsMatches(null, context.getMethodName(), context.getParameterTypes().length);
if (matches.size() > 1)
{
// This finds instances where a superclass and a subclass both implement the same method, and automatically selects the subclass implementation.
// Note that if the subclass doesn't implement a particular method, it will still be listed as an available method for the subclass,
// and it will be called on the subclass object, which will then pass the call to the superclass implementation.
if (matches.size() == 2)
{
Class<?> class0 = matches.get(0).getObjectClass();
Class<?> class1 = matches.get(1).getObjectClass();
if (!class0.equals(class1) && class0.isAssignableFrom(class1)) // class0 is superclass of class1
{
AbstractExtractorMethodCall<?> derivedMethodCall = matches.get(1);
return createExtractorForMethodCall(derivedMethodCall, context);
}
else if (!class0.equals(class1) && class1.isAssignableFrom(class0)) // class1 is superclass of class0
{
AbstractExtractorMethodCall<?> derivedMethodCall = matches.get(0);
return createExtractorForMethodCall(derivedMethodCall, context);
}
}
throw new IndexerSpecException("Multiple methods with name: " + context.getMethodName() + " you must specify the class of the method you intend to use. Known methods are: \n"
+ methodCallManager.loadedExtractorMixinsToString(matches));
}
else if (matches.size() == 0)
{
List<AbstractExtractorMethodCall<?>> matchesParmWildcard = methodCallManager.getLoadedExtractorMixinsMatches(null, context.getMethodName(), -1);
if (matchesParmWildcard.size() == 1)
{
int num = (matchesParmWildcard.iterator().next().getNumParameters()-1);
throw new IndexerSpecException("Incorrect number of parameters to method: " + context.getMethodName() + " The known method "+methodCallManager.loadedExtractorMixinsToString(matchesParmWildcard)+
" requires "+ num + " parameter" +((num == 1) ? "" : "s") + "\n");
}
else if (matchesParmWildcard.size() > 1)
{
throw new IndexerSpecException("Multiple methods with name: " + context.getMethodName() + " but none of them require " + context.getParameterTypes().length + " parameters. Known methods are: \n"
+ methodCallManager.loadedExtractorMixinsToString(matchesParmWildcard));
}
}
if (!haveShownKnownMethods)
{
haveShownKnownMethods = true;
throw new IndexerSpecException("Unknown extractor method: " + context.toString() + ". Known methods are: \n"
+ methodCallManager.loadedExtractorMixinsToString());
}
else
{
throw new IndexerSpecException("Unknown extractor method: " + context.toString());
}
}
else
{
List<AbstractExtractorMethodCall<?>> matchesOtherContext = methodCallManager.getLoadedExtractorMixinsMatches(null, context.getMethodName(), context.getParameterTypes().length);
if (matchesOtherContext.size() == 1)
{
String objName = matchesOtherContext.iterator().next().getObjectName();
throw new IndexerSpecException("Method not found in specified class: " + context.getObjectName() + " A known method does exist in the class : " + objName + "\n");
}
List<AbstractExtractorMethodCall<?>> matchesParmWildcard = methodCallManager.getLoadedExtractorMixinsMatches(context.getObjectName(), context.getMethodName(), -1);
if (matchesParmWildcard.size() == 1)
{
int num = (matchesParmWildcard.iterator().next().getNumParameters()-1);
throw new IndexerSpecException("Incorrect number of parameters to method: " + context.getMethodName() + " The known method "+methodCallManager.loadedExtractorMixinsToString(matchesParmWildcard)+
" requires "+ num + " parameter" +((num == 1) ? "" : "s") + "\n");
}
List<AbstractExtractorMethodCall<?>> matchesOtherContextParmWildCard = methodCallManager.getLoadedExtractorMixinsMatches(null, context.getMethodName(), -1);
if (matchesOtherContextParmWildCard.size() == 1)
{
@SuppressWarnings("unused")
AbstractExtractorMethodCall<?> match = matchesOtherContextParmWildCard.iterator().next();
throw new IndexerSpecException("Specified method with name: " + context.getMethodName() + " not found. Closest match is: \n"
+ methodCallManager.loadedExtractorMixinsToString(matchesOtherContextParmWildCard));
}
else if (matchesOtherContextParmWildCard.size() > 1)
{
throw new IndexerSpecException("Multiple methods with name: " + context.getMethodName() + " but none of them require " + context.getParameterTypes().length + " parameters. Known methods are: \n"
+ methodCallManager.loadedExtractorMixinsToString(matchesOtherContextParmWildCard));
}
else if (!haveShownKnownMethods)
{
haveShownKnownMethods = true;
throw new IndexerSpecException("Unknown extractor method: " + context.toString() + ". Known methods are: \n"
+ methodCallManager.loadedExtractorMixinsToString());
}
else
{
throw new IndexerSpecException("Unknown extractor method: " + context.toString());
}
}
}
private AbstractValueExtractor<?> createExtractorForMethodCall(AbstractExtractorMethodCall<?> methodCall, MethodCallContext context)
{
if (methodCall instanceof MultiValueExtractorMethodCall)
{
return new MethodCallMultiValueExtractor((MultiValueExtractorMethodCall) methodCall, context.getParameters());
}
else if (methodCall instanceof SingleValueExtractorMethodCall)
{
return new MethodCallSingleValueExtractor((SingleValueExtractorMethodCall) methodCall, context.getParameters());
}
return(null);
}
@Override
public AbstractValueExtractor<?> createExtractor(final String solrFieldName, final String[] parts)
{
MethodCallContext context = MethodCallContext.parseContextFromExtractorParts(parts);
return createExtractor(solrFieldName, context);
}
}