package org.apache.solr.search.xjoin;
/*
* 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.
*/
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import org.apache.lucene.index.BinaryDocValues;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.search.FunctionQParser;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.search.ValueSourceParser;
/**
* ValueSourceParser to provide a function for retrieving the value of a field from
* external process results (for use in sort spec, boost function, etc.)
*/
public class XJoinValueSourceParser extends ValueSourceParser {
// the name of the associated XJoinSearchComponent - could be null
private String componentName;
// the attribute to examine in external results - could be null
private String attribute;
// the default value if the results don't have an entry
private double defaultValue;
// the default value if a result exists but does not have the required field
private double fieldDefaultValue;
/**
* Initialise from configuration.
*/
@Override
@SuppressWarnings("rawtypes")
public void init(NamedList args) {
super.init(args);
componentName = (String)args.get(XJoinParameters.INIT_XJOIN_COMPONENT_NAME);
attribute = (String)args.get(XJoinParameters.INIT_ATTRIBUTE);
Double defaultValue = (Double)args.get(XJoinParameters.INIT_DEFAULT_VALUE);
if (defaultValue != null) {
this.defaultValue = defaultValue;
}
Double fieldDefaultValue = (Double)args.get(XJoinParameters.INIT_FIELD_DEFAULT_VALUE);
this.fieldDefaultValue = fieldDefaultValue != null ? fieldDefaultValue : 0.0d;
if (componentName == null && attribute == null) {
throw new RuntimeException("At least one of " + XJoinParameters.INIT_XJOIN_COMPONENT_NAME +
" or " + XJoinParameters.INIT_ATTRIBUTE +
" must be specified");
}
}
/**
* Provide a ValueSource for external process results, which are obtained from the
* request context (having been placed there by XJoinSearchComponent).
*/
@Override
public ValueSource parse(FunctionQParser fqp) throws SyntaxError {
String componentName = this.componentName != null ? this.componentName : fqp.parseArg();
String attribute = this.attribute != null ? this.attribute : fqp.parseArg();
XJoinSearchComponent xJoin = (XJoinSearchComponent)fqp.getReq().getCore().getSearchComponent(componentName);
String joinField = xJoin.getJoinField();
XJoinResults<?> results = (XJoinResults<?>)fqp.getReq().getContext().get(xJoin.getResultsTag());
if (results == null) {
throw new RuntimeException("No xjoin results in request context");
}
return new XJoinValueSource(joinField, results, attribute);
}
/**
* ValueSource class for external process results.
*/
public class XJoinValueSource extends ValueSource {
// the join field
private String joinField;
// the external process results (generated by XJoinSearchComponent)
private XJoinResults<?> results;
// the attribute on external results objects to use as the value
private String attribute;
/**
* Create an ExternalValueSource for the given external process results, for
* extracting the named attribute.
*/
public XJoinValueSource(String joinField, XJoinResults<?> results, String attribute) {
this.joinField = joinField;
this.results = results;
this.attribute = attribute;
}
@Override
@SuppressWarnings("rawtypes")
public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
final BinaryDocValues joinValues = DocValues.getBinary(readerContext.reader(), joinField);
return new DoubleDocValues(this) {
@Override
public double doubleVal(int doc) {
BytesRef joinValue = joinValues.get(doc);
if (joinValue == null) {
throw new RuntimeException("No such doc: " + doc);
}
Object result = results.getResult(joinValue.utf8ToString());
if (result == null) {
return defaultValue;
}
if (result instanceof Iterable) {
Double max = null;
for (Object object : (Iterable)result) {
if (object != null) {
double value = getValue(object);
if (max == null || value > max) {
max = value;
}
}
}
return max != null ? max : defaultValue;
} else {
return getValue(result);
}
}
//FIXME TODO What is the calling convention? Can we cache the BytesRef in exists() for doubleVal()?
@Override
public boolean exists(int doc) {
BytesRef joinValue = joinValues.get(doc);
return joinValue != null;
}
};
}
// unbox numeric types for coercing into double, also handle null
// as a last resort, try to parse toString()
private double convertFieldValue(Object object) {
if (object == null) {
return fieldDefaultValue;
}
if (object instanceof Double) {
return (Double)object;
}
if (object instanceof Float) {
return (Float)object;
}
if (object instanceof Integer) {
return (Integer)object;
}
return Double.valueOf(object.toString());
}
@SuppressWarnings("rawtypes")
private double getValue(Object result) {
if (result instanceof Map) {
return convertFieldValue(((Map)result).get(attribute));
} else {
try {
String methodName = NameConverter.getMethodName(attribute);
Method method = result.getClass().getMethod(methodName);
return convertFieldValue(method.invoke(result));
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
@Override
public String description() {
return "$description$";
}
@Override
public boolean equals(Object object) {
if (! (object instanceof XJoinValueSource)) {
return false;
}
return results.equals(((XJoinValueSource)object).results);
}
@Override
public int hashCode() {
return results.hashCode();
}
}
}